diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 035ba59f..ef545f22 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,6 +11,13 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + # Uses of [[deprecated]] TrussC API in the repo's own sources + # (core/examples/tests, all jobs incl. web/android) fail the build + # (GCC/Clang only; see core/CMakeLists.txt + trussc_app.cmake). + # User projects never set this. + TC_DEPRECATED_ERRORS: 1 + jobs: # Path-based change detector. Emits docs_only=true when EVERY file in the diff # is documentation (any *.md, or anything under docs/ — prose + doc tooling). diff --git a/addons/tcxBox2d/example-collision/src/tcApp.cpp b/addons/tcxBox2d/example-collision/src/tcApp.cpp index 9cd853b2..2c67ec76 100644 --- a/addons/tcxBox2d/example-collision/src/tcApp.cpp +++ b/addons/tcxBox2d/example-collision/src/tcApp.cpp @@ -54,15 +54,14 @@ void tcApp::addBall(float x, float y) { // Add collision callback to ball auto* collider = ball->getCollider(); if (collider) { - ballListeners.emplace_back(); - collider->onCollisionEnter.listen(ballListeners.back(), [this](box2d::CollisionEvent& e) { + ballListeners.push_back(collider->onCollisionEnter.listen([this](box2d::CollisionEvent& e) { enterCount++; collisionMarkers.push_back({ e.contactPoint, 0.3f, Color(0.2f, 0.8f, 1.0f) // cyan for ball collisions }); - }); + })); } addChild(ball); diff --git a/addons/tcxCurl/src/tcxCurl_android.cpp b/addons/tcxCurl/src/tcxCurl_android.cpp index 392b64ae..f310e6cf 100644 --- a/addons/tcxCurl/src/tcxCurl_android.cpp +++ b/addons/tcxCurl/src/tcxCurl_android.cpp @@ -22,7 +22,7 @@ #include #include -#include "sokol_app.h" +#include "sokol_app_tc.h" #include diff --git a/addons/tcxDepthRecord/src/tcxDepthPlayback.h b/addons/tcxDepthRecord/src/tcxDepthPlayback.h index c19e3a95..6fc0d1d3 100644 --- a/addons/tcxDepthRecord/src/tcxDepthPlayback.h +++ b/addons/tcxDepthRecord/src/tcxDepthPlayback.h @@ -63,8 +63,8 @@ class PlaybackDepthCamera : public DepthCamera { protected: bool openDevice() override { - std::filesystem::path p(path_); - std::string resolved = p.is_relative() ? getDataPath(path_) : path_; + // getDataPath passes absolute paths through unchanged + std::filesystem::path resolved = getDataPath(path_); file_.open(resolved, std::ios::binary); if (!file_) { logError("tcxDepthRecord") << "PlaybackDepthCamera: cannot open " << resolved; diff --git a/addons/tcxDepthRecord/src/tcxDepthRecorder.h b/addons/tcxDepthRecord/src/tcxDepthRecorder.h index f742cba7..41e8075c 100644 --- a/addons/tcxDepthRecord/src/tcxDepthRecorder.h +++ b/addons/tcxDepthRecord/src/tcxDepthRecorder.h @@ -44,16 +44,15 @@ class DepthRecorder { std::uint32_t streams = REC_ALL, DepthCodecId depthCodec = DepthCodecId::HiloLZ4, ColorCodecId colorCodec = ColorCodecId::LZ4) { - std::filesystem::path p(path); - std::string resolved = p.is_relative() ? getDataPath(path) : path; - std::filesystem::path rp(resolved); + // getDataPath passes absolute paths through unchanged + std::filesystem::path rp = getDataPath(path); if (rp.has_parent_path()) { std::error_code ec; std::filesystem::create_directories(rp.parent_path(), ec); } file_.open(rp, std::ios::binary | std::ios::trunc); if (!file_) { - logError("tcxDepthRecord") << "DepthRecorder: cannot open " << resolved; + logError("tcxDepthRecord") << "DepthRecorder: cannot open " << rp; return false; } streams_ = streams; diff --git a/addons/tcxGltf/src/tcxGltf.cpp b/addons/tcxGltf/src/tcxGltf.cpp index 5ce78a43..f28c87c4 100644 --- a/addons/tcxGltf/src/tcxGltf.cpp +++ b/addons/tcxGltf/src/tcxGltf.cpp @@ -226,7 +226,7 @@ bool GltfModel::load(const string& path) { textures_.clear(); loaded_ = false; - string resolved = getDataPath(path); + string resolved = getDataPath(path).string(); // Parse with cgltf cgltf_options options = {}; diff --git a/addons/tcxHap/CMakeLists.txt b/addons/tcxHap/CMakeLists.txt index 3f7502bf..e3690107 100644 --- a/addons/tcxHap/CMakeLists.txt +++ b/addons/tcxHap/CMakeLists.txt @@ -24,6 +24,13 @@ set(SNAPPY_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(SNAPPY_BUILD_BENCHMARKS OFF CACHE BOOL "" FORCE) set(SNAPPY_INSTALL OFF CACHE BOOL "" FORCE) FetchContent_MakeAvailable(snappy) +if(EMSCRIPTEN) + # snappy compiles itself with -Werror on Clang; its '#pragma clang loop' + # vectorization hints cannot be honored under wasm, and the resulting + # -Wpass-failed diagnostic would fail the build. Silence it on the + # dependency target only. + target_compile_options(snappy PRIVATE -Wno-pass-failed) +endif() # Snappy adds -Werror for Clang builds; its `#pragma clang loop` hints can fail # on backends that can't vectorize them (e.g. wasm/em++), turning the diff --git a/addons/tcxHap/example-hap/src/tcApp.cpp b/addons/tcxHap/example-hap/src/tcApp.cpp index fe7f08ad..feac38e7 100644 --- a/addons/tcxHap/example-hap/src/tcApp.cpp +++ b/addons/tcxHap/example-hap/src/tcApp.cpp @@ -11,7 +11,7 @@ void tcApp::setup() { .bind([this](const json& args) -> json { string path = args.at("path").get(); logNotice("tcApp") << "MCP: Loading " << path; - bool success = player_.load(path); + bool success = player_.load(path).ok(); if (success) { player_.play(); statusText_ = "Loaded: " + path; diff --git a/addons/tcxHap/src/tcxHapPlayer.h b/addons/tcxHap/src/tcxHapPlayer.h index 1024cd94..7774a6a3 100644 --- a/addons/tcxHap/src/tcxHapPlayer.h +++ b/addons/tcxHap/src/tcxHapPlayer.h @@ -74,16 +74,23 @@ class HapPlayer : public tc::VideoPlayerBase { // Load / Close // ========================================================================= - bool load(const std::string& path) override { + tc::LoadResult load(const tc::fs::path& path) override { if (initialized_) { close(); } resetStats(); + std::error_code ec; + if (!tc::fs::exists(path, ec)) { + return tc::LoadResult::fail(tc::LoadError::FileNotFound, + "file not found: " + tc::internal::pathToUtf8(path)); + } + // Parse MOV file if (!movParser_.open(path)) { tc::logError("HapPlayer") << "Failed to open: " << path; - return false; + return tc::LoadResult::fail(tc::LoadError::DecodeFailed, + "failed to parse MOV: " + tc::internal::pathToUtf8(path)); } const auto& info = movParser_.getInfo(); @@ -92,14 +99,17 @@ class HapPlayer : public tc::VideoPlayerBase { if (!videoTrack) { tc::logError("HapPlayer") << "No video track found"; movParser_.close(); - return false; + return tc::LoadResult::fail(tc::LoadError::DecodeFailed, + "no video track found"); } if (!videoTrack->isHap()) { tc::logError("HapPlayer") << "Not a HAP codec (FourCC: " << MovParser::fourccToString(videoTrack->codecFourCC) << ")"; movParser_.close(); - return false; + return tc::LoadResult::fail(tc::LoadError::UnsupportedFormat, + "not a HAP codec (FourCC: " + + MovParser::fourccToString(videoTrack->codecFourCC) + ")"); } // Store video info @@ -120,7 +130,8 @@ class HapPlayer : public tc::VideoPlayerBase { if (hapFormat_ == HapFormat::Unknown) { tc::logError("HapPlayer") << "Could not determine HAP format"; movParser_.close(); - return false; + return tc::LoadResult::fail(tc::LoadError::DecodeFailed, + "could not determine HAP format"); } // Allocate frame buffer for decoded DXT data @@ -131,7 +142,8 @@ class HapPlayer : public tc::VideoPlayerBase { if (!createCompressedTexture()) { tc::logError("HapPlayer") << "Failed to create compressed texture"; movParser_.close(); - return false; + return tc::LoadResult::fail(tc::LoadError::Unknown, + "failed to create compressed texture"); } // Load audio track if available @@ -147,7 +159,7 @@ class HapPlayer : public tc::VideoPlayerBase { initialized_ = true; currentFrame_ = 0; - return true; + return tc::LoadResult::success(); } void close() override { diff --git a/addons/tcxHap/src/tcxMovParser.h b/addons/tcxHap/src/tcxMovParser.h index 184c51b7..62cb241a 100644 --- a/addons/tcxHap/src/tcxMovParser.h +++ b/addons/tcxHap/src/tcxMovParser.h @@ -185,7 +185,7 @@ class MovParser { return *this; } - bool open(const std::string& path) { + bool open(const tc::fs::path& path) { close(); // Create a completely fresh fstream object (not reusing old one) diff --git a/addons/tcxImGui/README.md b/addons/tcxImGui/README.md index 817f4902..aa572ca5 100644 --- a/addons/tcxImGui/README.md +++ b/addons/tcxImGui/README.md @@ -96,10 +96,10 @@ void tcApp::setup() { | Tool | Arguments | Description | |------|-----------|-------------| -| `imgui_get_widgets` | `window` (optional) | List all widgets with labels, types, and positions | -| `imgui_click` | `label`, `window` (optional) | Click a widget by label | -| `imgui_input` | `label`, `text`, `window` (optional) | Set a widget's value: replaces text in input widgets, and enters numeric values directly into slider/drag widgets (Ctrl+Click temp input) | -| `imgui_checkbox` | `label`, `value` (optional), `window` (optional) | Toggle or set a checkbox | +| `tcx_imgui_get_widgets` | `window` (optional) | List all widgets with labels, types, and positions | +| `tcx_imgui_click` | `label`, `window` (optional) | Click a widget by label | +| `tcx_imgui_input` | `label`, `text`, `window` (optional) | Set a widget's value: replaces text in input widgets, and enters numeric values directly into slider/drag widgets (Ctrl+Click temp input) | +| `tcx_imgui_checkbox` | `label`, `value` (optional), `window` (optional) | Toggle or set a checkbox | These tools use ImGui's Test Engine hooks to collect widget info each frame. diff --git a/addons/tcxImGui/src/platform/imgui_impl_android.cpp b/addons/tcxImGui/src/platform/imgui_impl_android.cpp index eecaade6..879a1888 100644 --- a/addons/tcxImGui/src/platform/imgui_impl_android.cpp +++ b/addons/tcxImGui/src/platform/imgui_impl_android.cpp @@ -5,7 +5,7 @@ #ifdef __ANDROID__ // sokol headers (must come before sokol_imgui.h) -#include "sokol/sokol_app.h" +#include "sokol/sokol_app_tc.h" #include "sokol/sokol_gfx.h" #include "sokol/sokol_log.h" diff --git a/addons/tcxImGui/src/platform/imgui_impl_ios.mm b/addons/tcxImGui/src/platform/imgui_impl_ios.mm index 39f740c1..1bc21fd9 100644 --- a/addons/tcxImGui/src/platform/imgui_impl_ios.mm +++ b/addons/tcxImGui/src/platform/imgui_impl_ios.mm @@ -3,7 +3,7 @@ // ============================================================================= // sokol headers (required before sokol_imgui.h) -#include "sokol/sokol_app.h" +#include "sokol/sokol_app_tc.h" #include "sokol/sokol_gfx.h" #include "sokol/sokol_log.h" diff --git a/addons/tcxImGui/src/platform/imgui_impl_linux.cpp b/addons/tcxImGui/src/platform/imgui_impl_linux.cpp index 9da732e9..d2c522dd 100644 --- a/addons/tcxImGui/src/platform/imgui_impl_linux.cpp +++ b/addons/tcxImGui/src/platform/imgui_impl_linux.cpp @@ -3,7 +3,7 @@ // ============================================================================= // sokol ヘッダー(sokol_imgui.h より先に必要) -#include "sokol/sokol_app.h" +#include "sokol/sokol_app_tc.h" #include "sokol/sokol_gfx.h" #include "sokol/sokol_log.h" diff --git a/addons/tcxImGui/src/platform/imgui_impl_mac.mm b/addons/tcxImGui/src/platform/imgui_impl_mac.mm index b2eaa4a1..0764f23e 100644 --- a/addons/tcxImGui/src/platform/imgui_impl_mac.mm +++ b/addons/tcxImGui/src/platform/imgui_impl_mac.mm @@ -3,7 +3,7 @@ // ============================================================================= // sokol ヘッダー(sokol_imgui.h より先に必要) -#include "sokol/sokol_app.h" +#include "sokol/sokol_app_tc.h" #include "sokol/sokol_gfx.h" #include "sokol/sokol_log.h" diff --git a/addons/tcxImGui/src/platform/imgui_impl_web.cpp b/addons/tcxImGui/src/platform/imgui_impl_web.cpp index 89a5dce5..352c5519 100644 --- a/addons/tcxImGui/src/platform/imgui_impl_web.cpp +++ b/addons/tcxImGui/src/platform/imgui_impl_web.cpp @@ -3,7 +3,7 @@ // ============================================================================= // sokol ヘッダー(sokol_imgui.h より先に必要) -#include "sokol/sokol_app.h" +#include "sokol/sokol_app_tc.h" #include "sokol/sokol_gfx.h" #include "sokol/sokol_log.h" diff --git a/addons/tcxImGui/src/platform/imgui_impl_win.cpp b/addons/tcxImGui/src/platform/imgui_impl_win.cpp index 9da732e9..d2c522dd 100644 --- a/addons/tcxImGui/src/platform/imgui_impl_win.cpp +++ b/addons/tcxImGui/src/platform/imgui_impl_win.cpp @@ -3,7 +3,7 @@ // ============================================================================= // sokol ヘッダー(sokol_imgui.h より先に必要) -#include "sokol/sokol_app.h" +#include "sokol/sokol_app_tc.h" #include "sokol/sokol_gfx.h" #include "sokol/sokol_log.h" diff --git a/addons/tcxImGui/src/sokol_imgui.h b/addons/tcxImGui/src/sokol_imgui.h index c9ffb0f0..88237318 100644 --- a/addons/tcxImGui/src/sokol_imgui.h +++ b/addons/tcxImGui/src/sokol_imgui.h @@ -561,6 +561,18 @@ SOKOL_IMGUI_API_DECL void simgui_shutdown(void); inline void simgui_setup(const simgui_desc_t& desc) { return simgui_setup(&desc); } inline void simgui_new_frame(const simgui_frame_desc_t& desc) { return simgui_new_frame(&desc); } +/* [TrussC] multi-context API (multi-window support). Each context is a full, + independent sokol_imgui instance (own ImGui context, font atlas, buffers, + pipeline). simgui_tc_set_context() selects which instance the classic + simgui_* calls (and ImGui::GetCurrentContext) operate on. The context + handle from simgui_tc_get_context() before any make/set is the default + instance (what plain simgui_setup initialized). */ +typedef struct simgui_tc_context { void* ptr; } simgui_tc_context; +SOKOL_IMGUI_API_DECL simgui_tc_context simgui_tc_make_context(const simgui_desc_t* desc); +SOKOL_IMGUI_API_DECL void simgui_tc_set_context(simgui_tc_context ctx); +SOKOL_IMGUI_API_DECL simgui_tc_context simgui_tc_get_context(void); +SOKOL_IMGUI_API_DECL void simgui_tc_destroy_context(simgui_tc_context ctx); + #endif #endif /* SOKOL_IMGUI_INCLUDED */ @@ -646,8 +658,17 @@ typedef struct { sg_range vertices; sg_range indices; bool is_osx; + void* imgui_ctx; // [TrussC] the ImGuiContext* owned by this instance } _simgui_state_t; -static _simgui_state_t _simgui; + +// [TrussC] Multi-context support (multi-window): _simgui becomes a reference +// to the ACTIVE instance so the whole implementation below compiles +// unchanged. simgui_setup()/simgui_* operate on the instance selected via +// simgui_tc_set_context(); the default instance preserves classic +// single-window behavior. +static _simgui_state_t _simgui_default_instance; +static _simgui_state_t* _simgui_cur = &_simgui_default_instance; +#define _simgui (*_simgui_cur) //>#shdgen #if defined(SOKOL_GLCORE) @@ -2127,10 +2148,19 @@ static void _simgui_imgui_newframe(void) { } static void _simgui_imgui_create_context(void) { + // [TrussC] Since Dear ImGui 1.90, CreateContext() RESTORES the previously + // current context before returning (it is deliberately not + // context-switching). Capture the RETURN VALUE and make the new context + // current explicitly -- the rest of simgui_setup() configures "the + // current context" and must target the newly created one. #if defined(__cplusplus) - ImGui::CreateContext(); + ImGuiContext* ctx = ImGui::CreateContext(); + ImGui::SetCurrentContext(ctx); + _simgui.imgui_ctx = (void*)ctx; #else - _SIMGUI_CFUNC(CreateContext)(0); + ImGuiContext* ctx = _SIMGUI_CFUNC(CreateContext)(0); + _SIMGUI_CFUNC(SetCurrentContext)(ctx); + _simgui.imgui_ctx = (void*)ctx; #endif } @@ -3170,4 +3200,71 @@ SOKOL_API_IMPL bool simgui_handle_event(const sapp_event* ev) { } #endif // SOKOL_IMGUI_NO_SOKOL_APP +/* [TrussC] multi-context API implementation */ +SOKOL_API_IMPL simgui_tc_context simgui_tc_get_context(void) { + simgui_tc_context c; c.ptr = (void*)_simgui_cur; return c; +} + +SOKOL_API_IMPL void simgui_tc_set_context(simgui_tc_context ctx) { + SOKOL_ASSERT(ctx.ptr); + _simgui_cur = (_simgui_state_t*)ctx.ptr; + if (_simgui.imgui_ctx) { + #if defined(__cplusplus) + ImGui::SetCurrentContext((ImGuiContext*)_simgui.imgui_ctx); + #else + _SIMGUI_CFUNC(SetCurrentContext)((ImGuiContext*)_simgui.imgui_ctx); + #endif + } +} + +SOKOL_API_IMPL simgui_tc_context simgui_tc_make_context(const simgui_desc_t* desc) { + SOKOL_ASSERT(desc); + /* plain malloc: instance lifetime is independent of any per-instance + custom allocator (custom allocators across tc contexts unsupported) */ + _simgui_state_t* inst = (_simgui_state_t*)malloc(sizeof(_simgui_state_t)); + SOKOL_ASSERT(inst); + memset(inst, 0, sizeof(_simgui_state_t)); + _simgui_state_t* prev = _simgui_cur; + #if defined(__cplusplus) + ImGuiContext* prev_im = ImGui::GetCurrentContext(); + #else + ImGuiContext* prev_im = _SIMGUI_CFUNC(GetCurrentContext)(); + #endif + _simgui_cur = inst; + simgui_setup(desc); /* fills the new instance, creates its ImGui context */ + _simgui_cur = prev; + #if defined(__cplusplus) + ImGui::SetCurrentContext(prev_im); + #else + _SIMGUI_CFUNC(SetCurrentContext)(prev_im); + #endif + simgui_tc_context c; c.ptr = (void*)inst; return c; +} + +SOKOL_API_IMPL void simgui_tc_destroy_context(simgui_tc_context ctx) { + if (!ctx.ptr || ctx.ptr == (void*)&_simgui_default_instance) { + return; /* the default instance is torn down by plain simgui_shutdown() */ + } + _simgui_state_t* inst = (_simgui_state_t*)ctx.ptr; + _simgui_state_t* prev = _simgui_cur; + #if defined(__cplusplus) + ImGuiContext* prev_im = ImGui::GetCurrentContext(); + #else + ImGuiContext* prev_im = _SIMGUI_CFUNC(GetCurrentContext)(); + #endif + if (prev == inst) { prev = &_simgui_default_instance; prev_im = 0; } + simgui_tc_context sel; sel.ptr = (void*)inst; + simgui_tc_set_context(sel); + simgui_shutdown(); + _simgui_cur = prev; + if (prev_im) { + #if defined(__cplusplus) + ImGui::SetCurrentContext(prev_im); + #else + _SIMGUI_CFUNC(SetCurrentContext)(prev_im); + #endif + } + free(inst); +} + #endif // SOKOL_IMPL diff --git a/addons/tcxImGui/src/tcImGuiTools.h b/addons/tcxImGui/src/tcImGuiTools.h index efcb8069..93913c85 100644 --- a/addons/tcxImGui/src/tcImGuiTools.h +++ b/addons/tcxImGui/src/tcImGuiTools.h @@ -132,8 +132,8 @@ inline void registerImGuiTools() { // Activate collection enableCollection(); - // imgui_get_widgets — list all widgets - tc::mcp::tool("imgui_get_widgets", "List ImGui widgets with labels, types, and positions") + // tcx_imgui_get_widgets — list all widgets + tc::mcp::tool("tcx_imgui_get_widgets", "List ImGui widgets with labels, types, and positions") .arg("window", "Filter by window name (optional, omit for all)", false) .bind(std::function([](const json& args) -> json { std::string window = args.value("window", ""); @@ -175,8 +175,8 @@ inline void registerImGuiTools() { return json{{"widgets", widgets}, {"count", (int)widgets.size()}}; })); - // imgui_click — click a widget by label - tc::mcp::tool("imgui_click", "Click an ImGui widget by label") + // tcx_imgui_click — click a widget by label + tc::mcp::tool("tcx_imgui_click", "Click an ImGui widget by label") .arg("label", "Widget label text") .arg("window", "Window name (optional, required if label is ambiguous)", false) .bind(std::function([](const json& args) -> json { @@ -197,8 +197,8 @@ inline void registerImGuiTools() { }; })); - // imgui_input — set the value of an input/slider/drag widget - tc::mcp::tool("imgui_input", "Set the value of an ImGui widget: text inputs, and numeric entry on slider/drag widgets") + // tcx_imgui_input — set the value of an input/slider/drag widget + tc::mcp::tool("tcx_imgui_input", "Set the value of an ImGui widget: text inputs, and numeric entry on slider/drag widgets") .arg("label", "Widget label") .arg("text", "Replacement text (or numeric value for slider/drag)") .arg("window", "Window name (optional)", false) @@ -222,8 +222,8 @@ inline void registerImGuiTools() { }; })); - // imgui_checkbox — toggle or set a checkbox - tc::mcp::tool("imgui_checkbox", "Toggle an ImGui checkbox") + // tcx_imgui_checkbox — toggle or set a checkbox + tc::mcp::tool("tcx_imgui_checkbox", "Toggle an ImGui checkbox") .arg("label", "Checkbox label") .arg("value", "Desired state (true/false)", false) .arg("window", "Window name (optional)", false) @@ -262,7 +262,7 @@ inline void registerImGuiTools() { }; })); - tc::logNotice() << "[MCP] ImGui tools registered (imgui_get_widgets, imgui_click, imgui_input, imgui_checkbox)"; + tc::logNotice() << "[MCP] ImGui tools registered (tcx_imgui_get_widgets, tcx_imgui_click, tcx_imgui_input, tcx_imgui_checkbox)"; } } // namespace tcx::imgui diff --git a/addons/tcxImGui/src/tcxImGui.h b/addons/tcxImGui/src/tcxImGui.h index 1b8b1367..a7c583d1 100644 --- a/addons/tcxImGui/src/tcxImGui.h +++ b/addons/tcxImGui/src/tcxImGui.h @@ -11,6 +11,8 @@ #include "tcImGuiTools.h" #include #include +#include +#include namespace tcx::imgui { @@ -19,19 +21,38 @@ namespace tcx::imgui { // --------------------------------------------------------------------------- class ImGuiManager { public: - // Initialize (call in setup) + // Initialize (call in setup — from the App that drives the target window; + // the manager binds to whichever window's context is current, so an + // imguiSetup() inside a secondary window's App wires imgui to THAT window) void setup() { if (initialized_) return; + // Every manager owns a full sokol_imgui instance (own ImGui context, + // font atlas, buffers). The classic single-window app just has one. simgui_desc_t desc = {}; desc.logger.func = slog_func; - simgui_setup(&desc); + // Secondary windows have their own swapchain formats (BGRA8/RGBA8, + // not the main window's RGB10A2) -- the imgui pipeline must match + // the pass it renders into or sg validation fails. + { + auto& wctx = tc::internal::currentWindowContext(); + if (!wctx.isMain) { + desc.color_format = wctx.swapchainColorFormat; + desc.depth_format = SG_PIXELFORMAT_DEPTH_STENCIL; + desc.sample_count = wctx.swapchainSampleCount; + } + } + simguiCtx_ = simgui_tc_make_context(&desc); + simgui_tc_set_context(simguiCtx_); initialized_ = true; - // Listen to beforePresent for rendering + // Listen to beforePresent for rendering. events() resolves against the + // CURRENT window context, so these listeners land on this window's + // event streams (main or secondary alike). renderListener_ = tc::events().onRender.listen([this]() { if (renderPending_) { + simgui_tc_set_context(simguiCtx_); simgui_render(); renderPending_ = false; } @@ -39,14 +60,27 @@ class ImGuiManager { // Listen to rawEvent for input handling eventListener_ = tc::events().rawEvent.listen([this](const sapp_event& ev) { + simgui_tc_set_context(simguiCtx_); simgui_handle_event(&ev); }, tc::EventPriority::BeforeApp); // Overlay capture queries — tell the framework when imgui owns the // pointer / keyboard so node-tree hover is suppressed under panels and // user code can guard raw input (isOverlayHovered/isOverlayFocused). - tc::internal::overlayHoveredQuery = []() { return ImGui::GetIO().WantCaptureMouse; }; - tc::internal::overlayFocusedQuery = []() { return ImGui::GetIO().WantCaptureKeyboard; }; + // Installed once, shared by all windows: the query resolves the + // manager of the window whose context is current when it is asked. + tc::internal::overlayHoveredQuery = []() { + ImGuiManager* m = ImGuiManager::findForCurrentWindow(); + if (!m || !m->isInitialized()) return false; + simgui_tc_set_context(m->simguiCtx_); + return ImGui::GetIO().WantCaptureMouse; + }; + tc::internal::overlayFocusedQuery = []() { + ImGuiManager* m = ImGuiManager::findForCurrentWindow(); + if (!m || !m->isInitialized()) return false; + simgui_tc_set_context(m->simguiCtx_); + return ImGui::GetIO().WantCaptureKeyboard; + }; // Consume input before it reaches the node tree (BeforeApp). Pointer // capture follows the press: a gesture imgui claims on press stays @@ -54,6 +88,7 @@ class ImGuiManager { // mid-way, and a press over a panel owns the whole gesture. Move/scroll // and keys are stateless — consumed whenever imgui wants them. mousePressConsume_ = tc::events().mousePressed.listen([this](tc::MouseEventArgs& e) { + simgui_tc_set_context(simguiCtx_); if (ImGui::GetIO().WantCaptureMouse) { pointerCaptured_ = true; e.consumed = true; } }, tc::EventPriority::BeforeApp); mouseReleaseConsume_ = tc::events().mouseReleased.listen([this](tc::MouseEventArgs& e) { @@ -62,16 +97,20 @@ class ImGuiManager { mouseDragConsume_ = tc::events().mouseDragged.listen([this](tc::MouseDragEventArgs& e) { if (pointerCaptured_) e.consumed = true; }, tc::EventPriority::BeforeApp); - mouseMoveConsume_ = tc::events().mouseMoved.listen([](tc::MouseMoveEventArgs& e) { + mouseMoveConsume_ = tc::events().mouseMoved.listen([this](tc::MouseMoveEventArgs& e) { + simgui_tc_set_context(simguiCtx_); if (ImGui::GetIO().WantCaptureMouse) e.consumed = true; }, tc::EventPriority::BeforeApp); - mouseScrollConsume_ = tc::events().mouseScrolled.listen([](tc::ScrollEventArgs& e) { + mouseScrollConsume_ = tc::events().mouseScrolled.listen([this](tc::ScrollEventArgs& e) { + simgui_tc_set_context(simguiCtx_); if (ImGui::GetIO().WantCaptureMouse) e.consumed = true; }, tc::EventPriority::BeforeApp); - keyPressConsume_ = tc::events().keyPressed.listen([](tc::KeyEventArgs& e) { + keyPressConsume_ = tc::events().keyPressed.listen([this](tc::KeyEventArgs& e) { + simgui_tc_set_context(simguiCtx_); if (ImGui::GetIO().WantCaptureKeyboard) e.consumed = true; }, tc::EventPriority::BeforeApp); - keyReleaseConsume_ = tc::events().keyReleased.listen([](tc::KeyEventArgs& e) { + keyReleaseConsume_ = tc::events().keyReleased.listen([this](tc::KeyEventArgs& e) { + simgui_tc_set_context(simguiCtx_); if (ImGui::GetIO().WantCaptureKeyboard) e.consumed = true; }, tc::EventPriority::BeforeApp); @@ -96,27 +135,31 @@ class ImGuiManager { mouseScrollConsume_ = {}; keyPressConsume_ = {}; keyReleaseConsume_ = {}; - tc::internal::overlayHoveredQuery = nullptr; - tc::internal::overlayFocusedQuery = nullptr; + // The overlay queries are shared across windows and resolve per window + // through the manager registry — they stay installed (no-op for + // windows without an initialized manager). pointerCaptured_ = false; // GPU teardown only while sokol_gfx is alive. If shutdown runs from the // static destructor during exit() (e.g. an abnormal teardown path where // the exit event never fired), sokol_gfx is already gone and its objects // with it — skipping is the correct cleanup, touching them would crash. - if (sg_isvalid()) simgui_shutdown(); + if (sg_isvalid()) simgui_tc_destroy_context(simguiCtx_); + simguiCtx_ = {}; initialized_ = false; tc::logVerbose() << "ImGui shutdown"; } - // Begin frame (call at start of draw) + // Begin frame (call at start of draw). Sizes/dpi/delta are the CURRENT + // window's (context-routed), so imgui lays out for the window it draws in. void begin() { if (!initialized_) return; + simgui_tc_set_context(simguiCtx_); simgui_frame_desc_t desc = {}; - desc.width = sapp_width(); - desc.height = sapp_height(); - desc.delta_time = static_cast(sapp_frame_duration()); - desc.dpi_scale = sapp_dpi_scale(); + desc.width = tc::getFramebufferWidth(); + desc.height = tc::getFramebufferHeight(); + desc.delta_time = static_cast(tc::getDeltaTime()); + desc.dpi_scale = tc::getDpiScale(); simgui_new_frame(&desc); } @@ -128,15 +171,34 @@ class ImGuiManager { bool isInitialized() const { return initialized_; } - // Singleton access + // Per-window access, keyed by the current WindowContext: the main App and + // each secondary window's App get their own manager (own ImGui context). static ImGuiManager& instance() { - static ImGuiManager mgr; - return mgr; + auto& slot = registry()[&tc::internal::currentWindowContext()]; + if (!slot) slot.reset(new ImGuiManager()); + return *slot; + } + + // Lookup without creating (overlay queries, wants-helpers). May be null. + static ImGuiManager* findForCurrentWindow() { + auto& reg = registry(); + auto it = reg.find(&tc::internal::currentWindowContext()); + return (it != reg.end()) ? it->second.get() : nullptr; } +public: + ~ImGuiManager() { shutdown(); } // public: owned by unique_ptr in the registry private: ImGuiManager() = default; - ~ImGuiManager() { shutdown(); } + + static std::unordered_map>& registry() { + static std::unordered_map> reg; + return reg; + } + + simgui_tc_context simguiCtx_ {}; bool initialized_ = false; bool renderPending_ = false; @@ -183,11 +245,11 @@ inline void imguiEnd() { } inline bool imguiWantsMouse() { - return ImGui::GetIO().WantCaptureMouse; + return tc::internal::overlayHoveredQuery ? tc::internal::overlayHoveredQuery() : false; } inline bool imguiWantsKeyboard() { - return ImGui::GetIO().WantCaptureKeyboard; + return tc::internal::overlayFocusedQuery ? tc::internal::overlayFocusedQuery() : false; } } // namespace tcx::imgui diff --git a/addons/tcxLua/bindcheck/bin/data/bindcheck.lua b/addons/tcxLua/bindcheck/bin/data/bindcheck.lua index 0ecad903..b10829b5 100644 --- a/addons/tcxLua/bindcheck/bin/data/bindcheck.lua +++ b/addons/tcxLua/bindcheck/bin/data/bindcheck.lua @@ -105,6 +105,19 @@ try("fs::path to Lua string", function() -- tcxLuaPathAdapter: path -> stri local p = getDataPath('bindcheck_probe.txt') return type(p) == 'string' and #p > 0 end) +try("ctor CALL form (generated)", function() -- Type(...) via sol::call_constructor + -- .new() alone passing is NOT enough: without call_constructor the + -- usertype table has no __call and Vec3(1,2,3) dies with + -- "attempt to call a table value". Regression check for both worlds. + local v = Vec3(1, 2, 3) + local c = Color(1, 0, 0) + return approx(v.z, 3) and approx(c.r, 1) +end) +try("ctor CALL form (hand-written)", function() + local f = Fbo() + local m = Mat4() + return f ~= nil and m ~= nil +end) -- Emit machine-parseable summary via print (base lib; goes to stdout). print("##BINDCHECK_BEGIN##") diff --git a/addons/tcxLua/bindcheck/regen_expected.sh b/addons/tcxLua/bindcheck/regen_expected.sh index 122f4bf9..da6b3ccc 100755 --- a/addons/tcxLua/bindcheck/regen_expected.sh +++ b/addons/tcxLua/bindcheck/regen_expected.sh @@ -7,7 +7,8 @@ HERE=${0:A:h} ADDON=${HERE:h} # .../addons/tcxLua G="$ADDON/src/generated/trussc_generated.cpp" H="$ADDON/src/tcxLua.cpp" -GT="$ADDON/src/generated/trussctype_generated.cpp" # luagen-types Phase 2 usertypes +# luagen-types Phase 2 usertypes — may be a single file or N shard TUs +GT=("$ADDON"/src/generated/trussctype_generated*.cpp) OUT="$HERE/bin/data/expected.lua" { @@ -22,7 +23,7 @@ OUT="$HERE/bin/data/expected.lua" | awk '{printf " \"%s\",\n", $0}' echo " }," echo " usertypes = {" - { grep -vE '^[[:space:]]*//' "$H"; grep -vE '^[[:space:]]*//' "$GT"; } \ + { grep -vE '^[[:space:]]*//' "$H"; grep -vhE '^[[:space:]]*//' "${GT[@]}"; } \ | grep -oE 'new_usertype<.*>\([[:space:]]*"[^"]+"' \ | sed -E 's/.*\(\s*"([^"]+)"/\1/' | sort -u \ | awk '{printf " \"%s\",\n", $0}' diff --git a/addons/tcxLua/exampleFileReload/src/tcApp.cpp b/addons/tcxLua/exampleFileReload/src/tcApp.cpp index 331f5082..9b3588ee 100644 --- a/addons/tcxLua/exampleFileReload/src/tcApp.cpp +++ b/addons/tcxLua/exampleFileReload/src/tcApp.cpp @@ -25,19 +25,19 @@ std::string sep = "/"; void tcApp::reloadLuaFile(){ #ifdef FILE_RELOAD_SUPPORTED std::string luaScriptBaseName = "sketch.lua"; - std::string luaScriptPath = getDataPath(luaScriptBaseName); + fs::path luaScriptPath = getDataPath(luaScriptBaseName); if(fileExists(luaScriptPath)){ - sol::optional result = lua->safe_script_file(luaScriptPath); + sol::optional result = lua->safe_script_file(luaScriptPath.string()); if (result.has_value()) { std::cerr << "Lua execution failed: " << result.value().what() << std::endl; } }else{ - tcLogError("tcApp") << "Lua file not found at: " << luaScriptPath; + logError("tcApp") << "Lua file not found at: " << luaScriptPath; } #else - tcLogWarning("tcApp") << "[file reload] sorry, this platform currently not supported"; + logWarning("tcApp") << "[file reload] sorry, this platform currently not supported"; #endif // FILE_RELOAD_SUPPORTED } diff --git a/addons/tcxLua/src/generated/trussc_generated.cpp b/addons/tcxLua/src/generated/trussc_generated.cpp index 7d373ff3..26374e59 100644 --- a/addons/tcxLua/src/generated/trussc_generated.cpp +++ b/addons/tcxLua/src/generated/trussc_generated.cpp @@ -5,9 +5,14 @@ using namespace trussc; using namespace std; +// The Lua API deliberately keeps binding deprecated C++ compat names +// (colorFromHSB, tcLog*, ...) so existing Lua scripts stay stable — this shim +// layer is exempt from the CI deprecation gate (-Werror=deprecated-declarations). #ifndef _MSC_VER #pragma GCC diagnostic push #pragma clang diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#pragma clang diagnostic ignored "-Wdeprecated-declarations" #endif void tcxLua::setTrussCGeneratedBindings(const std::shared_ptr& lua) { @@ -83,12 +88,12 @@ void tcxLua::setTrussCGeneratedBindings(const std::shared_ptr& lua) lua->set_function("getLogger", []() -> decltype(auto) { return trussc::getLogger(); }); lua->set_function("setConsoleLogLevel", [](trussc::LogLevel level) { return trussc::setConsoleLogLevel(level); }); lua->set_function("setFileLogLevel", [](trussc::LogLevel level) { return trussc::setFileLogLevel(level); }); - lua->set_function("setLogFile", [](const std::string & path) { return trussc::setLogFile(path); }); + lua->set_function("setLogFile", [](const fs::path & path) { return trussc::setLogFile(path); }); lua->set_function("closeLogFile", []() { return trussc::closeLogFile(); }); lua->set_function("tcGetLogger", []() -> decltype(auto) { return trussc::tcGetLogger(); }); lua->set_function("tcSetConsoleLogLevel", [](trussc::LogLevel level) { return trussc::tcSetConsoleLogLevel(level); }); lua->set_function("tcSetFileLogLevel", [](trussc::LogLevel level) { return trussc::tcSetFileLogLevel(level); }); - lua->set_function("tcSetLogFile", [](const std::string & path) { return trussc::tcSetLogFile(path); }); + lua->set_function("tcSetLogFile", [](const fs::path & path) { return trussc::tcSetLogFile(path); }); lua->set_function("tcCloseLogFile", []() { return trussc::tcCloseLogFile(); }); lua->set_function("logAt", sol::overload( []() { return trussc::logAt(); }, @@ -198,12 +203,13 @@ void tcxLua::setTrussCGeneratedBindings(const std::shared_ptr& lua) lua->set_function("isProximityClose", []() { return trussc::isProximityClose(); }); lua->set_function("getLocation", []() { return trussc::getLocation(); }); lua->set_function("events", []() -> decltype(auto) { return trussc::events(); }); + lua->set_function("loadErrorName", [](trussc::LoadError e) { return trussc::loadErrorName(e); }); lua->set_function("initAudio", []() { return trussc::initAudio(); }); lua->set_function("shutdownAudio", []() { return trussc::shutdownAudio(); }); lua->set_function("getMicInput", []() -> decltype(auto) { return trussc::getMicInput(); }); - lua->set_function("setDataPathRoot", [](const std::string & path) { return trussc::setDataPathRoot(path); }); + lua->set_function("setDataPathRoot", [](const fs::path & path) { return trussc::setDataPathRoot(path); }); lua->set_function("getDataPathRoot", []() { return trussc::getDataPathRoot(); }); - lua->set_function("getDataPath", [](const std::string & filename) { return trussc::getDataPath(filename); }); + lua->set_function("getDataPath", [](const fs::path & filename) { return trussc::getDataPath(filename); }); lua->set_function("setDataPathToResources", []() { return trussc::setDataPathToResources(); }); lua->set_function("toInt", [](const std::string & str) { return trussc::toInt(str); }); lua->set_function("toInt64", [](const std::string & str) { return trussc::toInt64(str); }); @@ -276,45 +282,45 @@ void tcxLua::setTrussCGeneratedBindings(const std::shared_ptr& lua) []() { return trussc::loadDialog(); }, [](const std::string & title) { return trussc::loadDialog(title); }, [](const std::string & title, const std::string & message) { return trussc::loadDialog(title, message); }, - [](const std::string & title, const std::string & message, const std::string & defaultPath) { return trussc::loadDialog(title, message, defaultPath); }, - [](const std::string & title, const std::string & message, const std::string & defaultPath, bool folderSelection) { return trussc::loadDialog(title, message, defaultPath, folderSelection); } + [](const std::string & title, const std::string & message, const fs::path & defaultPath) { return trussc::loadDialog(title, message, defaultPath); }, + [](const std::string & title, const std::string & message, const fs::path & defaultPath, bool folderSelection) { return trussc::loadDialog(title, message, defaultPath, folderSelection); } )); - lua->set_function("loadDialogAsync", [](const std::string & title, const std::string & message, const std::string & defaultPath, bool folderSelection, std::function callback) { return trussc::loadDialogAsync(title, message, defaultPath, folderSelection, callback); }); + lua->set_function("loadDialogAsync", [](const std::string & title, const std::string & message, const fs::path & defaultPath, bool folderSelection, std::function callback) { return trussc::loadDialogAsync(title, message, defaultPath, folderSelection, callback); }); lua->set_function("saveDialog", sol::overload( []() { return trussc::saveDialog(); }, [](const std::string & title) { return trussc::saveDialog(title); }, [](const std::string & title, const std::string & message) { return trussc::saveDialog(title, message); }, - [](const std::string & title, const std::string & message, const std::string & defaultPath) { return trussc::saveDialog(title, message, defaultPath); }, - [](const std::string & title, const std::string & message, const std::string & defaultPath, const std::string & defaultName) { return trussc::saveDialog(title, message, defaultPath, defaultName); } + [](const std::string & title, const std::string & message, const fs::path & defaultPath) { return trussc::saveDialog(title, message, defaultPath); }, + [](const std::string & title, const std::string & message, const fs::path & defaultPath, const fs::path & defaultName) { return trussc::saveDialog(title, message, defaultPath, defaultName); } )); - lua->set_function("saveDialogAsync", [](const std::string & title, const std::string & message, const std::string & defaultPath, const std::string & defaultName, std::function callback) { return trussc::saveDialogAsync(title, message, defaultPath, defaultName, callback); }); - lua->set_function("loadJson", [](const std::string & path) { return trussc::loadJson(path); }); + lua->set_function("saveDialogAsync", [](const std::string & title, const std::string & message, const fs::path & defaultPath, const fs::path & defaultName, std::function callback) { return trussc::saveDialogAsync(title, message, defaultPath, defaultName, callback); }); + lua->set_function("loadJson", [](const fs::path & path) { return trussc::loadJson(path); }); lua->set_function("saveJson", sol::overload( - [](const trussc::Json & j, const std::string & path) { return trussc::saveJson(j, path); }, - [](const trussc::Json & j, const std::string & path, int indent) { return trussc::saveJson(j, path, indent); } + [](const trussc::Json & j, const fs::path & path) { return trussc::saveJson(j, path); }, + [](const trussc::Json & j, const fs::path & path, int indent) { return trussc::saveJson(j, path, indent); } )); lua->set_function("parseJson", [](const std::string & str) { return trussc::parseJson(str); }); lua->set_function("toJsonString", sol::overload( [](const trussc::Json & j) { return trussc::toJsonString(j); }, [](const trussc::Json & j, int indent) { return trussc::toJsonString(j, indent); } )); - lua->set_function("loadXml", [](const std::string & path) { return trussc::loadXml(path); }); + lua->set_function("loadXml", [](const fs::path & path) { return trussc::loadXml(path); }); lua->set_function("parseXml", [](const std::string & str) { return trussc::parseXml(str); }); - lua->set_function("getFileName", [](const std::string & path) { return trussc::getFileName(path); }); - lua->set_function("getBaseName", [](const std::string & path) { return trussc::getBaseName(path); }); - lua->set_function("getFileExtension", [](const std::string & path) { return trussc::getFileExtension(path); }); - lua->set_function("getParentDirectory", [](const std::string & path) { return trussc::getParentDirectory(path); }); - lua->set_function("joinPath", [](const std::string & dir, const std::string & file) { return trussc::joinPath(dir, file); }); - lua->set_function("getAbsolutePath", [](const std::string & path) { return trussc::getAbsolutePath(path); }); - lua->set_function("fileExists", [](const std::string & path) { return trussc::fileExists(path); }); - lua->set_function("directoryExists", [](const std::string & path) { return trussc::directoryExists(path); }); - lua->set_function("createDirectory", [](const std::string & path) { return trussc::createDirectory(path); }); - lua->set_function("listDirectory", [](const std::string & path) { return trussc::listDirectory(path); }); - lua->set_function("removeFile", [](const std::string & path) { return trussc::removeFile(path); }); - lua->set_function("getFileSize", [](const std::string & path) { return trussc::getFileSize(path); }); - lua->set_function("loadTextFile", [](const std::string & path) { return trussc::loadTextFile(path); }); - lua->set_function("saveTextFile", [](const std::string & path, const std::string & content) { return trussc::saveTextFile(path, content); }); - lua->set_function("appendToFile", [](const std::string & path, const std::string & content) { return trussc::appendToFile(path, content); }); + lua->set_function("getFileName", [](const fs::path & path) { return trussc::getFileName(path); }); + lua->set_function("getBaseName", [](const fs::path & path) { return trussc::getBaseName(path); }); + lua->set_function("getFileExtension", [](const fs::path & path) { return trussc::getFileExtension(path); }); + lua->set_function("getParentDirectory", [](const fs::path & path) { return trussc::getParentDirectory(path); }); + lua->set_function("joinPath", [](const fs::path & dir, const fs::path & file) { return trussc::joinPath(dir, file); }); + lua->set_function("getAbsolutePath", [](const fs::path & path) { return trussc::getAbsolutePath(path); }); + lua->set_function("fileExists", [](const fs::path & path) { return trussc::fileExists(path); }); + lua->set_function("directoryExists", [](const fs::path & path) { return trussc::directoryExists(path); }); + lua->set_function("createDirectory", [](const fs::path & path) { return trussc::createDirectory(path); }); + lua->set_function("listDirectory", [](const fs::path & path) { return trussc::listDirectory(path); }); + lua->set_function("removeFile", [](const fs::path & path) { return trussc::removeFile(path); }); + lua->set_function("getFileSize", [](const fs::path & path) { return trussc::getFileSize(path); }); + lua->set_function("loadTextFile", [](const fs::path & path) { return trussc::loadTextFile(path); }); + lua->set_function("saveTextFile", [](const fs::path & path, const std::string & content) { return trussc::saveTextFile(path, content); }); + lua->set_function("appendToFile", [](const fs::path & path, const std::string & content) { return trussc::appendToFile(path, content); }); lua->set_function("getVersion", []() { return trussc::getVersion(); }); lua->set_function("getGlobalMouseX", []() { return trussc::getGlobalMouseX(); }); lua->set_function("getGlobalMouseY", []() { return trussc::getGlobalMouseY(); }); @@ -634,17 +640,17 @@ void tcxLua::setTrussCGeneratedBindings(const std::shared_ptr& lua) lua->set_function("popShader", []() { return trussc::popShader(); }); lua->set_function("videoCodecName", [](trussc::VideoCodec c) { return trussc::videoCodecName(c); }); lua->set_function("startRecording", sol::overload( - [](const std::string & path) { return trussc::startRecording(path); }, - [](const std::string & path, const trussc::VideoRecordSettings & settings) { return trussc::startRecording(path, settings); }, - [](const std::string & path, float durationSec) { return trussc::startRecording(path, durationSec); }, - [](const trussc::Fbo & fbo, const std::string & path) { return trussc::startRecording(fbo, path); }, - [](const trussc::Fbo & fbo, const std::string & path, const trussc::VideoRecordSettings & settings) { return trussc::startRecording(fbo, path, settings); }, - [](const trussc::Fbo & fbo, const std::string & path, float durationSec) { return trussc::startRecording(fbo, path, durationSec); } + [](const fs::path & path) { return trussc::startRecording(path); }, + [](const fs::path & path, const trussc::VideoRecordSettings & settings) { return trussc::startRecording(path, settings); }, + [](const fs::path & path, float durationSec) { return trussc::startRecording(path, durationSec); }, + [](const trussc::Fbo & fbo, const fs::path & path) { return trussc::startRecording(fbo, path); }, + [](const trussc::Fbo & fbo, const fs::path & path, const trussc::VideoRecordSettings & settings) { return trussc::startRecording(fbo, path, settings); }, + [](const trussc::Fbo & fbo, const fs::path & path, float durationSec) { return trussc::startRecording(fbo, path, durationSec); } )); lua->set_function("stopRecording", []() { return trussc::stopRecording(); }); lua->set_function("isRecording", []() { return trussc::isRecording(); }); lua->set_function("recordingFrameCount", []() { return trussc::recordingFrameCount(); }); - lua->set_function("recordingPath", []() -> decltype(auto) { return trussc::recordingPath(); }); + lua->set_function("recordingPath", []() { return trussc::recordingPath(); }); lua->set_function("createPlane", sol::overload( [](float width, float height) { return trussc::createPlane(width, height); }, [](float width, float height, int cols) { return trussc::createPlane(width, height, cols); }, @@ -740,6 +746,10 @@ void tcxLua::setTrussCGeneratedBindings(const std::shared_ptr& lua) lua->set_function("isOverlayFocused", []() { return trussc::isOverlayFocused(); }); lua->set_function("getSelectedNode", []() { return trussc::getSelectedNode(); }); lua->set_function("getRootNode", []() { return trussc::getRootNode(); }); + lua->set_function("createWindow", sol::overload( + []() { return trussc::createWindow(); }, + [](const trussc::WindowSettings & settings) { return trussc::createWindow(settings); } + )); lua->set_function("nodeToJson", [](trussc::Node & node, int maxDepth) { return trussc::nodeToJson(node, maxDepth); }); lua->set_function("lerp", [](float a, float b, float t) { return std::lerp(a, b, t); }); lua->set_function("sin", [](float x) { return std::sin(x); }); diff --git a/addons/tcxLua/src/generated/trussctype_generated.cpp b/addons/tcxLua/src/generated/trussctype_generated.cpp index 62ad8eb8..da67cd91 100644 --- a/addons/tcxLua/src/generated/trussctype_generated.cpp +++ b/addons/tcxLua/src/generated/trussctype_generated.cpp @@ -8,1710 +8,39 @@ namespace { struct TcxLuaColorsTable {}; } // tag for the `colors` constant ta #pragma GCC diagnostic push #pragma clang diagnostic push #endif +void tcxLuaGenShard_00(const std::shared_ptr& lua); +void tcxLuaGenShard_01(const std::shared_ptr& lua); +void tcxLuaGenShard_02(const std::shared_ptr& lua); +void tcxLuaGenShard_03(const std::shared_ptr& lua); +void tcxLuaGenShard_04(const std::shared_ptr& lua); +void tcxLuaGenShard_05(const std::shared_ptr& lua); +void tcxLuaGenShard_06(const std::shared_ptr& lua); +void tcxLuaGenShard_07(const std::shared_ptr& lua); +void tcxLuaGenShard_08(const std::shared_ptr& lua); +void tcxLuaGenShard_09(const std::shared_ptr& lua); +void tcxLuaGenShard_10(const std::shared_ptr& lua); +void tcxLuaGenShard_11(const std::shared_ptr& lua); +void tcxLuaGenShard_12(const std::shared_ptr& lua); +void tcxLuaGenShard_13(const std::shared_ptr& lua); +void tcxLuaGenShard_14(const std::shared_ptr& lua); +void tcxLuaGenShard_15(const std::shared_ptr& lua); void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { - { - sol::usertype t = lua->new_usertype("Vec2", - sol::constructors(), - sol::meta_function::index, [](const trussc::Vec2& a, int b){ return a[b]; }, - sol::meta_function::addition, [](const trussc::Vec2& a, const trussc::Vec2 & b){ return a + b; }, - sol::meta_function::subtraction, [](const trussc::Vec2& a, const trussc::Vec2 & b){ return a - b; }, - sol::meta_function::unary_minus, [](const trussc::Vec2& a){ return -a; }, - sol::meta_function::multiplication, sol::overload([](const trussc::Vec2& a, float b){ return a * b; }, [](const trussc::Vec2& a, const trussc::Vec2 & b){ return a * b; }), - sol::meta_function::division, sol::overload([](const trussc::Vec2& a, float b){ return a / b; }, [](const trussc::Vec2& a, const trussc::Vec2 & b){ return a / b; }), - sol::meta_function::equal_to, [](const trussc::Vec2& a, const trussc::Vec2 & b){ return a == b; }); - t["x"] = &trussc::Vec2::x; - t["y"] = &trussc::Vec2::y; - t["set"] = sol::overload([](trussc::Vec2& self, float x_, float y_) -> decltype(auto) { return self.set(x_, y_); }, [](trussc::Vec2& self, const trussc::Vec2 & v) -> decltype(auto) { return self.set(v); }); - t["length"] = &trussc::Vec2::length; - t["lengthSquared"] = &trussc::Vec2::lengthSquared; - t["normalized"] = &trussc::Vec2::normalized; - t["normalize"] = &trussc::Vec2::normalize; - t["limit"] = &trussc::Vec2::limit; - t["dot"] = &trussc::Vec2::dot; - t["cross"] = &trussc::Vec2::cross; - t["distance"] = &trussc::Vec2::distance; - t["distanceSquared"] = &trussc::Vec2::distanceSquared; - t["angle"] = sol::overload([](trussc::Vec2& self) { return self.angle(); }, [](trussc::Vec2& self, const trussc::Vec2 & v) { return self.angle(v); }); - t["rotated"] = &trussc::Vec2::rotated; - t["rotate"] = &trussc::Vec2::rotate; - t["lerp"] = &trussc::Vec2::lerp; - t["perpendicular"] = &trussc::Vec2::perpendicular; - t["reflected"] = &trussc::Vec2::reflected; - t["fromAngle"] = sol::overload([](float radians) { return trussc::Vec2::fromAngle(radians); }, [](float radians, float length) { return trussc::Vec2::fromAngle(radians, length); }); - } - { - sol::usertype t = lua->new_usertype("Vec3", - sol::constructors(), - sol::meta_function::index, [](const trussc::Vec3& a, int b){ return a[b]; }, - sol::meta_function::addition, [](const trussc::Vec3& a, const trussc::Vec3 & b){ return a + b; }, - sol::meta_function::subtraction, [](const trussc::Vec3& a, const trussc::Vec3 & b){ return a - b; }, - sol::meta_function::unary_minus, [](const trussc::Vec3& a){ return -a; }, - sol::meta_function::multiplication, sol::overload([](const trussc::Vec3& a, float b){ return a * b; }, [](const trussc::Vec3& a, const trussc::Vec3 & b){ return a * b; }), - sol::meta_function::division, sol::overload([](const trussc::Vec3& a, float b){ return a / b; }, [](const trussc::Vec3& a, const trussc::Vec3 & b){ return a / b; }), - sol::meta_function::equal_to, [](const trussc::Vec3& a, const trussc::Vec3 & b){ return a == b; }); - t["x"] = &trussc::Vec3::x; - t["y"] = &trussc::Vec3::y; - t["z"] = &trussc::Vec3::z; - t["set"] = sol::overload([](trussc::Vec3& self, float x_, float y_, float z_) -> decltype(auto) { return self.set(x_, y_, z_); }, [](trussc::Vec3& self, const trussc::Vec3 & v) -> decltype(auto) { return self.set(v); }); - t["length"] = &trussc::Vec3::length; - t["lengthSquared"] = &trussc::Vec3::lengthSquared; - t["normalized"] = &trussc::Vec3::normalized; - t["normalize"] = &trussc::Vec3::normalize; - t["limit"] = &trussc::Vec3::limit; - t["dot"] = &trussc::Vec3::dot; - t["cross"] = &trussc::Vec3::cross; - t["distance"] = &trussc::Vec3::distance; - t["distanceSquared"] = &trussc::Vec3::distanceSquared; - t["lerp"] = &trussc::Vec3::lerp; - t["reflected"] = &trussc::Vec3::reflected; - t["xy"] = &trussc::Vec3::xy; - } - { - sol::usertype t = lua->new_usertype("IVec2", - sol::constructors(), - sol::meta_function::addition, [](const trussc::IVec2& a, const trussc::IVec2 & b){ return a + b; }, - sol::meta_function::subtraction, [](const trussc::IVec2& a, const trussc::IVec2 & b){ return a - b; }, - sol::meta_function::unary_minus, [](const trussc::IVec2& a){ return -a; }, - sol::meta_function::multiplication, [](const trussc::IVec2& a, int b){ return a * b; }, - sol::meta_function::equal_to, [](const trussc::IVec2& a, const trussc::IVec2 & b){ return a == b; }); - t["x"] = &trussc::IVec2::x; - t["y"] = &trussc::IVec2::y; - t["toVec2"] = &trussc::IVec2::toVec2; - } - { - sol::usertype t = lua->new_usertype("IVec3", - sol::constructors(), - sol::meta_function::addition, [](const trussc::IVec3& a, const trussc::IVec3 & b){ return a + b; }, - sol::meta_function::subtraction, [](const trussc::IVec3& a, const trussc::IVec3 & b){ return a - b; }, - sol::meta_function::unary_minus, [](const trussc::IVec3& a){ return -a; }, - sol::meta_function::multiplication, [](const trussc::IVec3& a, int b){ return a * b; }, - sol::meta_function::equal_to, [](const trussc::IVec3& a, const trussc::IVec3 & b){ return a == b; }); - t["x"] = &trussc::IVec3::x; - t["y"] = &trussc::IVec3::y; - t["z"] = &trussc::IVec3::z; - t["toVec3"] = &trussc::IVec3::toVec3; - t["xy"] = &trussc::IVec3::xy; - } - { - sol::usertype t = lua->new_usertype("Vec4", - sol::constructors(), - sol::meta_function::index, [](const trussc::Vec4& a, int b){ return a[b]; }, - sol::meta_function::addition, [](const trussc::Vec4& a, const trussc::Vec4 & b){ return a + b; }, - sol::meta_function::subtraction, [](const trussc::Vec4& a, const trussc::Vec4 & b){ return a - b; }, - sol::meta_function::unary_minus, [](const trussc::Vec4& a){ return -a; }, - sol::meta_function::multiplication, [](const trussc::Vec4& a, float b){ return a * b; }, - sol::meta_function::division, [](const trussc::Vec4& a, float b){ return a / b; }, - sol::meta_function::equal_to, [](const trussc::Vec4& a, const trussc::Vec4 & b){ return a == b; }); - t["x"] = &trussc::Vec4::x; - t["y"] = &trussc::Vec4::y; - t["z"] = &trussc::Vec4::z; - t["w"] = &trussc::Vec4::w; - t["set"] = sol::overload([](trussc::Vec4& self, float x_, float y_, float z_, float w_) -> decltype(auto) { return self.set(x_, y_, z_, w_); }, [](trussc::Vec4& self, const trussc::Vec4 & v) -> decltype(auto) { return self.set(v); }); - t["length"] = &trussc::Vec4::length; - t["lengthSquared"] = &trussc::Vec4::lengthSquared; - t["normalized"] = &trussc::Vec4::normalized; - t["normalize"] = &trussc::Vec4::normalize; - t["dot"] = &trussc::Vec4::dot; - t["lerp"] = &trussc::Vec4::lerp; - t["xy"] = &trussc::Vec4::xy; - t["xyz"] = &trussc::Vec4::xyz; - } - { - sol::usertype t = lua->new_usertype("Quaternion", - sol::constructors(), - sol::meta_function::equal_to, [](const trussc::Quaternion& a, const trussc::Quaternion & b){ return a == b; }, - sol::meta_function::multiplication, [](const trussc::Quaternion& a, const trussc::Quaternion & b){ return a * b; }); - t["w"] = &trussc::Quaternion::w; - t["x"] = &trussc::Quaternion::x; - t["y"] = &trussc::Quaternion::y; - t["z"] = &trussc::Quaternion::z; - t["toEuler"] = &trussc::Quaternion::toEuler; - t["toMatrix"] = &trussc::Quaternion::toMatrix; - t["length"] = &trussc::Quaternion::length; - t["lengthSquared"] = &trussc::Quaternion::lengthSquared; - t["normalized"] = &trussc::Quaternion::normalized; - t["normalize"] = &trussc::Quaternion::normalize; - t["conjugate"] = &trussc::Quaternion::conjugate; - t["rotate"] = &trussc::Quaternion::rotate; - t["identity"] = &trussc::Quaternion::identity; - t["fromAxisAngle"] = &trussc::Quaternion::fromAxisAngle; - t["fromEuler"] = sol::overload([](float pitch, float yaw, float roll) { return trussc::Quaternion::fromEuler(pitch, yaw, roll); }, [](const trussc::Vec3 & euler) { return trussc::Quaternion::fromEuler(euler); }); - t["slerp"] = &trussc::Quaternion::slerp; - } - { - sol::usertype t = lua->new_usertype("EnumLabelSpan"); - t["count"] = &trussc::EnumLabelSpan::count; - } - { - sol::usertype t = lua->new_usertype("Reflector"); - t["isReadOnly"] = &trussc::Reflector::isReadOnly; - t["pushReadOnly"] = &trussc::Reflector::pushReadOnly; - t["popReadOnly"] = &trussc::Reflector::popReadOnly; - t["endGroup"] = &trussc::Reflector::endGroup; - } - { - sol::usertype t = lua->new_usertype("Rect", - sol::constructors()); - t["x"] = &trussc::Rect::x; - t["y"] = &trussc::Rect::y; - t["width"] = &trussc::Rect::width; - t["height"] = &trussc::Rect::height; - t["set"] = sol::overload([](trussc::Rect& self, float x_, float y_, float w_, float h_) -> decltype(auto) { return self.set(x_, y_, w_, h_); }, [](trussc::Rect& self, const trussc::Vec2 & pos, float w_, float h_) -> decltype(auto) { return self.set(pos, w_, h_); }); - t["getRight"] = &trussc::Rect::getRight; - t["getBottom"] = &trussc::Rect::getBottom; - t["getCenter"] = &trussc::Rect::getCenter; - t["getCenterX"] = &trussc::Rect::getCenterX; - t["getCenterY"] = &trussc::Rect::getCenterY; - t["contains"] = &trussc::Rect::contains; - t["intersects"] = &trussc::Rect::intersects; - } - { - sol::usertype t = lua->new_usertype("Ray", - sol::constructors()); - t["origin"] = &trussc::Ray::origin; - t["direction"] = &trussc::Ray::direction; - t["at"] = &trussc::Ray::at; - t["transformed"] = &trussc::Ray::transformed; - t["fromScreenPoint2D"] = sol::overload([](float screenX, float screenY) { return trussc::Ray::fromScreenPoint2D(screenX, screenY); }, [](float screenX, float screenY, float startZ) { return trussc::Ray::fromScreenPoint2D(screenX, screenY, startZ); }); - } - { - sol::usertype t = lua->new_usertype("CameraContext"); - t["view"] = &trussc::CameraContext::view; - t["projection"] = &trussc::CameraContext::projection; - t["viewW"] = &trussc::CameraContext::viewW; - t["viewH"] = &trussc::CameraContext::viewH; - t["pickable"] = &trussc::CameraContext::pickable; - t["screenPointToRay"] = &trussc::CameraContext::screenPointToRay; - t["worldToScreen"] = &trussc::CameraContext::worldToScreen; - } - { - sol::usertype t = lua->new_usertype("EventListener", - sol::constructors()); - t["disconnect"] = &trussc::EventListener::disconnect; - t["isConnected"] = &trussc::EventListener::isConnected; - } - { - sol::usertype t = lua->new_usertype("LogEventArgs", - sol::constructors()); - t["level"] = &trussc::LogEventArgs::level; - t["message"] = &trussc::LogEventArgs::message; - t["timestamp"] = &trussc::LogEventArgs::timestamp; - } - { - sol::usertype t = lua->new_usertype("Logger", - sol::constructors()); - t["onLog"] = &trussc::Logger::onLog; - t["log"] = &trussc::Logger::log; - t["setConsoleLogLevel"] = &trussc::Logger::setConsoleLogLevel; - t["getConsoleLogLevel"] = &trussc::Logger::getConsoleLogLevel; - t["setLogFile"] = &trussc::Logger::setLogFile; - t["closeFile"] = &trussc::Logger::closeFile; - t["setFileLogLevel"] = &trussc::Logger::setFileLogLevel; - t["getFileLogLevel"] = &trussc::Logger::getFileLogLevel; - t["getLogFilePath"] = &trussc::Logger::getLogFilePath; - t["isFileOpen"] = &trussc::Logger::isFileOpen; - } - { - sol::usertype t = lua->new_usertype("LogStream", - sol::constructors()); - } - { - sol::usertype t = lua->new_usertype("Color", - sol::constructors(), - sol::meta_function::addition, [](const trussc::Color& a, const trussc::Color & b){ return a + b; }, - sol::meta_function::subtraction, [](const trussc::Color& a, const trussc::Color & b){ return a - b; }, - sol::meta_function::multiplication, [](const trussc::Color& a, float b){ return a * b; }, - sol::meta_function::division, [](const trussc::Color& a, float b){ return a / b; }, - sol::meta_function::equal_to, [](const trussc::Color& a, const trussc::Color & b){ return a == b; }); - t["r"] = &trussc::Color::r; - t["g"] = &trussc::Color::g; - t["b"] = &trussc::Color::b; - t["a"] = &trussc::Color::a; - t["set"] = sol::overload([](trussc::Color& self, float r_, float g_, float b_) -> decltype(auto) { return self.set(r_, g_, b_); }, [](trussc::Color& self, float r_, float g_, float b_, float a_) -> decltype(auto) { return self.set(r_, g_, b_, a_); }, [](trussc::Color& self, float gray) -> decltype(auto) { return self.set(gray); }, [](trussc::Color& self, float gray, float a_) -> decltype(auto) { return self.set(gray, a_); }, [](trussc::Color& self, const trussc::Color & c) -> decltype(auto) { return self.set(c); }); - t["toHex"] = sol::overload([](trussc::Color& self) { return self.toHex(); }, [](trussc::Color& self, bool includeAlpha) { return self.toHex(includeAlpha); }); - t["toLinear"] = &trussc::Color::toLinear; - t["toHSB"] = &trussc::Color::toHSB; - t["toOKLab"] = &trussc::Color::toOKLab; - t["toOKLCH"] = &trussc::Color::toOKLCH; - t["clamped"] = &trussc::Color::clamped; - t["lerpRGB"] = &trussc::Color::lerpRGB; - t["lerpLinear"] = &trussc::Color::lerpLinear; - t["lerpHSB"] = &trussc::Color::lerpHSB; - t["lerpOKLab"] = &trussc::Color::lerpOKLab; - t["lerpOKLCH"] = &trussc::Color::lerpOKLCH; - t["lerp"] = &trussc::Color::lerp; - t["fromBytes"] = sol::overload([](int r, int g, int b) { return trussc::Color::fromBytes(r, g, b); }, [](int r, int g, int b, int a) { return trussc::Color::fromBytes(r, g, b, a); }); - t["fromHex"] = sol::overload([](uint32_t hex) { return trussc::Color::fromHex(hex); }, [](uint32_t hex, bool hasAlpha) { return trussc::Color::fromHex(hex, hasAlpha); }); - t["fromHSB"] = sol::overload([](float h, float s, float b) { return trussc::Color::fromHSB(h, s, b); }, [](float h, float s, float b, float a) { return trussc::Color::fromHSB(h, s, b, a); }); - t["fromOKLCH"] = sol::overload([](float L, float C, float H) { return trussc::Color::fromOKLCH(L, C, H); }, [](float L, float C, float H, float a) { return trussc::Color::fromOKLCH(L, C, H, a); }); - t["fromOKLab"] = sol::overload([](float L, float a_lab, float b_lab) { return trussc::Color::fromOKLab(L, a_lab, b_lab); }, [](float L, float a_lab, float b_lab, float alpha) { return trussc::Color::fromOKLab(L, a_lab, b_lab, alpha); }); - t["fromLinear"] = sol::overload([](float r, float g, float b) { return trussc::Color::fromLinear(r, g, b); }, [](float r, float g, float b, float a) { return trussc::Color::fromLinear(r, g, b, a); }); - } - { - sol::usertype t = lua->new_usertype("ColorLinear", - sol::constructors(), - sol::meta_function::addition, [](const trussc::ColorLinear& a, const trussc::ColorLinear & b){ return a + b; }, - sol::meta_function::subtraction, [](const trussc::ColorLinear& a, const trussc::ColorLinear & b){ return a - b; }, - sol::meta_function::multiplication, sol::overload([](const trussc::ColorLinear& a, float b){ return a * b; }, [](const trussc::ColorLinear& a, const trussc::ColorLinear & b){ return a * b; }), - sol::meta_function::division, [](const trussc::ColorLinear& a, float b){ return a / b; }, - sol::meta_function::equal_to, [](const trussc::ColorLinear& a, const trussc::ColorLinear & b){ return a == b; }); - t["r"] = &trussc::ColorLinear::r; - t["g"] = &trussc::ColorLinear::g; - t["b"] = &trussc::ColorLinear::b; - t["a"] = &trussc::ColorLinear::a; - t["toSRGB"] = &trussc::ColorLinear::toSRGB; - t["toHSB"] = &trussc::ColorLinear::toHSB; - t["toOKLab"] = &trussc::ColorLinear::toOKLab; - t["toOKLCH"] = &trussc::ColorLinear::toOKLCH; - t["clamped"] = &trussc::ColorLinear::clamped; - t["clampedLDR"] = &trussc::ColorLinear::clampedLDR; - t["lerp"] = &trussc::ColorLinear::lerp; - } - { - sol::usertype t = lua->new_usertype("ColorHSB", - sol::constructors()); - t["h"] = &trussc::ColorHSB::h; - t["s"] = &trussc::ColorHSB::s; - t["b"] = &trussc::ColorHSB::b; - t["a"] = &trussc::ColorHSB::a; - t["toRGB"] = &trussc::ColorHSB::toRGB; - t["toLinear"] = &trussc::ColorHSB::toLinear; - t["toOKLab"] = &trussc::ColorHSB::toOKLab; - t["toOKLCH"] = &trussc::ColorHSB::toOKLCH; - t["lerp"] = sol::overload([](trussc::ColorHSB& self, const trussc::ColorHSB & target, float t) { return self.lerp(target, t); }, [](trussc::ColorHSB& self, const trussc::ColorHSB & target, float t, bool shortestPath) { return self.lerp(target, t, shortestPath); }); - } - { - sol::usertype t = lua->new_usertype("ColorOKLab", - sol::constructors()); - t["L"] = &trussc::ColorOKLab::L; - t["a"] = &trussc::ColorOKLab::a; - t["b"] = &trussc::ColorOKLab::b; - t["alpha"] = &trussc::ColorOKLab::alpha; - t["toLinear"] = &trussc::ColorOKLab::toLinear; - t["toRGB"] = &trussc::ColorOKLab::toRGB; - t["toHSB"] = &trussc::ColorOKLab::toHSB; - t["toOKLCH"] = &trussc::ColorOKLab::toOKLCH; - t["lerp"] = &trussc::ColorOKLab::lerp; - } - { - sol::usertype t = lua->new_usertype("ColorOKLCH", - sol::constructors()); - t["L"] = &trussc::ColorOKLCH::L; - t["C"] = &trussc::ColorOKLCH::C; - t["H"] = &trussc::ColorOKLCH::H; - t["alpha"] = &trussc::ColorOKLCH::alpha; - t["toOKLab"] = &trussc::ColorOKLCH::toOKLab; - t["toLinear"] = &trussc::ColorOKLCH::toLinear; - t["toRGB"] = &trussc::ColorOKLCH::toRGB; - t["toHSB"] = &trussc::ColorOKLCH::toHSB; - t["lerp"] = sol::overload([](trussc::ColorOKLCH& self, const trussc::ColorOKLCH & target, float t) { return self.lerp(target, t); }, [](trussc::ColorOKLCH& self, const trussc::ColorOKLCH & target, float t, bool shortestPath) { return self.lerp(target, t, shortestPath); }); - } - { - sol::usertype t = lua->new_usertype("Platform"); - t["isWeb"] = &trussc::Platform::isWeb; - t["isMacOS"] = &trussc::Platform::isMacOS; - t["isIOS"] = &trussc::Platform::isIOS; - t["isWindows"] = &trussc::Platform::isWindows; - t["isAndroid"] = &trussc::Platform::isAndroid; - t["isLinux"] = &trussc::Platform::isLinux; - t["isApple"] = &trussc::Platform::isApple; - t["isMobile"] = &trussc::Platform::isMobile; - t["isDesktop"] = &trussc::Platform::isDesktop; - t["name"] = &trussc::Platform::name; - } - { - sol::usertype t = lua->new_usertype("Location"); - t["latitude"] = &trussc::Location::latitude; - t["longitude"] = &trussc::Location::longitude; - t["altitude"] = &trussc::Location::altitude; - t["accuracy"] = &trussc::Location::accuracy; - } - { - sol::usertype t = lua->new_usertype("GraphicsBackend"); - t["isWebGPU"] = &trussc::GraphicsBackend::isWebGPU; - t["isWebGL2"] = &trussc::GraphicsBackend::isWebGL2; - t["isMetal"] = &trussc::GraphicsBackend::isMetal; - t["isD3D11"] = &trussc::GraphicsBackend::isD3D11; - t["isVulkan"] = &trussc::GraphicsBackend::isVulkan; - t["isOpenGL"] = &trussc::GraphicsBackend::isOpenGL; - t["name"] = &trussc::GraphicsBackend::name; - } - { - sol::usertype t = lua->new_usertype("BuildInfo"); - t["date"] = &trussc::BuildInfo::date; - t["time"] = &trussc::BuildInfo::time; - t["dateTime"] = &trussc::BuildInfo::dateTime; - t["timestamp"] = &trussc::BuildInfo::timestamp; - t["year"] = &trussc::BuildInfo::year; - t["month"] = &trussc::BuildInfo::month; - t["day"] = &trussc::BuildInfo::day; - t["hour"] = &trussc::BuildInfo::hour; - t["minute"] = &trussc::BuildInfo::minute; - t["second"] = &trussc::BuildInfo::second; - } - { - sol::usertype t = lua->new_usertype("KeyEventArgs"); - t["key"] = &trussc::KeyEventArgs::key; - t["isRepeat"] = &trussc::KeyEventArgs::isRepeat; - t["shift"] = &trussc::KeyEventArgs::shift; - t["ctrl"] = &trussc::KeyEventArgs::ctrl; - t["alt"] = &trussc::KeyEventArgs::alt; - t["super"] = &trussc::KeyEventArgs::super; - t["consumed"] = &trussc::KeyEventArgs::consumed; - } - { - sol::usertype t = lua->new_usertype("MouseEventArgs"); - t["x"] = &trussc::MouseEventArgs::x; - t["y"] = &trussc::MouseEventArgs::y; - t["button"] = &trussc::MouseEventArgs::button; - t["shift"] = &trussc::MouseEventArgs::shift; - t["ctrl"] = &trussc::MouseEventArgs::ctrl; - t["alt"] = &trussc::MouseEventArgs::alt; - t["super"] = &trussc::MouseEventArgs::super; - t["pos"] = &trussc::MouseEventArgs::pos; - t["globalPos"] = &trussc::MouseEventArgs::globalPos; - t["consumed"] = &trussc::MouseEventArgs::consumed; - t["syncLegacy"] = &trussc::MouseEventArgs::syncLegacy; - } - { - sol::usertype t = lua->new_usertype("MouseMoveEventArgs"); - t["x"] = &trussc::MouseMoveEventArgs::x; - t["y"] = &trussc::MouseMoveEventArgs::y; - t["deltaX"] = &trussc::MouseMoveEventArgs::deltaX; - t["deltaY"] = &trussc::MouseMoveEventArgs::deltaY; - t["shift"] = &trussc::MouseMoveEventArgs::shift; - t["ctrl"] = &trussc::MouseMoveEventArgs::ctrl; - t["alt"] = &trussc::MouseMoveEventArgs::alt; - t["super"] = &trussc::MouseMoveEventArgs::super; - t["pos"] = &trussc::MouseMoveEventArgs::pos; - t["globalPos"] = &trussc::MouseMoveEventArgs::globalPos; - t["delta"] = &trussc::MouseMoveEventArgs::delta; - t["globalDelta"] = &trussc::MouseMoveEventArgs::globalDelta; - t["consumed"] = &trussc::MouseMoveEventArgs::consumed; - t["syncLegacy"] = &trussc::MouseMoveEventArgs::syncLegacy; - } - { - sol::usertype t = lua->new_usertype("MouseDragEventArgs"); - t["x"] = &trussc::MouseDragEventArgs::x; - t["y"] = &trussc::MouseDragEventArgs::y; - t["deltaX"] = &trussc::MouseDragEventArgs::deltaX; - t["deltaY"] = &trussc::MouseDragEventArgs::deltaY; - t["button"] = &trussc::MouseDragEventArgs::button; - t["shift"] = &trussc::MouseDragEventArgs::shift; - t["ctrl"] = &trussc::MouseDragEventArgs::ctrl; - t["alt"] = &trussc::MouseDragEventArgs::alt; - t["super"] = &trussc::MouseDragEventArgs::super; - t["pos"] = &trussc::MouseDragEventArgs::pos; - t["globalPos"] = &trussc::MouseDragEventArgs::globalPos; - t["delta"] = &trussc::MouseDragEventArgs::delta; - t["globalDelta"] = &trussc::MouseDragEventArgs::globalDelta; - t["consumed"] = &trussc::MouseDragEventArgs::consumed; - t["syncLegacy"] = &trussc::MouseDragEventArgs::syncLegacy; - } - { - sol::usertype t = lua->new_usertype("ScrollEventArgs"); - t["scrollX"] = &trussc::ScrollEventArgs::scrollX; - t["scrollY"] = &trussc::ScrollEventArgs::scrollY; - t["shift"] = &trussc::ScrollEventArgs::shift; - t["ctrl"] = &trussc::ScrollEventArgs::ctrl; - t["alt"] = &trussc::ScrollEventArgs::alt; - t["super"] = &trussc::ScrollEventArgs::super; - t["pos"] = &trussc::ScrollEventArgs::pos; - t["globalPos"] = &trussc::ScrollEventArgs::globalPos; - t["scroll"] = &trussc::ScrollEventArgs::scroll; - t["consumed"] = &trussc::ScrollEventArgs::consumed; - t["syncLegacy"] = &trussc::ScrollEventArgs::syncLegacy; - } - { - sol::usertype t = lua->new_usertype("ResizeEventArgs"); - t["width"] = &trussc::ResizeEventArgs::width; - t["height"] = &trussc::ResizeEventArgs::height; - } - { - sol::usertype t = lua->new_usertype("DragDropEventArgs"); - t["files"] = &trussc::DragDropEventArgs::files; - t["x"] = &trussc::DragDropEventArgs::x; - t["y"] = &trussc::DragDropEventArgs::y; - } - { - sol::usertype t = lua->new_usertype("ClipboardPastedEventArgs"); - t["text"] = &trussc::ClipboardPastedEventArgs::text; - } - { - sol::usertype t = lua->new_usertype("TouchPoint"); - t["id"] = &trussc::TouchPoint::id; - t["x"] = &trussc::TouchPoint::x; - t["y"] = &trussc::TouchPoint::y; - t["pressure"] = &trussc::TouchPoint::pressure; - t["changed"] = &trussc::TouchPoint::changed; - } - { - sol::usertype t = lua->new_usertype("TouchEventArgs"); - t["numTouches"] = &trussc::TouchEventArgs::numTouches; - t["cancelled"] = &trussc::TouchEventArgs::cancelled; - t["x"] = &trussc::TouchEventArgs::x; - t["y"] = &trussc::TouchEventArgs::y; - t["id"] = &trussc::TouchEventArgs::id; - } - { - sol::usertype t = lua->new_usertype("ConsoleEventArgs"); - t["raw"] = &trussc::ConsoleEventArgs::raw; - t["args"] = &trussc::ConsoleEventArgs::args; - } - { - sol::usertype t = lua->new_usertype("ExitRequestEventArgs"); - t["cancel"] = &trussc::ExitRequestEventArgs::cancel; - } - { - sol::usertype t = lua->new_usertype("CoreEvents"); - t["setup"] = &trussc::CoreEvents::setup; - t["update"] = &trussc::CoreEvents::update; - t["draw"] = &trussc::CoreEvents::draw; - t["onRender"] = &trussc::CoreEvents::onRender; - t["afterFrame"] = &trussc::CoreEvents::afterFrame; - t["exit"] = &trussc::CoreEvents::exit; - t["exitRequested"] = &trussc::CoreEvents::exitRequested; - t["keyPressed"] = &trussc::CoreEvents::keyPressed; - t["keyReleased"] = &trussc::CoreEvents::keyReleased; - t["mousePressed"] = &trussc::CoreEvents::mousePressed; - t["mouseReleased"] = &trussc::CoreEvents::mouseReleased; - t["mouseMoved"] = &trussc::CoreEvents::mouseMoved; - t["mouseDragged"] = &trussc::CoreEvents::mouseDragged; - t["mouseScrolled"] = &trussc::CoreEvents::mouseScrolled; - t["windowResized"] = &trussc::CoreEvents::windowResized; - t["filesDropped"] = &trussc::CoreEvents::filesDropped; - t["clipboardPasted"] = &trussc::CoreEvents::clipboardPasted; - t["console"] = &trussc::CoreEvents::console; - t["touchPressed"] = &trussc::CoreEvents::touchPressed; - t["touchMoved"] = &trussc::CoreEvents::touchMoved; - t["touchReleased"] = &trussc::CoreEvents::touchReleased; - t["rawEvent"] = &trussc::CoreEvents::rawEvent; - } - { - sol::usertype t = lua->new_usertype("SoundStream", - sol::constructors()); - t["loadStream"] = sol::overload([](trussc::SoundStream& self, const std::string & path) { return self.loadStream(path); }, [](trussc::SoundStream& self, const std::string & path, int maxPolyphony) { return self.loadStream(path, maxPolyphony); }); - t["getDuration"] = &trussc::SoundStream::getDuration; - t["getPath"] = &trussc::SoundStream::getPath; - t["getMaxPolyphony"] = &trussc::SoundStream::getMaxPolyphony; - } - { - sol::usertype t = lua->new_usertype("PlayingSound"); - t["buffer"] = &trussc::PlayingSound::buffer; - t["volume"] = &trussc::PlayingSound::volume; - t["pan"] = &trussc::PlayingSound::pan; - t["speed"] = &trussc::PlayingSound::speed; - t["loop"] = &trussc::PlayingSound::loop; - t["playing"] = &trussc::PlayingSound::playing; - t["paused"] = &trussc::PlayingSound::paused; - t["mixMode"] = &trussc::PlayingSound::mixMode; - t["positionF"] = &trussc::PlayingSound::positionF; - t["rateRatio"] = &trussc::PlayingSound::rateRatio; - } - { - sol::usertype t = lua->new_usertype("AudioSettings"); - t["sampleRate"] = &trussc::AudioSettings::sampleRate; - t["channels"] = &trussc::AudioSettings::channels; - t["bufferSize"] = &trussc::AudioSettings::bufferSize; - t["maxPolyphony"] = &trussc::AudioSettings::maxPolyphony; - t["deviceName"] = &trussc::AudioSettings::deviceName; - } - { - sol::usertype t = lua->new_usertype("AudioDeviceInfo"); - t["name"] = &trussc::AudioDeviceInfo::name; - t["isDefault"] = &trussc::AudioDeviceInfo::isDefault; - } - { - sol::usertype t = lua->new_usertype("AudioDeviceChangedArgs"); - t["deviceName"] = &trussc::AudioDeviceChangedArgs::deviceName; - t["isDefaultDevice"] = &trussc::AudioDeviceChangedArgs::isDefaultDevice; - t["sampleRate"] = &trussc::AudioDeviceChangedArgs::sampleRate; - t["channels"] = &trussc::AudioDeviceChangedArgs::channels; - t["bufferSize"] = &trussc::AudioDeviceChangedArgs::bufferSize; - t["maxPolyphony"] = &trussc::AudioDeviceChangedArgs::maxPolyphony; - } - { - sol::usertype t = lua->new_usertype("AudioOutBuffer"); - t["frameCount"] = &trussc::AudioOutBuffer::frameCount; - t["channels"] = &trussc::AudioOutBuffer::channels; - t["sampleRate"] = &trussc::AudioOutBuffer::sampleRate; - t["framePosition"] = &trussc::AudioOutBuffer::framePosition; - } - { - sol::usertype t = lua->new_usertype("AudioInBuffer"); - t["frameCount"] = &trussc::AudioInBuffer::frameCount; - t["channels"] = &trussc::AudioInBuffer::channels; - t["sampleRate"] = &trussc::AudioInBuffer::sampleRate; - t["framePosition"] = &trussc::AudioInBuffer::framePosition; - } - { - sol::usertype t = lua->new_usertype("Sound", - sol::constructors()); - t["load"] = &trussc::Sound::load; -#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) - t["loadStream"] = sol::overload([](trussc::Sound& self, const std::string & path) { return self.loadStream(path); }, [](trussc::Sound& self, const std::string & path, int maxPolyphony) { return self.loadStream(path, maxPolyphony); }); -#endif - t["loadTestTone"] = sol::overload([](trussc::Sound& self) { return self.loadTestTone(); }, [](trussc::Sound& self, float frequency) { return self.loadTestTone(frequency); }, [](trussc::Sound& self, float frequency, float duration) { return self.loadTestTone(frequency, duration); }); - t["loadFromBuffer"] = sol::overload([](trussc::Sound& self, const trussc::SoundBuffer & buf) { return self.loadFromBuffer(buf); }, [](trussc::Sound& self, std::shared_ptr buf) { return self.loadFromBuffer(buf); }); - t["isLoaded"] = &trussc::Sound::isLoaded; - t["isStreaming"] = &trussc::Sound::isStreaming; - t["play"] = &trussc::Sound::play; - t["stop"] = &trussc::Sound::stop; - t["pause"] = &trussc::Sound::pause; - t["resume"] = &trussc::Sound::resume; - t["setVolume"] = &trussc::Sound::setVolume; - t["getVolume"] = &trussc::Sound::getVolume; - t["setLoop"] = &trussc::Sound::setLoop; - t["isLoop"] = &trussc::Sound::isLoop; - t["setPan"] = &trussc::Sound::setPan; - t["getPan"] = &trussc::Sound::getPan; - t["setSpeed"] = &trussc::Sound::setSpeed; - t["getSpeed"] = &trussc::Sound::getSpeed; - t["setMixMode"] = &trussc::Sound::setMixMode; - t["getMixMode"] = &trussc::Sound::getMixMode; - t["setChannelMap"] = sol::overload([](trussc::Sound& self, const std::vector & map) { return self.setChannelMap(map); }, [](trussc::Sound& self, std::vector> map) { return self.setChannelMap(map); }); - t["getChannelMap"] = &trussc::Sound::getChannelMap; - t["setChannelGains"] = &trussc::Sound::setChannelGains; - t["getChannelGains"] = &trussc::Sound::getChannelGains; - t["clearChannelMap"] = &trussc::Sound::clearChannelMap; - t["clearChannelGains"] = &trussc::Sound::clearChannelGains; - t["isPlaying"] = &trussc::Sound::isPlaying; - t["isPaused"] = &trussc::Sound::isPaused; - t["getPosition"] = &trussc::Sound::getPosition; - t["setPosition"] = &trussc::Sound::setPosition; - t["getDuration"] = &trussc::Sound::getDuration; - } - { - sol::usertype t = lua->new_usertype("FileDialogResult"); - t["filePath"] = &trussc::FileDialogResult::filePath; - t["fileName"] = &trussc::FileDialogResult::fileName; - t["success"] = &trussc::FileDialogResult::success; - } - { - sol::usertype t = lua->new_usertype("JsonWriteReflector"); - t["members"] = &trussc::JsonWriteReflector::members; - t["endGroup"] = &trussc::JsonWriteReflector::endGroup; - } - { - sol::usertype t = lua->new_usertype("JsonReadReflector", - sol::constructors()); - t["applied"] = &trussc::JsonReadReflector::applied; - t["skipped"] = &trussc::JsonReadReflector::skipped; - t["readOnly"] = &trussc::JsonReadReflector::readOnly; - t["unknownKeys"] = &trussc::JsonReadReflector::unknownKeys; - t["endGroup"] = &trussc::JsonReadReflector::endGroup; - } - { - sol::usertype t = lua->new_usertype("FileWriter", - sol::constructors()); - t["open"] = sol::overload([](trussc::FileWriter& self, const std::string & path) { return self.open(path); }, [](trussc::FileWriter& self, const std::string & path, bool append) { return self.open(path, append); }); - t["close"] = &trussc::FileWriter::close; - t["isOpen"] = &trussc::FileWriter::isOpen; - t["write"] = sol::overload([](trussc::FileWriter& self, const std::string & text) -> decltype(auto) { return self.write(text); }, [](trussc::FileWriter& self, char c) -> decltype(auto) { return self.write(c); }); - t["writeLine"] = sol::overload([](trussc::FileWriter& self) -> decltype(auto) { return self.writeLine(); }, [](trussc::FileWriter& self, const std::string & text) -> decltype(auto) { return self.writeLine(text); }); - t["flush"] = &trussc::FileWriter::flush; - } - { - sol::usertype t = lua->new_usertype("FileReader", - sol::constructors()); - t["open"] = &trussc::FileReader::open; - t["close"] = &trussc::FileReader::close; - t["isOpen"] = &trussc::FileReader::isOpen; - t["eof"] = &trussc::FileReader::eof; - t["readLine"] = [](trussc::FileReader& self) { return self.readLine(); }; - t["readChar"] = &trussc::FileReader::readChar; - t["seek"] = &trussc::FileReader::seek; - t["tell"] = &trussc::FileReader::tell; - t["remaining"] = &trussc::FileReader::remaining; - } - { - sol::usertype t = lua->new_usertype("ShaderVertex"); - t["x"] = &trussc::ShaderVertex::x; - t["y"] = &trussc::ShaderVertex::y; - t["z"] = &trussc::ShaderVertex::z; - t["u"] = &trussc::ShaderVertex::u; - t["v"] = &trussc::ShaderVertex::v; - t["r"] = &trussc::ShaderVertex::r; - t["g"] = &trussc::ShaderVertex::g; - t["b"] = &trussc::ShaderVertex::b; - t["a"] = &trussc::ShaderVertex::a; - } - { - sol::usertype t = lua->new_usertype("CurveStyle"); - t["mode"] = &trussc::CurveStyle::mode; - t["tolerance"] = &trussc::CurveStyle::tolerance; - t["resolution"] = &trussc::CurveStyle::resolution; - } - { - sol::usertype t = lua->new_usertype("FpsSettings"); - t["updateFps"] = &trussc::FpsSettings::updateFps; - t["drawFps"] = &trussc::FpsSettings::drawFps; - t["actualVsyncFps"] = &trussc::FpsSettings::actualVsyncFps; - t["synced"] = &trussc::FpsSettings::synced; - } - { - sol::usertype t = lua->new_usertype("WindowSettings"); - t["width"] = &trussc::WindowSettings::width; - t["height"] = &trussc::WindowSettings::height; - t["title"] = &trussc::WindowSettings::title; - t["highDpi"] = &trussc::WindowSettings::highDpi; - t["pixelPerfect"] = &trussc::WindowSettings::pixelPerfect; - t["sampleCount"] = &trussc::WindowSettings::sampleCount; - t["fullscreen"] = &trussc::WindowSettings::fullscreen; - t["decorated"] = &trussc::WindowSettings::decorated; - t["clipboardSize"] = &trussc::WindowSettings::clipboardSize; - t["swapInterval"] = &trussc::WindowSettings::swapInterval; - t["uniformBufferReserve"] = &trussc::WindowSettings::uniformBufferReserve; - t["setSize"] = &trussc::WindowSettings::setSize; - t["setTitle"] = &trussc::WindowSettings::setTitle; - t["setHighDpi"] = &trussc::WindowSettings::setHighDpi; - t["setPixelPerfect"] = &trussc::WindowSettings::setPixelPerfect; - t["setSampleCount"] = &trussc::WindowSettings::setSampleCount; - t["setFullscreen"] = &trussc::WindowSettings::setFullscreen; - t["setDecorated"] = &trussc::WindowSettings::setDecorated; - t["setClipboardSize"] = &trussc::WindowSettings::setClipboardSize; - t["setSwapInterval"] = &trussc::WindowSettings::setSwapInterval; - t["reserveUniformBuffer"] = &trussc::WindowSettings::reserveUniformBuffer; - } - { - sol::usertype t = lua->new_usertype("Path", - sol::constructors &), trussc::Path(const std::vector &)>(), - sol::meta_function::index, [](const trussc::Path& a, int b){ return a[b]; }); - t["addVertex"] = sol::overload([](trussc::Path& self, float x, float y) { return self.addVertex(x, y); }, [](trussc::Path& self, float x, float y, float z) { return self.addVertex(x, y, z); }, [](trussc::Path& self, const trussc::Vec2 & v) { return self.addVertex(v); }, [](trussc::Path& self, const trussc::Vec3 & v) { return self.addVertex(v); }); - t["addVertices"] = sol::overload([](trussc::Path& self, const std::vector & verts) { return self.addVertices(verts); }, [](trussc::Path& self, const std::vector & verts) { return self.addVertices(verts); }); - t["getVertices"] = [](trussc::Path& self) -> decltype(auto) { return self.getVertices(); }; - t["size"] = &trussc::Path::size; - t["empty"] = &trussc::Path::empty; - t["clear"] = &trussc::Path::clear; - t["moveTo"] = sol::overload([](trussc::Path& self, float x, float y) { return self.moveTo(x, y); }, [](trussc::Path& self, float x, float y, float z) { return self.moveTo(x, y, z); }, [](trussc::Path& self, const trussc::Vec2 & p) { return self.moveTo(p); }, [](trussc::Path& self, const trussc::Vec3 & p) { return self.moveTo(p); }); - t["getNumSubpaths"] = &trussc::Path::getNumSubpaths; - t["getSubpathRange"] = &trussc::Path::getSubpathRange; - t["isSubpathClosed"] = &trussc::Path::isSubpathClosed; - t["lineTo"] = sol::overload([](trussc::Path& self, float x, float y) { return self.lineTo(x, y); }, [](trussc::Path& self, float x, float y, float z) { return self.lineTo(x, y, z); }, [](trussc::Path& self, const trussc::Vec2 & p) { return self.lineTo(p); }, [](trussc::Path& self, const trussc::Vec3 & p) { return self.lineTo(p); }); - t["bezierTo"] = sol::overload([](trussc::Path& self, const trussc::Vec3 & cp1, const trussc::Vec3 & cp2, const trussc::Vec3 & to, int resolution) { return self.bezierTo(cp1, cp2, to, resolution); }, [](trussc::Path& self, float cx1, float cy1, float cx2, float cy2, float x, float y, int resolution) { return self.bezierTo(cx1, cy1, cx2, cy2, x, y, resolution); }, [](trussc::Path& self, const trussc::Vec2 & cp1, const trussc::Vec2 & cp2, const trussc::Vec2 & to, int resolution) { return self.bezierTo(cp1, cp2, to, resolution); }); - t["quadBezierTo"] = sol::overload([](trussc::Path& self, const trussc::Vec3 & cp, const trussc::Vec3 & to, int resolution) { return self.quadBezierTo(cp, to, resolution); }, [](trussc::Path& self, float cx, float cy, float x, float y, int resolution) { return self.quadBezierTo(cx, cy, x, y, resolution); }, [](trussc::Path& self, const trussc::Vec2 & cp, const trussc::Vec2 & to, int resolution) { return self.quadBezierTo(cp, to, resolution); }); - t["curveTo"] = sol::overload([](trussc::Path& self, const trussc::Vec3 & to, int resolution) { return self.curveTo(to, resolution); }, [](trussc::Path& self, float x, float y) { return self.curveTo(x, y); }, [](trussc::Path& self, float x, float y, float z) { return self.curveTo(x, y, z); }, [](trussc::Path& self, float x, float y, float z, int resolution) { return self.curveTo(x, y, z, resolution); }, [](trussc::Path& self, const trussc::Vec2 & to, int resolution) { return self.curveTo(to, resolution); }); - t["arc"] = sol::overload([](trussc::Path& self, const trussc::Vec3 & center, float radiusX, float radiusY, float angleBegin, float angleEnd) { return self.arc(center, radiusX, radiusY, angleBegin, angleEnd); }, [](trussc::Path& self, const trussc::Vec3 & center, float radiusX, float radiusY, float angleBegin, float angleEnd, bool clockwise) { return self.arc(center, radiusX, radiusY, angleBegin, angleEnd, clockwise); }, [](trussc::Path& self, const trussc::Vec3 & center, float radiusX, float radiusY, float angleBegin, float angleEnd, bool clockwise, int circleResolution) { return self.arc(center, radiusX, radiusY, angleBegin, angleEnd, clockwise, circleResolution); }, [](trussc::Path& self, float x, float y, float radiusX, float radiusY, float angleBegin, float angleEnd) { return self.arc(x, y, radiusX, radiusY, angleBegin, angleEnd); }, [](trussc::Path& self, float x, float y, float radiusX, float radiusY, float angleBegin, float angleEnd, int circleResolution) { return self.arc(x, y, radiusX, radiusY, angleBegin, angleEnd, circleResolution); }, [](trussc::Path& self, const trussc::Vec2 & center, float radiusX, float radiusY, float angleBegin, float angleEnd) { return self.arc(center, radiusX, radiusY, angleBegin, angleEnd); }, [](trussc::Path& self, const trussc::Vec2 & center, float radiusX, float radiusY, float angleBegin, float angleEnd, int circleResolution) { return self.arc(center, radiusX, radiusY, angleBegin, angleEnd, circleResolution); }, [](trussc::Path& self, const trussc::Vec3 & center, float radius, float angleBegin, float angleEnd) { return self.arc(center, radius, angleBegin, angleEnd); }, [](trussc::Path& self, const trussc::Vec3 & center, float radius, float angleBegin, float angleEnd, bool clockwise) { return self.arc(center, radius, angleBegin, angleEnd, clockwise); }, [](trussc::Path& self, float x, float y, float radius, float angleBegin, float angleEnd) { return self.arc(x, y, radius, angleBegin, angleEnd); }, [](trussc::Path& self, float x, float y, float radius, float angleBegin, float angleEnd, bool clockwise) { return self.arc(x, y, radius, angleBegin, angleEnd, clockwise); }, [](trussc::Path& self, const trussc::Vec2 & center, float radius, float angleBegin, float angleEnd) { return self.arc(center, radius, angleBegin, angleEnd); }, [](trussc::Path& self, const trussc::Vec2 & center, float radius, float angleBegin, float angleEnd, bool clockwise) { return self.arc(center, radius, angleBegin, angleEnd, clockwise); }); - t["close"] = &trussc::Path::close; - t["setClosed"] = &trussc::Path::setClosed; - t["isClosed"] = &trussc::Path::isClosed; - t["reverseWinding"] = sol::overload([](trussc::Path& self, size_t i) -> decltype(auto) { return self.reverseWinding(i); }, [](trussc::Path& self) -> decltype(auto) { return self.reverseWinding(); }); - t["draw"] = &trussc::Path::draw; - t["buildFillTriangles"] = &trussc::Path::buildFillTriangles; - t["drawFill"] = &trussc::Path::drawFill; - t["toFillMesh"] = &trussc::Path::toFillMesh; - t["drawStroke"] = &trussc::Path::drawStroke; - t["getBounds"] = &trussc::Path::getBounds; - t["getPerimeter"] = &trussc::Path::getPerimeter; - } - { - sol::usertype t = lua->new_usertype("IesProfile", - sol::constructors()); - t["load"] = &trussc::IesProfile::load; - t["loadFromString"] = &trussc::IesProfile::loadFromString; - t["isLoaded"] = &trussc::IesProfile::isLoaded; - t["getMaxVerticalAngle"] = &trussc::IesProfile::getMaxVerticalAngle; - t["getMaxCandela"] = &trussc::IesProfile::getMaxCandela; - t["getTextureWidth"] = &trussc::IesProfile::getTextureWidth; - t["getView"] = &trussc::IesProfile::getView; - t["getSampler"] = &trussc::IesProfile::getSampler; - } - { - sol::usertype t = lua->new_usertype("HasTexture"); - t["getTexture"] = [](trussc::HasTexture& self) -> decltype(auto) { return self.getTexture(); }; - t["hasTexture"] = &trussc::HasTexture::hasTexture; - t["draw"] = sol::overload([](trussc::HasTexture& self, float x, float y) { return self.draw(x, y); }, [](trussc::HasTexture& self, float x, float y, float w, float h) { return self.draw(x, y, w, h); }); - t["setMinFilter"] = &trussc::HasTexture::setMinFilter; - t["setMagFilter"] = &trussc::HasTexture::setMagFilter; - t["setFilter"] = &trussc::HasTexture::setFilter; - t["getMinFilter"] = &trussc::HasTexture::getMinFilter; - t["getMagFilter"] = &trussc::HasTexture::getMagFilter; - t["setWrapU"] = &trussc::HasTexture::setWrapU; - t["setWrapV"] = &trussc::HasTexture::setWrapV; - t["setWrap"] = &trussc::HasTexture::setWrap; - t["getWrapU"] = &trussc::HasTexture::getWrapU; - t["getWrapV"] = &trussc::HasTexture::getWrapV; - t["save"] = &trussc::HasTexture::save; - } - { - sol::usertype t = lua->new_usertype("Mesh", - sol::constructors()); - t["setMode"] = &trussc::Mesh::setMode; - t["getMode"] = &trussc::Mesh::getMode; - t["addVertex"] = sol::overload([](trussc::Mesh& self, float x, float y) -> decltype(auto) { return self.addVertex(x, y); }, [](trussc::Mesh& self, float x, float y, float z) -> decltype(auto) { return self.addVertex(x, y, z); }, [](trussc::Mesh& self, const trussc::Vec2 & v) -> decltype(auto) { return self.addVertex(v); }, [](trussc::Mesh& self, const trussc::Vec3 & v) -> decltype(auto) { return self.addVertex(v); }); - t["addVertices"] = &trussc::Mesh::addVertices; - t["getVertices"] = [](trussc::Mesh& self) -> decltype(auto) { return self.getVertices(); }; - t["getNumVertices"] = &trussc::Mesh::getNumVertices; - t["addColor"] = sol::overload([](trussc::Mesh& self, const trussc::Color & c) -> decltype(auto) { return self.addColor(c); }, [](trussc::Mesh& self, float r, float g, float b) -> decltype(auto) { return self.addColor(r, g, b); }, [](trussc::Mesh& self, float r, float g, float b, float a) -> decltype(auto) { return self.addColor(r, g, b, a); }); - t["addColors"] = &trussc::Mesh::addColors; - t["getColors"] = [](trussc::Mesh& self) -> decltype(auto) { return self.getColors(); }; - t["getNumColors"] = &trussc::Mesh::getNumColors; - t["hasColors"] = &trussc::Mesh::hasColors; - t["addIndex"] = &trussc::Mesh::addIndex; - t["addIndices"] = &trussc::Mesh::addIndices; - t["addTriangle"] = &trussc::Mesh::addTriangle; - t["getIndices"] = [](trussc::Mesh& self) -> decltype(auto) { return self.getIndices(); }; - t["getNumIndices"] = &trussc::Mesh::getNumIndices; - t["hasIndices"] = &trussc::Mesh::hasIndices; - t["addNormal"] = sol::overload([](trussc::Mesh& self, float nx, float ny, float nz) -> decltype(auto) { return self.addNormal(nx, ny, nz); }, [](trussc::Mesh& self, const trussc::Vec3 & n) -> decltype(auto) { return self.addNormal(n); }); - t["addNormals"] = &trussc::Mesh::addNormals; - t["setNormal"] = &trussc::Mesh::setNormal; - t["getNormal"] = &trussc::Mesh::getNormal; - t["getNormals"] = [](trussc::Mesh& self) -> decltype(auto) { return self.getNormals(); }; - t["getNumNormals"] = &trussc::Mesh::getNumNormals; - t["hasNormals"] = &trussc::Mesh::hasNormals; - t["addTexCoord"] = sol::overload([](trussc::Mesh& self, float u, float v) -> decltype(auto) { return self.addTexCoord(u, v); }, [](trussc::Mesh& self, const trussc::Vec2 & t) -> decltype(auto) { return self.addTexCoord(t); }); - t["getTexCoords"] = [](trussc::Mesh& self) -> decltype(auto) { return self.getTexCoords(); }; - t["getNumTexCoords"] = &trussc::Mesh::getNumTexCoords; - t["hasTexCoords"] = &trussc::Mesh::hasTexCoords; - t["hasValidTexCoords"] = &trussc::Mesh::hasValidTexCoords; - t["addTangent"] = sol::overload([](trussc::Mesh& self, float tx, float ty, float tz) -> decltype(auto) { return self.addTangent(tx, ty, tz); }, [](trussc::Mesh& self, float tx, float ty, float tz, float tw) -> decltype(auto) { return self.addTangent(tx, ty, tz, tw); }, [](trussc::Mesh& self, const trussc::Vec4 & t) -> decltype(auto) { return self.addTangent(t); }, [](trussc::Mesh& self, const trussc::Vec3 & t) -> decltype(auto) { return self.addTangent(t); }, [](trussc::Mesh& self, const trussc::Vec3 & t, float w) -> decltype(auto) { return self.addTangent(t, w); }); - t["getTangents"] = [](trussc::Mesh& self) -> decltype(auto) { return self.getTangents(); }; - t["getNumTangents"] = &trussc::Mesh::getNumTangents; - t["hasTangents"] = &trussc::Mesh::hasTangents; - t["clear"] = &trussc::Mesh::clear; - t["clearVertices"] = &trussc::Mesh::clearVertices; - t["clearNormals"] = &trussc::Mesh::clearNormals; - t["clearColors"] = &trussc::Mesh::clearColors; - t["clearIndices"] = &trussc::Mesh::clearIndices; - t["clearTexCoords"] = &trussc::Mesh::clearTexCoords; - t["clearTangents"] = &trussc::Mesh::clearTangents; - t["translate"] = sol::overload([](trussc::Mesh& self, float x, float y, float z) -> decltype(auto) { return self.translate(x, y, z); }, [](trussc::Mesh& self, const trussc::Vec3 & offset) -> decltype(auto) { return self.translate(offset); }); - t["rotateX"] = &trussc::Mesh::rotateX; - t["rotateY"] = &trussc::Mesh::rotateY; - t["rotateZ"] = &trussc::Mesh::rotateZ; - t["scale"] = sol::overload([](trussc::Mesh& self, float x, float y, float z) -> decltype(auto) { return self.scale(x, y, z); }, [](trussc::Mesh& self, float s) -> decltype(auto) { return self.scale(s); }, [](trussc::Mesh& self, const trussc::Vec3 & s) -> decltype(auto) { return self.scale(s); }); - t["transform"] = &trussc::Mesh::transform; - t["append"] = &trussc::Mesh::append; - t["draw"] = sol::overload([](trussc::Mesh& self) { return self.draw(); }, [](trussc::Mesh& self, const trussc::Texture & texture) { return self.draw(texture); }, [](trussc::Mesh& self, const trussc::Image & image) { return self.draw(image); }); - t["drawNoLighting"] = &trussc::Mesh::drawNoLighting; - t["drawWithLighting"] = &trussc::Mesh::drawWithLighting; - t["drawNoLightingWithTexture"] = &trussc::Mesh::drawNoLightingWithTexture; - t["drawWireframe"] = &trussc::Mesh::drawWireframe; - t["markGpuDirty"] = &trussc::Mesh::markGpuDirty; - t["uploadToGpu"] = &trussc::Mesh::uploadToGpu; - t["drawGpuPbr"] = &trussc::Mesh::drawGpuPbr; - t["drawGpuPoints"] = &trussc::Mesh::drawGpuPoints; - t["uploadPointsToGpu"] = &trussc::Mesh::uploadPointsToGpu; - t["getGpuVertexBuffer"] = &trussc::Mesh::getGpuVertexBuffer; - t["getGpuIndexBuffer"] = &trussc::Mesh::getGpuIndexBuffer; - t["getGpuVertexCount"] = &trussc::Mesh::getGpuVertexCount; - t["getGpuIndexCount"] = &trussc::Mesh::getGpuIndexCount; - t["getGpuPointBuffer"] = &trussc::Mesh::getGpuPointBuffer; - t["getGpuPointCount"] = &trussc::Mesh::getGpuPointCount; - } - { - sol::usertype t = lua->new_usertype("StrokeMesh", - sol::constructors()); - t["setWidth"] = &trussc::StrokeMesh::setWidth; - t["setColor"] = &trussc::StrokeMesh::setColor; - t["setCapType"] = &trussc::StrokeMesh::setCapType; - t["setJoinType"] = &trussc::StrokeMesh::setJoinType; - t["setMiterLimit"] = &trussc::StrokeMesh::setMiterLimit; - t["addVertex"] = sol::overload([](trussc::StrokeMesh& self, float x, float y) -> decltype(auto) { return self.addVertex(x, y); }, [](trussc::StrokeMesh& self, float x, float y, float z) -> decltype(auto) { return self.addVertex(x, y, z); }, [](trussc::StrokeMesh& self, const trussc::Vec3 & p) -> decltype(auto) { return self.addVertex(p); }, [](trussc::StrokeMesh& self, const trussc::Vec2 & p) -> decltype(auto) { return self.addVertex(p); }); - t["addVertexWithWidth"] = sol::overload([](trussc::StrokeMesh& self, float x, float y, float width) -> decltype(auto) { return self.addVertexWithWidth(x, y, width); }, [](trussc::StrokeMesh& self, const trussc::Vec3 & p, float width) -> decltype(auto) { return self.addVertexWithWidth(p, width); }); - t["setWidths"] = &trussc::StrokeMesh::setWidths; - t["setShape"] = &trussc::StrokeMesh::setShape; - t["setClosed"] = &trussc::StrokeMesh::setClosed; - t["clear"] = &trussc::StrokeMesh::clear; - t["update"] = &trussc::StrokeMesh::update; - t["draw"] = &trussc::StrokeMesh::draw; - t["getMesh"] = &trussc::StrokeMesh::getMesh; - t["getPolylines"] = &trussc::StrokeMesh::getPolylines; - } - { - sol::usertype t = lua->new_usertype("Font", - sol::constructors()); - t["load"] = &trussc::Font::load; - t["isLoaded"] = &trussc::Font::isLoaded; - t["setAlign"] = sol::overload([](trussc::Font& self, trussc::Direction h, trussc::Direction v) { return self.setAlign(h, v); }, [](trussc::Font& self, trussc::Direction h) { return self.setAlign(h); }); - t["getAlignH"] = &trussc::Font::getAlignH; - t["getAlignV"] = &trussc::Font::getAlignV; - t["setLineHeight"] = &trussc::Font::setLineHeight; - t["setLineHeightEm"] = &trussc::Font::setLineHeightEm; - t["resetLineHeight"] = &trussc::Font::resetLineHeight; - t["drawString"] = sol::overload([](trussc::Font& self, const std::string & text, float x, float y) { return self.drawString(text, x, y); }, [](trussc::Font& self, const std::string & text, float x, float y, trussc::Direction h, trussc::Direction v) { return self.drawString(text, x, y, h, v); }); - t["getGlyphPath"] = &trussc::Font::getGlyphPath; - t["getStringPath"] = sol::overload([](trussc::Font& self, const std::string & text, float x, float y, trussc::Direction h, trussc::Direction v) { return self.getStringPath(text, x, y, h, v); }, [](trussc::Font& self, const std::string & text, float x, float y) { return self.getStringPath(text, x, y); }); - t["setWritingMode"] = &trussc::Font::setWritingMode; - t["getWritingMode"] = &trussc::Font::getWritingMode; - t["setTcyDigits"] = &trussc::Font::setTcyDigits; - t["setTcyLatin"] = &trussc::Font::setTcyLatin; - t["getTcyLatinMode"] = &trussc::Font::getTcyLatinMode; - t["getTcyDigitMax"] = &trussc::Font::getTcyDigitMax; - t["enableWrap"] = &trussc::Font::enableWrap; - t["isWrapEnabled"] = &trussc::Font::isWrapEnabled; - t["setMaxLineLength"] = &trussc::Font::setMaxLineLength; - t["getMaxLineLength"] = &trussc::Font::getMaxLineLength; - t["setLatinHyphenation"] = &trussc::Font::setLatinHyphenation; - t["getLatinHyphenation"] = &trussc::Font::getLatinHyphenation; - t["setHangingPunctuation"] = &trussc::Font::setHangingPunctuation; - t["getHangingPunctuation"] = &trussc::Font::getHangingPunctuation; - t["setKinsoku"] = &trussc::Font::setKinsoku; - t["getKinsoku"] = &trussc::Font::getKinsoku; - t["forEachGlyph"] = sol::overload([](trussc::Font& self, const std::string & text, float x, float y, trussc::Direction h, trussc::Direction v, const trussc::Font::GlyphVisitor & visitor) { return self.forEachGlyph(text, x, y, h, v, visitor); }, [](trussc::Font& self, const std::string & text, float x, float y, const trussc::Font::GlyphVisitor & visitor) { return self.forEachGlyph(text, x, y, visitor); }); - t["getWidth"] = &trussc::Font::getWidth; - t["stringWidth"] = &trussc::Font::stringWidth; - t["getHeight"] = &trussc::Font::getHeight; - t["getBBox"] = &trussc::Font::getBBox; - t["getLineHeight"] = &trussc::Font::getLineHeight; - t["getDefaultLineHeight"] = &trussc::Font::getDefaultLineHeight; - t["getAscent"] = &trussc::Font::getAscent; - t["getDescent"] = &trussc::Font::getDescent; - t["getSize"] = &trussc::Font::getSize; - t["getMemoryUsage"] = &trussc::Font::getMemoryUsage; - t["getAtlasMemoryUsage"] = &trussc::Font::getAtlasMemoryUsage; - t["clearAtlas"] = &trussc::Font::clearAtlas; - t["getAtlasCount"] = &trussc::Font::getAtlasCount; - t["getSampler"] = &trussc::Font::getSampler; - t["getLoadedGlyphCount"] = &trussc::Font::getLoadedGlyphCount; - t["getTotalCacheMemoryUsage"] = &trussc::Font::getTotalCacheMemoryUsage; - } - { - sol::usertype t = lua->new_usertype("FullscreenShader", - sol::constructors()); - t["draw"] = &trussc::FullscreenShader::draw; - } - { - sol::usertype t = lua->new_usertype("VideoDeviceInfo"); - t["deviceId"] = &trussc::VideoDeviceInfo::deviceId; - t["deviceName"] = &trussc::VideoDeviceInfo::deviceName; - t["uniqueId"] = &trussc::VideoDeviceInfo::uniqueId; - t["getDeviceID"] = &trussc::VideoDeviceInfo::getDeviceID; - t["getDeviceName"] = &trussc::VideoDeviceInfo::getDeviceName; - t["getUniqueId"] = &trussc::VideoDeviceInfo::getUniqueId; - } - { - sol::usertype t = lua->new_usertype("GrabberFrame"); - t["pixels"] = &trussc::GrabberFrame::pixels; - t["timestampUs"] = &trussc::GrabberFrame::timestampUs; - } -#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) || defined(__EMSCRIPTEN__) - { - sol::usertype t = lua->new_usertype("VideoGrabber", - sol::constructors()); - t["listDevices"] = &trussc::VideoGrabber::listDevices; - t["setDeviceID"] = &trussc::VideoGrabber::setDeviceID; - t["getDeviceID"] = &trussc::VideoGrabber::getDeviceID; - t["setDesiredFrameRate"] = &trussc::VideoGrabber::setDesiredFrameRate; - t["getDesiredFrameRate"] = &trussc::VideoGrabber::getDesiredFrameRate; - t["setVerbose"] = &trussc::VideoGrabber::setVerbose; - t["isVerbose"] = &trussc::VideoGrabber::isVerbose; - t["setup"] = sol::overload([](trussc::VideoGrabber& self) { return self.setup(); }, [](trussc::VideoGrabber& self, int width) { return self.setup(width); }, [](trussc::VideoGrabber& self, int width, int height) { return self.setup(width, height); }); - t["close"] = &trussc::VideoGrabber::close; - t["update"] = &trussc::VideoGrabber::update; - t["isFrameNew"] = &trussc::VideoGrabber::isFrameNew; - t["isInitialized"] = &trussc::VideoGrabber::isInitialized; - t["isPendingPermission"] = &trussc::VideoGrabber::isPendingPermission; - t["getWidth"] = &trussc::VideoGrabber::getWidth; - t["getHeight"] = &trussc::VideoGrabber::getHeight; - t["getDeviceName"] = &trussc::VideoGrabber::getDeviceName; - t["getPixels"] = [](trussc::VideoGrabber& self) { return self.getPixels(); }; -#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) - t["setFrameQueueSize"] = &trussc::VideoGrabber::setFrameQueueSize; -#endif -#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) - t["getFrameQueueSize"] = &trussc::VideoGrabber::getFrameQueueSize; -#endif - t["copyToImage"] = &trussc::VideoGrabber::copyToImage; - t["getTexture"] = [](trussc::VideoGrabber& self) -> decltype(auto) { return self.getTexture(); }; - t["checkCameraPermission"] = &trussc::VideoGrabber::checkCameraPermission; - t["requestCameraPermission"] = &trussc::VideoGrabber::requestCameraPermission; - } -#endif -#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) || defined(__EMSCRIPTEN__) - { - sol::usertype t = lua->new_usertype("VideoPlayer", - sol::constructors()); - t["load"] = &trussc::VideoPlayer::load; - t["close"] = &trussc::VideoPlayer::close; - t["update"] = &trussc::VideoPlayer::update; - t["play"] = &trussc::VideoPlayer::play; - t["setAutoPoster"] = &trussc::VideoPlayer::setAutoPoster; - t["getAutoPoster"] = &trussc::VideoPlayer::getAutoPoster; - t["draw"] = sol::overload([](trussc::VideoPlayer& self, float x, float y) { return self.draw(x, y); }, [](trussc::VideoPlayer& self, float x, float y, float w, float h) { return self.draw(x, y, w, h); }); - t["getDuration"] = &trussc::VideoPlayer::getDuration; - t["getPosition"] = &trussc::VideoPlayer::getPosition; - t["getCurrentFrame"] = &trussc::VideoPlayer::getCurrentFrame; - t["getTotalFrames"] = &trussc::VideoPlayer::getTotalFrames; - t["setFrame"] = &trussc::VideoPlayer::setFrame; - t["nextFrame"] = &trussc::VideoPlayer::nextFrame; - t["previousFrame"] = &trussc::VideoPlayer::previousFrame; - t["setGammaCorrection"] = &trussc::VideoPlayer::setGammaCorrection; - t["getGammaCorrection"] = &trussc::VideoPlayer::getGammaCorrection; - t["setUseHwAccel"] = &trussc::VideoPlayer::setUseHwAccel; - t["getUseHwAccel"] = &trussc::VideoPlayer::getUseHwAccel; - t["getPixels"] = [](trussc::VideoPlayer& self) { return self.getPixels(); }; - t["getPixelsY"] = &trussc::VideoPlayer::getPixelsY; - t["getPixelsUV"] = &trussc::VideoPlayer::getPixelsUV; - t["hasAudio"] = &trussc::VideoPlayer::hasAudio; -#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) - t["getAudioCodec"] = &trussc::VideoPlayer::getAudioCodec; -#endif -#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) - t["getAudioData"] = &trussc::VideoPlayer::getAudioData; -#endif -#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) - t["getAudioSampleRate"] = &trussc::VideoPlayer::getAudioSampleRate; -#endif -#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) - t["getAudioChannels"] = &trussc::VideoPlayer::getAudioChannels; -#endif - t["isUsingHwAccel"] = &trussc::VideoPlayer::isUsingHwAccel; - t["getHwAccelName"] = &trussc::VideoPlayer::getHwAccelName; - t["getPath"] = &trussc::VideoPlayer::getPath; - } -#endif - { - sol::usertype t = lua->new_usertype("VideoRecordSettings"); - t["codec"] = &trussc::VideoRecordSettings::codec; - t["fps"] = &trussc::VideoRecordSettings::fps; - t["bitrate"] = &trussc::VideoRecordSettings::bitrate; - t["keyframeInterval"] = &trussc::VideoRecordSettings::keyframeInterval; - t["duration"] = &trussc::VideoRecordSettings::duration; - } -#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) - { - sol::usertype t = lua->new_usertype("VideoWriter", - sol::constructors()); - t["open"] = sol::overload([](trussc::VideoWriter& self, const std::string & path, int width, int height) { return self.open(path, width, height); }, [](trussc::VideoWriter& self, const std::string & path, int width, int height, const trussc::VideoRecordSettings & settings) { return self.open(path, width, height, settings); }); - t["close"] = &trussc::VideoWriter::close; - t["isOpen"] = &trussc::VideoWriter::isOpen; - t["getFrameCount"] = &trussc::VideoWriter::getFrameCount; - t["getWidth"] = &trussc::VideoWriter::getWidth; - t["getHeight"] = &trussc::VideoWriter::getHeight; - t["getFps"] = &trussc::VideoWriter::getFps; - t["getPath"] = &trussc::VideoWriter::getPath; - t["getSettings"] = &trussc::VideoWriter::getSettings; - t["addFrame"] = sol::overload([](trussc::VideoWriter& self, const trussc::Fbo & fbo) { return self.addFrame(fbo); }, [](trussc::VideoWriter& self, const trussc::Pixels & pixels) { return self.addFrame(pixels); }); - t["addFrameAt"] = sol::overload([](trussc::VideoWriter& self, const trussc::Fbo & fbo, double timeSec) { return self.addFrameAt(fbo, timeSec); }, [](trussc::VideoWriter& self, const trussc::Pixels & pixels, double timeSec) { return self.addFrameAt(pixels, timeSec); }); -#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) - t["submitFrame"] = &trussc::VideoWriter::submitFrame; -#endif - } -#endif -#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) - { - sol::usertype t = lua->new_usertype("ScreenRecorder", - sol::constructors()); - t["start"] = sol::overload([](trussc::ScreenRecorder& self, const std::string & path) { return self.start(path); }, [](trussc::ScreenRecorder& self, const std::string & path, const trussc::VideoRecordSettings & settings) { return self.start(path, settings); }, [](trussc::ScreenRecorder& self, const trussc::Fbo & fbo, const std::string & path) { return self.start(fbo, path); }, [](trussc::ScreenRecorder& self, const trussc::Fbo & fbo, const std::string & path, const trussc::VideoRecordSettings & settings) { return self.start(fbo, path, settings); }, [](trussc::ScreenRecorder& self, const std::string & path, float durationSec) { return self.start(path, durationSec); }, [](trussc::ScreenRecorder& self, const trussc::Fbo & fbo, const std::string & path, float durationSec) { return self.start(fbo, path, durationSec); }); - t["stop"] = &trussc::ScreenRecorder::stop; - t["isRecording"] = &trussc::ScreenRecorder::isRecording; - t["getFrameCount"] = &trussc::ScreenRecorder::getFrameCount; - t["getPath"] = &trussc::ScreenRecorder::getPath; - t["writer"] = &trussc::ScreenRecorder::writer; - } -#endif - { - sol::usertype t = lua->new_usertype("UdpReceiveEventArgs"); - t["data"] = &trussc::UdpReceiveEventArgs::data; - t["remoteHost"] = &trussc::UdpReceiveEventArgs::remoteHost; - t["remotePort"] = &trussc::UdpReceiveEventArgs::remotePort; - } - { - sol::usertype t = lua->new_usertype("UdpErrorEventArgs"); - t["message"] = &trussc::UdpErrorEventArgs::message; - t["errorCode"] = &trussc::UdpErrorEventArgs::errorCode; - } -#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) - { - sol::usertype t = lua->new_usertype("UdpSocket", - sol::constructors()); - t["onReceive"] = &trussc::UdpSocket::onReceive; - t["onError"] = &trussc::UdpSocket::onError; - t["create"] = &trussc::UdpSocket::create; - t["bind"] = sol::overload([](trussc::UdpSocket& self, int port) { return self.bind(port); }, [](trussc::UdpSocket& self, int port, bool startReceiving) { return self.bind(port, startReceiving); }); - t["connect"] = &trussc::UdpSocket::connect; - t["close"] = &trussc::UdpSocket::close; - t["sendTo"] = [](trussc::UdpSocket& self, const std::string & host, int port, const std::string & message) { return self.sendTo(host, port, message); }; - t["send"] = [](trussc::UdpSocket& self, const std::string & message) { return self.send(message); }; - t["startReceiving"] = &trussc::UdpSocket::startReceiving; - t["stopReceiving"] = &trussc::UdpSocket::stopReceiving; - t["isReceiving"] = &trussc::UdpSocket::isReceiving; - t["setNonBlocking"] = &trussc::UdpSocket::setNonBlocking; - t["setBroadcast"] = &trussc::UdpSocket::setBroadcast; - t["setReuseAddress"] = &trussc::UdpSocket::setReuseAddress; - t["setReusePort"] = &trussc::UdpSocket::setReusePort; - t["joinMulticastGroup"] = sol::overload([](trussc::UdpSocket& self, const std::string & groupAddr) { return self.joinMulticastGroup(groupAddr); }, [](trussc::UdpSocket& self, const std::string & groupAddr, const std::string & interfaceAddr) { return self.joinMulticastGroup(groupAddr, interfaceAddr); }); - t["leaveMulticastGroup"] = sol::overload([](trussc::UdpSocket& self, const std::string & groupAddr) { return self.leaveMulticastGroup(groupAddr); }, [](trussc::UdpSocket& self, const std::string & groupAddr, const std::string & interfaceAddr) { return self.leaveMulticastGroup(groupAddr, interfaceAddr); }); - t["setMulticastTTL"] = &trussc::UdpSocket::setMulticastTTL; - t["setMulticastLoopback"] = &trussc::UdpSocket::setMulticastLoopback; - t["setMulticastInterface"] = &trussc::UdpSocket::setMulticastInterface; - t["setReceiveBufferSize"] = &trussc::UdpSocket::setReceiveBufferSize; - t["setSendBufferSize"] = &trussc::UdpSocket::setSendBufferSize; - t["setReceiveTimeout"] = &trussc::UdpSocket::setReceiveTimeout; - t["setUseThread"] = &trussc::UdpSocket::setUseThread; - t["getLocalPort"] = &trussc::UdpSocket::getLocalPort; - t["processNetwork"] = &trussc::UdpSocket::processNetwork; - t["isValid"] = &trussc::UdpSocket::isValid; - t["getConnectedHost"] = &trussc::UdpSocket::getConnectedHost; - t["getConnectedPort"] = &trussc::UdpSocket::getConnectedPort; - } -#endif - { - sol::usertype t = lua->new_usertype("TcpConnectEventArgs"); - t["success"] = &trussc::TcpConnectEventArgs::success; - t["message"] = &trussc::TcpConnectEventArgs::message; - } - { - sol::usertype t = lua->new_usertype("TcpReceiveEventArgs"); - t["data"] = &trussc::TcpReceiveEventArgs::data; - } - { - sol::usertype t = lua->new_usertype("TcpDisconnectEventArgs"); - t["reason"] = &trussc::TcpDisconnectEventArgs::reason; - t["wasClean"] = &trussc::TcpDisconnectEventArgs::wasClean; - } - { - sol::usertype t = lua->new_usertype("TcpErrorEventArgs"); - t["message"] = &trussc::TcpErrorEventArgs::message; - t["errorCode"] = &trussc::TcpErrorEventArgs::errorCode; - } -#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) - { - sol::usertype t = lua->new_usertype("TcpClient", - sol::constructors()); - t["onConnect"] = &trussc::TcpClient::onConnect; - t["onReceive"] = &trussc::TcpClient::onReceive; - t["onDisconnect"] = &trussc::TcpClient::onDisconnect; - t["onError"] = &trussc::TcpClient::onError; - t["connect"] = &trussc::TcpClient::connect; - t["connectAsync"] = &trussc::TcpClient::connectAsync; - t["disconnect"] = &trussc::TcpClient::disconnect; - t["isConnected"] = &trussc::TcpClient::isConnected; - t["send"] = sol::overload([](trussc::TcpClient& self, const std::vector & data) { return self.send(data); }, [](trussc::TcpClient& self, const std::string & message) { return self.send(message); }); - t["setReceiveBufferSize"] = &trussc::TcpClient::setReceiveBufferSize; - t["setBlocking"] = &trussc::TcpClient::setBlocking; - t["setUseThread"] = &trussc::TcpClient::setUseThread; - t["isUsingThread"] = &trussc::TcpClient::isUsingThread; - t["processNetwork"] = &trussc::TcpClient::processNetwork; - t["getRemoteHost"] = &trussc::TcpClient::getRemoteHost; - t["getRemotePort"] = &trussc::TcpClient::getRemotePort; - } -#endif - { - sol::usertype t = lua->new_usertype("TcpServerClient"); - t["getId"] = &trussc::TcpServerClient::getId; - t["getHost"] = &trussc::TcpServerClient::getHost; - t["getPort"] = &trussc::TcpServerClient::getPort; - } - { - sol::usertype t = lua->new_usertype("TcpClientConnectEventArgs"); - t["clientId"] = &trussc::TcpClientConnectEventArgs::clientId; - t["host"] = &trussc::TcpClientConnectEventArgs::host; - t["port"] = &trussc::TcpClientConnectEventArgs::port; - } - { - sol::usertype t = lua->new_usertype("TcpServerReceiveEventArgs"); - t["clientId"] = &trussc::TcpServerReceiveEventArgs::clientId; - t["data"] = &trussc::TcpServerReceiveEventArgs::data; - } - { - sol::usertype t = lua->new_usertype("TcpClientDisconnectEventArgs"); - t["clientId"] = &trussc::TcpClientDisconnectEventArgs::clientId; - t["reason"] = &trussc::TcpClientDisconnectEventArgs::reason; - t["wasClean"] = &trussc::TcpClientDisconnectEventArgs::wasClean; - } - { - sol::usertype t = lua->new_usertype("TcpServerErrorEventArgs"); - t["message"] = &trussc::TcpServerErrorEventArgs::message; - t["errorCode"] = &trussc::TcpServerErrorEventArgs::errorCode; - t["clientId"] = &trussc::TcpServerErrorEventArgs::clientId; - } -#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) - { - sol::usertype t = lua->new_usertype("TcpServer", - sol::constructors()); - t["onClientConnect"] = &trussc::TcpServer::onClientConnect; - t["onReceive"] = &trussc::TcpServer::onReceive; - t["onClientDisconnect"] = &trussc::TcpServer::onClientDisconnect; - t["onError"] = &trussc::TcpServer::onError; - t["start"] = sol::overload([](trussc::TcpServer& self, int port) { return self.start(port); }, [](trussc::TcpServer& self, int port, int maxClients) { return self.start(port, maxClients); }); - t["stop"] = &trussc::TcpServer::stop; - t["isRunning"] = &trussc::TcpServer::isRunning; - t["disconnectClient"] = &trussc::TcpServer::disconnectClient; - t["disconnectAllClients"] = &trussc::TcpServer::disconnectAllClients; - t["getClientCount"] = &trussc::TcpServer::getClientCount; - t["getClientIds"] = &trussc::TcpServer::getClientIds; - t["getClient"] = &trussc::TcpServer::getClient; - t["send"] = sol::overload([](trussc::TcpServer& self, int clientId, const std::vector & data) { return self.send(clientId, data); }, [](trussc::TcpServer& self, int clientId, const std::string & message) { return self.send(clientId, message); }); - t["broadcast"] = sol::overload([](trussc::TcpServer& self, const std::vector & data) { return self.broadcast(data); }, [](trussc::TcpServer& self, const std::string & message) { return self.broadcast(message); }); - t["setReceiveBufferSize"] = &trussc::TcpServer::setReceiveBufferSize; - t["getPort"] = &trussc::TcpServer::getPort; - } -#endif - { - sol::usertype t = lua->new_usertype("NetworkInterface"); - t["name"] = &trussc::NetworkInterface::name; - t["address"] = &trussc::NetworkInterface::address; - t["netmask"] = &trussc::NetworkInterface::netmask; - t["mac"] = &trussc::NetworkInterface::mac; - t["isIPv4"] = &trussc::NetworkInterface::isIPv4; - t["isLoopback"] = &trussc::NetworkInterface::isLoopback; - t["isUp"] = &trussc::NetworkInterface::isUp; - t["getName"] = &trussc::NetworkInterface::getName; - t["getAddress"] = &trussc::NetworkInterface::getAddress; - t["getNetmask"] = &trussc::NetworkInterface::getNetmask; - t["getMac"] = &trussc::NetworkInterface::getMac; - t["getIsIPv4"] = &trussc::NetworkInterface::getIsIPv4; - t["getIsLoopback"] = &trussc::NetworkInterface::getIsLoopback; - t["getIsUp"] = &trussc::NetworkInterface::getIsUp; - } - { - sol::usertype t = lua->new_usertype("SerialDeviceInfo"); - t["deviceId"] = &trussc::SerialDeviceInfo::deviceId; - t["devicePath"] = &trussc::SerialDeviceInfo::devicePath; - t["deviceName"] = &trussc::SerialDeviceInfo::deviceName; - t["getDeviceID"] = &trussc::SerialDeviceInfo::getDeviceID; - t["getDevicePath"] = &trussc::SerialDeviceInfo::getDevicePath; - t["getDeviceName"] = &trussc::SerialDeviceInfo::getDeviceName; - } -#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) - { - sol::usertype t = lua->new_usertype("Serial", - sol::constructors()); - t["getDeviceList"] = &trussc::Serial::getDeviceList; - t["setup"] = sol::overload([](trussc::Serial& self, const std::string & portName, int baudRate) { return self.setup(portName, baudRate); }, [](trussc::Serial& self, int deviceIndex, int baudRate) { return self.setup(deviceIndex, baudRate); }); - t["close"] = &trussc::Serial::close; - t["isInitialized"] = &trussc::Serial::isInitialized; - t["getDevicePath"] = &trussc::Serial::getDevicePath; - t["available"] = &trussc::Serial::available; - t["readByte"] = &trussc::Serial::readByte; - t["writeBytes"] = [](trussc::Serial& self, const std::string & buffer) { return self.writeBytes(buffer); }; - t["writeByte"] = &trussc::Serial::writeByte; - t["flushInput"] = &trussc::Serial::flushInput; - t["flushOutput"] = &trussc::Serial::flushOutput; - t["flush"] = &trussc::Serial::flush; - t["drain"] = &trussc::Serial::drain; - t["printDevices"] = &trussc::Serial::printDevices; - t["listDevices"] = &trussc::Serial::listDevices; - } -#endif - { - sol::usertype t = lua->new_usertype("ChipSoundNote", - sol::constructors()); - t["wave"] = &trussc::ChipSoundNote::wave; - t["hz"] = &trussc::ChipSoundNote::hz; - t["volume"] = &trussc::ChipSoundNote::volume; - t["duration"] = &trussc::ChipSoundNote::duration; - t["attack"] = &trussc::ChipSoundNote::attack; - t["decay"] = &trussc::ChipSoundNote::decay; - t["sustain"] = &trussc::ChipSoundNote::sustain; - t["release"] = &trussc::ChipSoundNote::release; - t["build"] = &trussc::ChipSoundNote::build; - t["generateBuffer"] = &trussc::ChipSoundNote::generateBuffer; - t["getTotalDuration"] = &trussc::ChipSoundNote::getTotalDuration; - } - { - sol::usertype t = lua->new_usertype("ChipSoundBundle"); - t["entries"] = &trussc::ChipSoundBundle::entries; - t["volume"] = &trussc::ChipSoundBundle::volume; - t["add"] = sol::overload([](trussc::ChipSoundBundle& self, const trussc::ChipSoundNote & note, float time) -> decltype(auto) { return self.add(note, time); }, [](trussc::ChipSoundBundle& self, trussc::ChipSoundNote::Wave wave, float hz, float duration, float time) -> decltype(auto) { return self.add(wave, hz, duration, time); }, [](trussc::ChipSoundBundle& self, trussc::ChipSoundNote::Wave wave, float hz, float duration, float time, float vol) -> decltype(auto) { return self.add(wave, hz, duration, time, vol); }); - t["clear"] = &trussc::ChipSoundBundle::clear; - t["getDuration"] = &trussc::ChipSoundBundle::getDuration; - t["build"] = &trussc::ChipSoundBundle::build; - } - { - sol::usertype> t = lua->new_usertype>("Tween_float", - sol::constructors(), trussc::Tween(float, float, float), trussc::Tween(float, float, float, trussc::EaseType), trussc::Tween(float, float, float, trussc::EaseType, trussc::EaseMode)>()); - } - { - sol::usertype> t = lua->new_usertype>("Tween_Vec2", - sol::constructors(), trussc::Tween(trussc::Vec2, trussc::Vec2, float), trussc::Tween(trussc::Vec2, trussc::Vec2, float, trussc::EaseType), trussc::Tween(trussc::Vec2, trussc::Vec2, float, trussc::EaseType, trussc::EaseMode)>()); - } - { - sol::usertype> t = lua->new_usertype>("Tween_Vec3", - sol::constructors(), trussc::Tween(trussc::Vec3, trussc::Vec3, float), trussc::Tween(trussc::Vec3, trussc::Vec3, float, trussc::EaseType), trussc::Tween(trussc::Vec3, trussc::Vec3, float, trussc::EaseType, trussc::EaseMode)>()); - } - { - sol::usertype t = lua->new_usertype("Mod"); - t["getOwner"] = [](trussc::Mod& self) { return self.getOwner(); }; - } - { - sol::usertype t = lua->new_usertype("Node", - sol::constructors()); - t["localMatrixChanged"] = &trussc::Node::localMatrixChanged; - t["setup"] = &trussc::Node::setup; - t["update"] = &trussc::Node::update; - t["draw"] = &trussc::Node::draw; - t["cleanup"] = &trussc::Node::cleanup; - t["addChild"] = sol::overload([](trussc::Node& self, trussc::Node::Ptr child) { return self.addChild(child); }, [](trussc::Node& self, trussc::Node::Ptr child, bool keepGlobalPosition) { return self.addChild(child, keepGlobalPosition); }); - t["insertChild"] = sol::overload([](trussc::Node& self, size_t index, trussc::Node::Ptr child) { return self.insertChild(index, child); }, [](trussc::Node& self, size_t index, trussc::Node::Ptr child, bool keepGlobalPosition) { return self.insertChild(index, child, keepGlobalPosition); }); - t["removeChild"] = &trussc::Node::removeChild; - t["removeAllChildren"] = &trussc::Node::removeAllChildren; - t["onChildAdded"] = &trussc::Node::onChildAdded; - t["onChildRemoved"] = &trussc::Node::onChildRemoved; - t["getParent"] = &trussc::Node::getParent; - t["getChildren"] = &trussc::Node::getChildren; - t["getChildCount"] = &trussc::Node::getChildCount; - t["getChildIndex"] = &trussc::Node::getChildIndex; - t["moveToFront"] = &trussc::Node::moveToFront; - t["moveToBack"] = &trussc::Node::moveToBack; - t["isActive"] = &trussc::Node::isActive; - t["setActive"] = &trussc::Node::setActive; - t["isVisible"] = &trussc::Node::isVisible; - t["setVisible"] = &trussc::Node::setVisible; - t["getActive"] = &trussc::Node::getActive; - t["getVisible"] = &trussc::Node::getVisible; - t["setIsActive"] = &trussc::Node::setIsActive; - t["setIsVisible"] = &trussc::Node::setIsVisible; - t["destroy"] = &trussc::Node::destroy; - t["isDead"] = &trussc::Node::isDead; - t["enableEvents"] = &trussc::Node::enableEvents; - t["disableEvents"] = &trussc::Node::disableEvents; - t["isEventsEnabled"] = &trussc::Node::isEventsEnabled; - t["isMouseOver"] = &trussc::Node::isMouseOver; - t["setName"] = &trussc::Node::setName; - t["getName"] = &trussc::Node::getName; - t["hasName"] = &trussc::Node::hasName; - t["getTypeName"] = &trussc::Node::getTypeName; - t["getDisplayName"] = &trussc::Node::getDisplayName; - t["getInstanceId"] = &trussc::Node::getInstanceId; - t["findByInstanceId"] = &trussc::Node::findByInstanceId; - t["getPos"] = &trussc::Node::getPos; - t["getX"] = &trussc::Node::getX; - t["getY"] = &trussc::Node::getY; - t["getZ"] = &trussc::Node::getZ; - t["setPos"] = sol::overload([](trussc::Node& self, const trussc::Vec3 & pos) { return self.setPos(pos); }, [](trussc::Node& self, float x, float y) { return self.setPos(x, y); }, [](trussc::Node& self, float x, float y, float z) { return self.setPos(x, y, z); }); - t["setX"] = &trussc::Node::setX; - t["setY"] = &trussc::Node::setY; - t["setZ"] = &trussc::Node::setZ; - t["getQuaternion"] = &trussc::Node::getQuaternion; - t["setQuaternion"] = &trussc::Node::setQuaternion; - t["getEuler"] = &trussc::Node::getEuler; - t["setEuler"] = sol::overload([](trussc::Node& self, const trussc::Vec3 & euler) { return self.setEuler(euler); }, [](trussc::Node& self, float pitch, float yaw, float roll) { return self.setEuler(pitch, yaw, roll); }); - t["getEulerDeg"] = &trussc::Node::getEulerDeg; - t["setEulerDeg"] = &trussc::Node::setEulerDeg; - t["getRot"] = &trussc::Node::getRot; - t["setRot"] = &trussc::Node::setRot; - t["getRotDeg"] = &trussc::Node::getRotDeg; - t["setRotDeg"] = &trussc::Node::setRotDeg; - t["getScale"] = &trussc::Node::getScale; - t["getScaleX"] = &trussc::Node::getScaleX; - t["getScaleY"] = &trussc::Node::getScaleY; - t["getScaleZ"] = &trussc::Node::getScaleZ; - t["setScale"] = sol::overload([](trussc::Node& self, const trussc::Vec3 & s) { return self.setScale(s); }, [](trussc::Node& self, float uniform) { return self.setScale(uniform); }, [](trussc::Node& self, float sx, float sy) { return self.setScale(sx, sy); }, [](trussc::Node& self, float sx, float sy, float sz) { return self.setScale(sx, sy, sz); }); - t["setScaleX"] = &trussc::Node::setScaleX; - t["setScaleY"] = &trussc::Node::setScaleY; - t["setScaleZ"] = &trussc::Node::setScaleZ; - t["getLocalMatrix"] = &trussc::Node::getLocalMatrix; - t["getGlobalMatrix"] = &trussc::Node::getGlobalMatrix; - t["getGlobalMatrixInverse"] = &trussc::Node::getGlobalMatrixInverse; - t["globalToLocal"] = [](trussc::Node& self, const trussc::Vec3 & global) { return self.globalToLocal(global); }; - t["getGlobalPos"] = &trussc::Node::getGlobalPos; - t["setGlobalPos"] = sol::overload([](trussc::Node& self, const trussc::Vec3 & global) { return self.setGlobalPos(global); }, [](trussc::Node& self, float x, float y) { return self.setGlobalPos(x, y); }, [](trussc::Node& self, float x, float y, float z) { return self.setGlobalPos(x, y, z); }); - t["localToGlobal"] = [](trussc::Node& self, const trussc::Vec3 & local) { return self.localToGlobal(local); }; - t["getMouseX"] = &trussc::Node::getMouseX; - t["getMouseY"] = &trussc::Node::getMouseY; - t["getPMouseX"] = &trussc::Node::getPMouseX; - t["getPMouseY"] = &trussc::Node::getPMouseY; - t["findHitNode"] = &trussc::Node::findHitNode; - t["findHitNodeFromScreen"] = &trussc::Node::findHitNodeFromScreen; - t["getCameraContext"] = &trussc::Node::getCameraContext; - t["setCameraContext"] = &trussc::Node::setCameraContext; - t["getModTypeNames"] = &trussc::Node::getModTypeNames; - t["getMods"] = &trussc::Node::getMods; - t["getModByTypeName"] = &trussc::Node::getModByTypeName; - t["callAfter"] = &trussc::Node::callAfter; - t["callEvery"] = &trussc::Node::callEvery; - t["cancelTimer"] = &trussc::Node::cancelTimer; - t["cancelAllTimers"] = &trussc::Node::cancelAllTimers; -#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) - t["callAfterAsync"] = &trussc::Node::callAfterAsync; -#endif -#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) - t["callEveryAsync"] = &trussc::Node::callEveryAsync; -#endif -#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) - t["cancelAsyncTimer"] = &trussc::Node::cancelAsyncTimer; -#endif -#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) - t["cancelAllAsyncTimers"] = &trussc::Node::cancelAllAsyncTimers; -#endif - } - { - sol::usertype t = lua->new_usertype("RectNode"); - t["mousePressed"] = &trussc::RectNode::mousePressed; - t["mouseReleased"] = &trussc::RectNode::mouseReleased; - t["mouseDragged"] = &trussc::RectNode::mouseDragged; - t["mouseScrolled"] = &trussc::RectNode::mouseScrolled; - t["getWidth"] = &trussc::RectNode::getWidth; - t["getHeight"] = &trussc::RectNode::getHeight; - t["getSize"] = &trussc::RectNode::getSize; - t["setWidth"] = &trussc::RectNode::setWidth; - t["setHeight"] = &trussc::RectNode::setHeight; - t["setSize"] = sol::overload([](trussc::RectNode& self, float w, float h) { return self.setSize(w, h); }, [](trussc::RectNode& self, float size) { return self.setSize(size); }, [](trussc::RectNode& self, const trussc::Vec2 & s) { return self.setSize(s); }); - t["setRect"] = &trussc::RectNode::setRect; - t["setClipping"] = &trussc::RectNode::setClipping; - t["isClipping"] = &trussc::RectNode::isClipping; - t["getLeft"] = &trussc::RectNode::getLeft; - t["getRight"] = &trussc::RectNode::getRight; - t["getTop"] = &trussc::RectNode::getTop; - t["getBottom"] = &trussc::RectNode::getBottom; - t["hitTest"] = [](trussc::RectNode& self, trussc::Vec2 local) { return self.hitTest(local); }; - t["draw"] = &trussc::RectNode::draw; - } - { - sol::usertype t = lua->new_usertype("RectNodeButton", - sol::constructors()); - t["normalColor"] = &trussc::RectNodeButton::normalColor; - t["hoverColor"] = &trussc::RectNodeButton::hoverColor; - t["pressColor"] = &trussc::RectNodeButton::pressColor; - t["label"] = &trussc::RectNodeButton::label; - t["isPressed"] = &trussc::RectNodeButton::isPressed; - t["draw"] = &trussc::RectNodeButton::draw; - } - { - sol::usertype t = lua->new_usertype("LayoutMod", - sol::constructors()); - t["getDirection"] = &trussc::LayoutMod::getDirection; - t["setDirection"] = &trussc::LayoutMod::setDirection; - t["getSpacing"] = &trussc::LayoutMod::getSpacing; - t["setSpacing"] = &trussc::LayoutMod::setSpacing; - t["getCrossAxis"] = &trussc::LayoutMod::getCrossAxis; - t["setCrossAxis"] = &trussc::LayoutMod::setCrossAxis; - t["getMainAxis"] = &trussc::LayoutMod::getMainAxis; - t["setMainAxis"] = &trussc::LayoutMod::setMainAxis; - t["getPaddingLeft"] = &trussc::LayoutMod::getPaddingLeft; - t["getPaddingTop"] = &trussc::LayoutMod::getPaddingTop; - t["getPaddingRight"] = &trussc::LayoutMod::getPaddingRight; - t["getPaddingBottom"] = &trussc::LayoutMod::getPaddingBottom; - t["setPadding"] = sol::overload([](trussc::LayoutMod& self, float padding) -> decltype(auto) { return self.setPadding(padding); }, [](trussc::LayoutMod& self, float vertical, float horizontal) -> decltype(auto) { return self.setPadding(vertical, horizontal); }, [](trussc::LayoutMod& self, float top, float right, float bottom, float left) -> decltype(auto) { return self.setPadding(top, right, bottom, left); }); - t["setPaddingLeft"] = &trussc::LayoutMod::setPaddingLeft; - t["setPaddingTop"] = &trussc::LayoutMod::setPaddingTop; - t["setPaddingRight"] = &trussc::LayoutMod::setPaddingRight; - t["setPaddingBottom"] = &trussc::LayoutMod::setPaddingBottom; - t["updateLayout"] = &trussc::LayoutMod::updateLayout; - } - { - sol::usertype t = lua->new_usertype("ScrollContainer", - sol::constructors()); - t["setContent"] = &trussc::ScrollContainer::setContent; - t["getContent"] = &trussc::ScrollContainer::getContent; - t["getContentRect"] = &trussc::ScrollContainer::getContentRect; - t["getScrollX"] = &trussc::ScrollContainer::getScrollX; - t["getScrollY"] = &trussc::ScrollContainer::getScrollY; - t["getScroll"] = &trussc::ScrollContainer::getScroll; - t["setScrollX"] = &trussc::ScrollContainer::setScrollX; - t["setScrollY"] = &trussc::ScrollContainer::setScrollY; - t["setScroll"] = sol::overload([](trussc::ScrollContainer& self, float x, float y) { return self.setScroll(x, y); }, [](trussc::ScrollContainer& self, trussc::Vec2 pos) { return self.setScroll(pos); }); - t["getMaxScrollX"] = &trussc::ScrollContainer::getMaxScrollX; - t["getMaxScrollY"] = &trussc::ScrollContainer::getMaxScrollY; - t["updateScrollBounds"] = &trussc::ScrollContainer::updateScrollBounds; - t["isHorizontalScrollEnabled"] = &trussc::ScrollContainer::isHorizontalScrollEnabled; - t["isVerticalScrollEnabled"] = &trussc::ScrollContainer::isVerticalScrollEnabled; - t["setHorizontalScrollEnabled"] = &trussc::ScrollContainer::setHorizontalScrollEnabled; - t["setVerticalScrollEnabled"] = &trussc::ScrollContainer::setVerticalScrollEnabled; - t["getScrollSpeed"] = &trussc::ScrollContainer::getScrollSpeed; - t["setScrollSpeed"] = &trussc::ScrollContainer::setScrollSpeed; - } - { - sol::usertype t = lua->new_usertype("ScrollBar"); - t["getBarColor"] = &trussc::ScrollBar::getBarColor; - t["setBarColor"] = &trussc::ScrollBar::setBarColor; - t["getBarWidth"] = &trussc::ScrollBar::getBarWidth; - t["setBarWidth"] = &trussc::ScrollBar::setBarWidth; - t["getMargin"] = &trussc::ScrollBar::getMargin; - t["setMargin"] = &trussc::ScrollBar::setMargin; - t["getOffset"] = &trussc::ScrollBar::getOffset; - t["updateFromContainer"] = &trussc::ScrollBar::updateFromContainer; - } - { - sol::usertype t = lua->new_usertype("TweenMod", - sol::constructors()); - t["complete"] = &trussc::TweenMod::complete; - t["moveTo"] = sol::overload([](trussc::TweenMod& self, float x, float y) -> decltype(auto) { return self.moveTo(x, y); }, [](trussc::TweenMod& self, float x, float y, float z) -> decltype(auto) { return self.moveTo(x, y, z); }, [](trussc::TweenMod& self, const trussc::Vec3 & pos) -> decltype(auto) { return self.moveTo(pos); }, [](trussc::TweenMod& self, const trussc::Vec2 & pos) -> decltype(auto) { return self.moveTo(pos); }); - t["moveBy"] = sol::overload([](trussc::TweenMod& self, float dx, float dy) -> decltype(auto) { return self.moveBy(dx, dy); }, [](trussc::TweenMod& self, float dx, float dy, float dz) -> decltype(auto) { return self.moveBy(dx, dy, dz); }, [](trussc::TweenMod& self, const trussc::Vec3 & delta) -> decltype(auto) { return self.moveBy(delta); }, [](trussc::TweenMod& self, const trussc::Vec2 & delta) -> decltype(auto) { return self.moveBy(delta); }); - t["moveFrom"] = sol::overload([](trussc::TweenMod& self, float x, float y) -> decltype(auto) { return self.moveFrom(x, y); }, [](trussc::TweenMod& self, float x, float y, float z) -> decltype(auto) { return self.moveFrom(x, y, z); }, [](trussc::TweenMod& self, const trussc::Vec3 & pos) -> decltype(auto) { return self.moveFrom(pos); }); - t["scaleTo"] = sol::overload([](trussc::TweenMod& self, float uniform) -> decltype(auto) { return self.scaleTo(uniform); }, [](trussc::TweenMod& self, float sx, float sy) -> decltype(auto) { return self.scaleTo(sx, sy); }, [](trussc::TweenMod& self, float sx, float sy, float sz) -> decltype(auto) { return self.scaleTo(sx, sy, sz); }, [](trussc::TweenMod& self, const trussc::Vec3 & s) -> decltype(auto) { return self.scaleTo(s); }); - t["scaleBy"] = sol::overload([](trussc::TweenMod& self, float factor) -> decltype(auto) { return self.scaleBy(factor); }, [](trussc::TweenMod& self, float sx, float sy) -> decltype(auto) { return self.scaleBy(sx, sy); }, [](trussc::TweenMod& self, float sx, float sy, float sz) -> decltype(auto) { return self.scaleBy(sx, sy, sz); }); - t["scaleFrom"] = sol::overload([](trussc::TweenMod& self, float uniform) -> decltype(auto) { return self.scaleFrom(uniform); }, [](trussc::TweenMod& self, float sx, float sy) -> decltype(auto) { return self.scaleFrom(sx, sy); }, [](trussc::TweenMod& self, float sx, float sy, float sz) -> decltype(auto) { return self.scaleFrom(sx, sy, sz); }); - t["rotateTo"] = sol::overload([](trussc::TweenMod& self, float radians) -> decltype(auto) { return self.rotateTo(radians); }, [](trussc::TweenMod& self, const trussc::Quaternion & q) -> decltype(auto) { return self.rotateTo(q); }); - t["rotateBy"] = &trussc::TweenMod::rotateBy; - t["rotateFrom"] = sol::overload([](trussc::TweenMod& self, float radians) -> decltype(auto) { return self.rotateFrom(radians); }, [](trussc::TweenMod& self, const trussc::Quaternion & q) -> decltype(auto) { return self.rotateFrom(q); }); - t["rotateXTo"] = &trussc::TweenMod::rotateXTo; - t["rotateXBy"] = &trussc::TweenMod::rotateXBy; - t["rotateYTo"] = &trussc::TweenMod::rotateYTo; - t["rotateYBy"] = &trussc::TweenMod::rotateYBy; - t["rotateZTo"] = &trussc::TweenMod::rotateZTo; - t["rotateZBy"] = &trussc::TweenMod::rotateZBy; - t["rotateXFrom"] = &trussc::TweenMod::rotateXFrom; - t["rotateYFrom"] = &trussc::TweenMod::rotateYFrom; - t["duration"] = &trussc::TweenMod::duration; - t["ease"] = sol::overload([](trussc::TweenMod& self, trussc::EaseType type) -> decltype(auto) { return self.ease(type); }, [](trussc::TweenMod& self, trussc::EaseType type, trussc::EaseMode mode) -> decltype(auto) { return self.ease(type, mode); }); - t["delay"] = &trussc::TweenMod::delay; - t["start"] = &trussc::TweenMod::start; - t["pause"] = &trussc::TweenMod::pause; - t["resume"] = &trussc::TweenMod::resume; - t["reset"] = &trussc::TweenMod::reset; - t["isPlaying"] = &trussc::TweenMod::isPlaying; - t["isComplete"] = &trussc::TweenMod::isComplete; - t["getProgress"] = &trussc::TweenMod::getProgress; - t["getDuration"] = &trussc::TweenMod::getDuration; - t["getDelay"] = &trussc::TweenMod::getDelay; - t["getEaseType"] = &trussc::TweenMod::getEaseType; - t["getEaseMode"] = &trussc::TweenMod::getEaseMode; - } - { - sol::usertype t = lua->new_usertype("App", - sol::constructors()); - t["setSize"] = &trussc::App::setSize; - t["requestExit"] = &trussc::App::requestExit; - t["isExitRequested"] = &trussc::App::isExitRequested; - t["keyPressed"] = sol::overload([](trussc::App& self, const trussc::KeyEventArgs & e) { return self.keyPressed(e); }, [](trussc::App& self, int key) { return self.keyPressed(key); }); - t["keyReleased"] = sol::overload([](trussc::App& self, const trussc::KeyEventArgs & e) { return self.keyReleased(e); }, [](trussc::App& self, int key) { return self.keyReleased(key); }); - t["mousePressed"] = sol::overload([](trussc::App& self, const trussc::MouseEventArgs & e) { return self.mousePressed(e); }, [](trussc::App& self, trussc::Vec2 pos, int button) { return self.mousePressed(pos, button); }); - t["mouseReleased"] = sol::overload([](trussc::App& self, const trussc::MouseEventArgs & e) { return self.mouseReleased(e); }, [](trussc::App& self, trussc::Vec2 pos, int button) { return self.mouseReleased(pos, button); }); - t["mouseMoved"] = sol::overload([](trussc::App& self, const trussc::MouseMoveEventArgs & e) { return self.mouseMoved(e); }, [](trussc::App& self, trussc::Vec2 pos) { return self.mouseMoved(pos); }); - t["mouseDragged"] = sol::overload([](trussc::App& self, const trussc::MouseDragEventArgs & e) { return self.mouseDragged(e); }, [](trussc::App& self, trussc::Vec2 pos, int button) { return self.mouseDragged(pos, button); }); - t["mouseScrolled"] = sol::overload([](trussc::App& self, const trussc::ScrollEventArgs & e) { return self.mouseScrolled(e); }, [](trussc::App& self, trussc::Vec2 delta) { return self.mouseScrolled(delta); }); - t["touchPressed"] = &trussc::App::touchPressed; - t["touchMoved"] = &trussc::App::touchMoved; - t["touchReleased"] = &trussc::App::touchReleased; - t["windowResized"] = &trussc::App::windowResized; - t["filesDropped"] = &trussc::App::filesDropped; - t["exit"] = &trussc::App::exit; - t["audioOut"] = &trussc::App::audioOut; - t["audioIn"] = &trussc::App::audioIn; - t["handleKeyPressed"] = &trussc::App::handleKeyPressed; - t["handleKeyReleased"] = &trussc::App::handleKeyReleased; - t["handleMousePressed"] = &trussc::App::handleMousePressed; - t["handleMouseReleased"] = &trussc::App::handleMouseReleased; - t["handleMouseScrolled"] = &trussc::App::handleMouseScrolled; - t["handleWindowResized"] = &trussc::App::handleWindowResized; - t["handleUpdate"] = &trussc::App::handleUpdate; - t["handleDraw"] = &trussc::App::handleDraw; - } - { - sol::usertype t = lua->new_usertype("HeadlessSettings"); - t["targetFps"] = &trussc::HeadlessSettings::targetFps; - t["setFps"] = &trussc::HeadlessSettings::setFps; - } - lua->new_usertype("Direction", - sol::meta_function::equal_to, [](trussc::Direction a, trussc::Direction b){ return a == b; }, - "Left", sol::var(trussc::Direction::Left), - "Center", sol::var(trussc::Direction::Center), - "Right", sol::var(trussc::Direction::Right), - "Top", sol::var(trussc::Direction::Top), - "Bottom", sol::var(trussc::Direction::Bottom), - "Baseline", sol::var(trussc::Direction::Baseline)); - lua->new_usertype("Deliver", - sol::meta_function::equal_to, [](trussc::Deliver a, trussc::Deliver b){ return a == b; }, - "Inline", sol::var(trussc::Deliver::Inline), - "Main", sol::var(trussc::Deliver::Main)); - lua->new_usertype("LogLevel", - sol::meta_function::equal_to, [](trussc::LogLevel a, trussc::LogLevel b){ return a == b; }, - "Verbose", sol::var(trussc::LogLevel::Verbose), - "Notice", sol::var(trussc::LogLevel::Notice), - "Warning", sol::var(trussc::LogLevel::Warning), - "Error", sol::var(trussc::LogLevel::Error), - "Fatal", sol::var(trussc::LogLevel::Fatal), - "Silent", sol::var(trussc::LogLevel::Silent)); - lua->new_usertype("WindowType", - sol::meta_function::equal_to, [](trussc::WindowType a, trussc::WindowType b){ return a == b; }, - "Rect", sol::var(trussc::WindowType::Rect), - "Hanning", sol::var(trussc::WindowType::Hanning), - "Hamming", sol::var(trussc::WindowType::Hamming), - "Blackman", sol::var(trussc::WindowType::Blackman)); - lua->new_usertype("ThermalState", - sol::meta_function::equal_to, [](trussc::ThermalState a, trussc::ThermalState b){ return a == b; }, - "Nominal", sol::var(trussc::ThermalState::Nominal), - "Fair", sol::var(trussc::ThermalState::Fair), - "Serious", sol::var(trussc::ThermalState::Serious), - "Critical", sol::var(trussc::ThermalState::Critical)); - lua->new_usertype("MouseButton", - sol::meta_function::equal_to, [](trussc::MouseButton a, trussc::MouseButton b){ return a == b; }, - "Left", sol::var(trussc::MouseButton::Left), - "Right", sol::var(trussc::MouseButton::Right), - "Middle", sol::var(trussc::MouseButton::Middle), - "None", sol::var(trussc::MouseButton::None)); - lua->new_usertype("MixMode", - sol::meta_function::equal_to, [](trussc::MixMode a, trussc::MixMode b){ return a == b; }, - "Auto", sol::var(trussc::MixMode::Auto), - "DownmixMono", sol::var(trussc::MixMode::DownmixMono)); - lua->new_usertype("Beep", - sol::meta_function::equal_to, [](trussc::Beep a, trussc::Beep b){ return a == b; }, - "ping", sol::var(trussc::Beep::ping), - "success", sol::var(trussc::Beep::success), - "complete", sol::var(trussc::Beep::complete), - "coin", sol::var(trussc::Beep::coin), - "error", sol::var(trussc::Beep::error), - "warning", sol::var(trussc::Beep::warning), - "cancel", sol::var(trussc::Beep::cancel), - "click", sol::var(trussc::Beep::click), - "typing", sol::var(trussc::Beep::typing), - "notify", sol::var(trussc::Beep::notify), - "sweep", sol::var(trussc::Beep::sweep)); - lua->new_usertype("Codec", - sol::meta_function::equal_to, [](trussc::Codec a, trussc::Codec b){ return a == b; }, - "None", sol::var(trussc::Codec::None), - "LZ4", sol::var(trussc::Codec::LZ4)); - lua->new_usertype("BlendMode", - sol::meta_function::equal_to, [](trussc::BlendMode a, trussc::BlendMode b){ return a == b; }, - "Alpha", sol::var(trussc::BlendMode::Alpha), - "Add", sol::var(trussc::BlendMode::Add), - "Multiply", sol::var(trussc::BlendMode::Multiply), - "Screen", sol::var(trussc::BlendMode::Screen), - "Subtract", sol::var(trussc::BlendMode::Subtract), - "Disabled", sol::var(trussc::BlendMode::Disabled)); - lua->new_usertype("TextureFilter", - sol::meta_function::equal_to, [](trussc::TextureFilter a, trussc::TextureFilter b){ return a == b; }, - "Nearest", sol::var(trussc::TextureFilter::Nearest), - "Linear", sol::var(trussc::TextureFilter::Linear)); - lua->new_usertype("TextureWrap", - sol::meta_function::equal_to, [](trussc::TextureWrap a, trussc::TextureWrap b){ return a == b; }, - "Repeat", sol::var(trussc::TextureWrap::Repeat), - "ClampToEdge", sol::var(trussc::TextureWrap::ClampToEdge), - "MirroredRepeat", sol::var(trussc::TextureWrap::MirroredRepeat)); - lua->new_usertype("PrimitiveType", - sol::meta_function::equal_to, [](trussc::PrimitiveType a, trussc::PrimitiveType b){ return a == b; }, - "Points", sol::var(trussc::PrimitiveType::Points), - "Lines", sol::var(trussc::PrimitiveType::Lines), - "LineStrip", sol::var(trussc::PrimitiveType::LineStrip), - "Triangles", sol::var(trussc::PrimitiveType::Triangles), - "TriangleStrip", sol::var(trussc::PrimitiveType::TriangleStrip), - "Quads", sol::var(trussc::PrimitiveType::Quads)); - lua->new_usertype("StrokeCap", - sol::meta_function::equal_to, [](trussc::StrokeCap a, trussc::StrokeCap b){ return a == b; }, - "Butt", sol::var(trussc::StrokeCap::Butt), - "Round", sol::var(trussc::StrokeCap::Round), - "Square", sol::var(trussc::StrokeCap::Square)); - lua->new_usertype("StrokeJoin", - sol::meta_function::equal_to, [](trussc::StrokeJoin a, trussc::StrokeJoin b){ return a == b; }, - "Miter", sol::var(trussc::StrokeJoin::Miter), - "Round", sol::var(trussc::StrokeJoin::Round), - "Bevel", sol::var(trussc::StrokeJoin::Bevel)); - lua->new_usertype("PointStyle", - sol::meta_function::equal_to, [](trussc::PointStyle a, trussc::PointStyle b){ return a == b; }, - "Square", sol::var(trussc::PointStyle::Square), - "Round", sol::var(trussc::PointStyle::Round), - "Pixel", sol::var(trussc::PointStyle::Pixel)); - lua->new_usertype("Cursor", - sol::meta_function::equal_to, [](trussc::Cursor a, trussc::Cursor b){ return a == b; }, - "Default", sol::var(trussc::Cursor::Default), - "Arrow", sol::var(trussc::Cursor::Arrow), - "IBeam", sol::var(trussc::Cursor::IBeam), - "Crosshair", sol::var(trussc::Cursor::Crosshair), - "Hand", sol::var(trussc::Cursor::Hand), - "ResizeEW", sol::var(trussc::Cursor::ResizeEW), - "ResizeNS", sol::var(trussc::Cursor::ResizeNS), - "ResizeNWSE", sol::var(trussc::Cursor::ResizeNWSE), - "ResizeNESW", sol::var(trussc::Cursor::ResizeNESW), - "ResizeAll", sol::var(trussc::Cursor::ResizeAll), - "NotAllowed", sol::var(trussc::Cursor::NotAllowed), - "Custom0", sol::var(trussc::Cursor::Custom0), - "Custom1", sol::var(trussc::Cursor::Custom1), - "Custom2", sol::var(trussc::Cursor::Custom2), - "Custom3", sol::var(trussc::Cursor::Custom3), - "Custom4", sol::var(trussc::Cursor::Custom4), - "Custom5", sol::var(trussc::Cursor::Custom5), - "Custom6", sol::var(trussc::Cursor::Custom6), - "Custom7", sol::var(trussc::Cursor::Custom7), - "Custom8", sol::var(trussc::Cursor::Custom8), - "Custom9", sol::var(trussc::Cursor::Custom9), - "Custom10", sol::var(trussc::Cursor::Custom10), - "Custom11", sol::var(trussc::Cursor::Custom11), - "Custom12", sol::var(trussc::Cursor::Custom12), - "Custom13", sol::var(trussc::Cursor::Custom13), - "Custom14", sol::var(trussc::Cursor::Custom14), - "Custom15", sol::var(trussc::Cursor::Custom15)); - lua->new_usertype("Orientation", - sol::meta_function::equal_to, [](trussc::Orientation a, trussc::Orientation b){ return a == b; }, - "Portrait", sol::var(trussc::Orientation::Portrait), - "PortraitUpsideDown", sol::var(trussc::Orientation::PortraitUpsideDown), - "LandscapeLeft", sol::var(trussc::Orientation::LandscapeLeft), - "LandscapeRight", sol::var(trussc::Orientation::LandscapeRight), - "Landscape", sol::var(trussc::Orientation::Landscape), - "All", sol::var(trussc::Orientation::All), - "AllButUpsideDown", sol::var(trussc::Orientation::AllButUpsideDown)); - lua->new_usertype("LightType", - sol::meta_function::equal_to, [](trussc::LightType a, trussc::LightType b){ return a == b; }, - "Directional", sol::var(trussc::LightType::Directional), - "Point", sol::var(trussc::LightType::Point), - "Spot", sol::var(trussc::LightType::Spot)); - lua->new_usertype("PixelFormat", - sol::meta_function::equal_to, [](trussc::PixelFormat a, trussc::PixelFormat b){ return a == b; }, - "U8", sol::var(trussc::PixelFormat::U8), - "F32", sol::var(trussc::PixelFormat::F32)); - lua->new_usertype("TextureFormat", - sol::meta_function::equal_to, [](trussc::TextureFormat a, trussc::TextureFormat b){ return a == b; }, - "RGBA8", sol::var(trussc::TextureFormat::RGBA8), - "RGBA16F", sol::var(trussc::TextureFormat::RGBA16F), - "RGBA32F", sol::var(trussc::TextureFormat::RGBA32F), - "R8", sol::var(trussc::TextureFormat::R8), - "R16F", sol::var(trussc::TextureFormat::R16F), - "R32F", sol::var(trussc::TextureFormat::R32F), - "RG8", sol::var(trussc::TextureFormat::RG8), - "RG16F", sol::var(trussc::TextureFormat::RG16F), - "RG32F", sol::var(trussc::TextureFormat::RG32F), - "BGRA8", sol::var(trussc::TextureFormat::BGRA8), - "RGBA16", sol::var(trussc::TextureFormat::RGBA16)); - lua->new_usertype("TextureUsage", - sol::meta_function::equal_to, [](trussc::TextureUsage a, trussc::TextureUsage b){ return a == b; }, - "Immutable", sol::var(trussc::TextureUsage::Immutable), - "Dynamic", sol::var(trussc::TextureUsage::Dynamic), - "Stream", sol::var(trussc::TextureUsage::Stream), - "RenderTarget", sol::var(trussc::TextureUsage::RenderTarget)); - lua->new_usertype("ImageType", - sol::meta_function::equal_to, [](trussc::ImageType a, trussc::ImageType b){ return a == b; }, - "Color", sol::var(trussc::ImageType::Color), - "Grayscale", sol::var(trussc::ImageType::Grayscale)); - lua->new_usertype("PrimitiveMode", - sol::meta_function::equal_to, [](trussc::PrimitiveMode a, trussc::PrimitiveMode b){ return a == b; }, - "Triangles", sol::var(trussc::PrimitiveMode::Triangles), - "TriangleStrip", sol::var(trussc::PrimitiveMode::TriangleStrip), - "TriangleFan", sol::var(trussc::PrimitiveMode::TriangleFan), - "Lines", sol::var(trussc::PrimitiveMode::Lines), - "LineStrip", sol::var(trussc::PrimitiveMode::LineStrip), - "LineLoop", sol::var(trussc::PrimitiveMode::LineLoop), - "Points", sol::var(trussc::PrimitiveMode::Points)); - lua->new_usertype("WritingMode", - sol::meta_function::equal_to, [](trussc::WritingMode a, trussc::WritingMode b){ return a == b; }, - "Horizontal", sol::var(trussc::WritingMode::Horizontal), - "VerticalRL", sol::var(trussc::WritingMode::VerticalRL)); - lua->new_usertype("TcyMode", - sol::meta_function::equal_to, [](trussc::TcyMode a, trussc::TcyMode b){ return a == b; }, - "Rotate", sol::var(trussc::TcyMode::Rotate), - "Upright", sol::var(trussc::TcyMode::Upright), - "Combine", sol::var(trussc::TcyMode::Combine)); - lua->new_usertype("KinsokuLevel", - sol::meta_function::equal_to, [](trussc::KinsokuLevel a, trussc::KinsokuLevel b){ return a == b; }, - "Off", sol::var(trussc::KinsokuLevel::Off), - "PunctuationOnly", sol::var(trussc::KinsokuLevel::PunctuationOnly), - "Standard", sol::var(trussc::KinsokuLevel::Standard)); - lua->new_usertype("VideoCodec", - sol::meta_function::equal_to, [](trussc::VideoCodec a, trussc::VideoCodec b){ return a == b; }, - "H264", sol::var(trussc::VideoCodec::H264), - "HEVC", sol::var(trussc::VideoCodec::HEVC), - "ProRes422", sol::var(trussc::VideoCodec::ProRes422), - "ProRes4444", sol::var(trussc::VideoCodec::ProRes4444)); - lua->new_usertype("EaseType", - sol::meta_function::equal_to, [](trussc::EaseType a, trussc::EaseType b){ return a == b; }, - "Linear", sol::var(trussc::EaseType::Linear), - "Quad", sol::var(trussc::EaseType::Quad), - "Cubic", sol::var(trussc::EaseType::Cubic), - "Quart", sol::var(trussc::EaseType::Quart), - "Quint", sol::var(trussc::EaseType::Quint), - "Sine", sol::var(trussc::EaseType::Sine), - "Expo", sol::var(trussc::EaseType::Expo), - "Circ", sol::var(trussc::EaseType::Circ), - "Back", sol::var(trussc::EaseType::Back), - "Elastic", sol::var(trussc::EaseType::Elastic), - "Bounce", sol::var(trussc::EaseType::Bounce)); - lua->new_usertype("EaseMode", - sol::meta_function::equal_to, [](trussc::EaseMode a, trussc::EaseMode b){ return a == b; }, - "In", sol::var(trussc::EaseMode::In), - "Out", sol::var(trussc::EaseMode::Out), - "InOut", sol::var(trussc::EaseMode::InOut)); - lua->new_usertype("LayoutDirection", - sol::meta_function::equal_to, [](trussc::LayoutDirection a, trussc::LayoutDirection b){ return a == b; }, - "Vertical", sol::var(trussc::LayoutDirection::Vertical), - "Horizontal", sol::var(trussc::LayoutDirection::Horizontal)); - lua->new_usertype("AxisMode", - sol::meta_function::equal_to, [](trussc::AxisMode a, trussc::AxisMode b){ return a == b; }, - "None", sol::var(trussc::AxisMode::None), - "Fill", sol::var(trussc::AxisMode::Fill), - "Content", sol::var(trussc::AxisMode::Content)); + tcxLuaGenShard_00(lua); + tcxLuaGenShard_01(lua); + tcxLuaGenShard_02(lua); + tcxLuaGenShard_03(lua); + tcxLuaGenShard_04(lua); + tcxLuaGenShard_05(lua); + tcxLuaGenShard_06(lua); + tcxLuaGenShard_07(lua); + tcxLuaGenShard_08(lua); + tcxLuaGenShard_09(lua); + tcxLuaGenShard_10(lua); + tcxLuaGenShard_11(lua); + tcxLuaGenShard_12(lua); + tcxLuaGenShard_13(lua); + tcxLuaGenShard_14(lua); + tcxLuaGenShard_15(lua); // constants (*lua)["TAU"] = trussc::TAU; (*lua)["HALF_TAU"] = trussc::HALF_TAU; diff --git a/addons/tcxLua/src/generated/trussctype_generated_00.cpp b/addons/tcxLua/src/generated/trussctype_generated_00.cpp new file mode 100644 index 00000000..6c421a9c --- /dev/null +++ b/addons/tcxLua/src/generated/trussctype_generated_00.cpp @@ -0,0 +1,127 @@ +// AUTO-GENERATED usertype bindings from reference-data.json by luagen-types.js +#include "tcxLua.h" +#include "TrussC.h" +using namespace trussc; +using namespace std; +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma clang diagnostic push +#endif +void tcxLuaGenShard_00(const std::shared_ptr& lua) { + { + sol::usertype t = lua->new_usertype("Node", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["localMatrixChanged"] = &trussc::Node::localMatrixChanged; + t["setup"] = &trussc::Node::setup; + t["update"] = &trussc::Node::update; + t["draw"] = &trussc::Node::draw; + t["cleanup"] = &trussc::Node::cleanup; + t["addChild"] = sol::overload([](trussc::Node& self, trussc::Node::Ptr child) { return self.addChild(child); }, [](trussc::Node& self, trussc::Node::Ptr child, bool keepGlobalPosition) { return self.addChild(child, keepGlobalPosition); }); + t["insertChild"] = sol::overload([](trussc::Node& self, size_t index, trussc::Node::Ptr child) { return self.insertChild(index, child); }, [](trussc::Node& self, size_t index, trussc::Node::Ptr child, bool keepGlobalPosition) { return self.insertChild(index, child, keepGlobalPosition); }); + t["removeChild"] = &trussc::Node::removeChild; + t["removeAllChildren"] = &trussc::Node::removeAllChildren; + t["onChildAdded"] = &trussc::Node::onChildAdded; + t["onChildRemoved"] = &trussc::Node::onChildRemoved; + t["getParent"] = &trussc::Node::getParent; + t["getChildren"] = &trussc::Node::getChildren; + t["getChildCount"] = &trussc::Node::getChildCount; + t["getChildIndex"] = &trussc::Node::getChildIndex; + t["moveToFront"] = &trussc::Node::moveToFront; + t["moveToBack"] = &trussc::Node::moveToBack; + t["isActive"] = &trussc::Node::isActive; + t["setActive"] = &trussc::Node::setActive; + t["isVisible"] = &trussc::Node::isVisible; + t["setVisible"] = &trussc::Node::setVisible; + t["getActive"] = &trussc::Node::getActive; + t["getVisible"] = &trussc::Node::getVisible; + t["setIsActive"] = &trussc::Node::setIsActive; + t["setIsVisible"] = &trussc::Node::setIsVisible; + t["destroy"] = &trussc::Node::destroy; + t["isDead"] = &trussc::Node::isDead; + t["enableEvents"] = &trussc::Node::enableEvents; + t["disableEvents"] = &trussc::Node::disableEvents; + t["isEventsEnabled"] = &trussc::Node::isEventsEnabled; + t["isMouseOver"] = &trussc::Node::isMouseOver; + t["setName"] = &trussc::Node::setName; + t["getName"] = &trussc::Node::getName; + t["hasName"] = &trussc::Node::hasName; + t["getTypeName"] = &trussc::Node::getTypeName; + t["getDisplayName"] = &trussc::Node::getDisplayName; + t["getInstanceId"] = &trussc::Node::getInstanceId; + t["findByInstanceId"] = &trussc::Node::findByInstanceId; + t["getPos"] = &trussc::Node::getPos; + t["getX"] = &trussc::Node::getX; + t["getY"] = &trussc::Node::getY; + t["getZ"] = &trussc::Node::getZ; + t["setPos"] = sol::overload([](trussc::Node& self, const trussc::Vec3 & pos) { return self.setPos(pos); }, [](trussc::Node& self, float x, float y) { return self.setPos(x, y); }, [](trussc::Node& self, float x, float y, float z) { return self.setPos(x, y, z); }); + t["setX"] = &trussc::Node::setX; + t["setY"] = &trussc::Node::setY; + t["setZ"] = &trussc::Node::setZ; + t["getQuaternion"] = &trussc::Node::getQuaternion; + t["setQuaternion"] = &trussc::Node::setQuaternion; + t["getEuler"] = &trussc::Node::getEuler; + t["setEuler"] = sol::overload([](trussc::Node& self, const trussc::Vec3 & euler) { return self.setEuler(euler); }, [](trussc::Node& self, float pitch, float yaw, float roll) { return self.setEuler(pitch, yaw, roll); }); + t["getEulerDeg"] = &trussc::Node::getEulerDeg; + t["setEulerDeg"] = &trussc::Node::setEulerDeg; + t["getRot"] = &trussc::Node::getRot; + t["setRot"] = &trussc::Node::setRot; + t["getRotDeg"] = &trussc::Node::getRotDeg; + t["setRotDeg"] = &trussc::Node::setRotDeg; + t["getScale"] = &trussc::Node::getScale; + t["getScaleX"] = &trussc::Node::getScaleX; + t["getScaleY"] = &trussc::Node::getScaleY; + t["getScaleZ"] = &trussc::Node::getScaleZ; + t["setScale"] = sol::overload([](trussc::Node& self, const trussc::Vec3 & s) { return self.setScale(s); }, [](trussc::Node& self, float uniform) { return self.setScale(uniform); }, [](trussc::Node& self, float sx, float sy) { return self.setScale(sx, sy); }, [](trussc::Node& self, float sx, float sy, float sz) { return self.setScale(sx, sy, sz); }); + t["setScaleX"] = &trussc::Node::setScaleX; + t["setScaleY"] = &trussc::Node::setScaleY; + t["setScaleZ"] = &trussc::Node::setScaleZ; + t["getLocalMatrix"] = &trussc::Node::getLocalMatrix; + t["getGlobalMatrix"] = &trussc::Node::getGlobalMatrix; + t["getGlobalMatrixInverse"] = &trussc::Node::getGlobalMatrixInverse; + t["globalToLocal"] = [](trussc::Node& self, const trussc::Vec3 & global) { return self.globalToLocal(global); }; + t["getGlobalPos"] = &trussc::Node::getGlobalPos; + t["setGlobalPos"] = sol::overload([](trussc::Node& self, const trussc::Vec3 & global) { return self.setGlobalPos(global); }, [](trussc::Node& self, float x, float y) { return self.setGlobalPos(x, y); }, [](trussc::Node& self, float x, float y, float z) { return self.setGlobalPos(x, y, z); }); + t["localToGlobal"] = [](trussc::Node& self, const trussc::Vec3 & local) { return self.localToGlobal(local); }; + t["getMouseX"] = &trussc::Node::getMouseX; + t["getMouseY"] = &trussc::Node::getMouseY; + t["getPMouseX"] = &trussc::Node::getPMouseX; + t["getPMouseY"] = &trussc::Node::getPMouseY; + t["findHitNode"] = &trussc::Node::findHitNode; + t["findHitNodeFromScreen"] = &trussc::Node::findHitNodeFromScreen; + t["getCameraContext"] = &trussc::Node::getCameraContext; + t["setCameraContext"] = &trussc::Node::setCameraContext; + t["getModTypeNames"] = &trussc::Node::getModTypeNames; + t["getMods"] = &trussc::Node::getMods; + t["getModByTypeName"] = &trussc::Node::getModByTypeName; + t["callAfter"] = &trussc::Node::callAfter; + t["callEvery"] = &trussc::Node::callEvery; + t["cancelTimer"] = &trussc::Node::cancelTimer; + t["cancelAllTimers"] = &trussc::Node::cancelAllTimers; +#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) + t["callAfterAsync"] = &trussc::Node::callAfterAsync; +#endif +#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) + t["callEveryAsync"] = &trussc::Node::callEveryAsync; +#endif +#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) + t["cancelAsyncTimer"] = &trussc::Node::cancelAsyncTimer; +#endif +#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) + t["cancelAllAsyncTimers"] = &trussc::Node::cancelAllAsyncTimers; +#endif + } + lua->new_usertype("LayoutDirection", + sol::meta_function::equal_to, [](trussc::LayoutDirection a, trussc::LayoutDirection b){ return a == b; }, + "Vertical", sol::var(trussc::LayoutDirection::Vertical), + "Horizontal", sol::var(trussc::LayoutDirection::Horizontal)); + { + sol::usertype t = lua->new_usertype("JsonWriteReflector"); + t["members"] = &trussc::JsonWriteReflector::members; + t["endGroup"] = &trussc::JsonWriteReflector::endGroup; + } +} +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#pragma clang diagnostic pop +#endif diff --git a/addons/tcxLua/src/generated/trussctype_generated_01.cpp b/addons/tcxLua/src/generated/trussctype_generated_01.cpp new file mode 100644 index 00000000..2411da09 --- /dev/null +++ b/addons/tcxLua/src/generated/trussctype_generated_01.cpp @@ -0,0 +1,69 @@ +// AUTO-GENERATED usertype bindings from reference-data.json by luagen-types.js +#include "tcxLua.h" +#include "TrussC.h" +using namespace trussc; +using namespace std; +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma clang diagnostic push +#endif +void tcxLuaGenShard_01(const std::shared_ptr& lua) { + { + sol::usertype t = lua->new_usertype("Path", + sol::constructors &), trussc::Path(const std::vector &)>(), + sol::call_constructor, sol::constructors &), trussc::Path(const std::vector &)>(), + sol::meta_function::index, [](const trussc::Path& a, int b){ return a[b]; }); + t["addVertex"] = sol::overload([](trussc::Path& self, float x, float y) { return self.addVertex(x, y); }, [](trussc::Path& self, float x, float y, float z) { return self.addVertex(x, y, z); }, [](trussc::Path& self, const trussc::Vec2 & v) { return self.addVertex(v); }, [](trussc::Path& self, const trussc::Vec3 & v) { return self.addVertex(v); }); + t["addVertices"] = sol::overload([](trussc::Path& self, const std::vector & verts) { return self.addVertices(verts); }, [](trussc::Path& self, const std::vector & verts) { return self.addVertices(verts); }); + t["getVertices"] = [](trussc::Path& self) -> decltype(auto) { return self.getVertices(); }; + t["size"] = &trussc::Path::size; + t["empty"] = &trussc::Path::empty; + t["clear"] = &trussc::Path::clear; + t["moveTo"] = sol::overload([](trussc::Path& self, float x, float y) { return self.moveTo(x, y); }, [](trussc::Path& self, float x, float y, float z) { return self.moveTo(x, y, z); }, [](trussc::Path& self, const trussc::Vec2 & p) { return self.moveTo(p); }, [](trussc::Path& self, const trussc::Vec3 & p) { return self.moveTo(p); }); + t["getNumSubpaths"] = &trussc::Path::getNumSubpaths; + t["getSubpathRange"] = &trussc::Path::getSubpathRange; + t["isSubpathClosed"] = &trussc::Path::isSubpathClosed; + t["lineTo"] = sol::overload([](trussc::Path& self, float x, float y) { return self.lineTo(x, y); }, [](trussc::Path& self, float x, float y, float z) { return self.lineTo(x, y, z); }, [](trussc::Path& self, const trussc::Vec2 & p) { return self.lineTo(p); }, [](trussc::Path& self, const trussc::Vec3 & p) { return self.lineTo(p); }); + t["bezierTo"] = sol::overload([](trussc::Path& self, const trussc::Vec3 & cp1, const trussc::Vec3 & cp2, const trussc::Vec3 & to, int resolution) { return self.bezierTo(cp1, cp2, to, resolution); }, [](trussc::Path& self, float cx1, float cy1, float cx2, float cy2, float x, float y, int resolution) { return self.bezierTo(cx1, cy1, cx2, cy2, x, y, resolution); }, [](trussc::Path& self, const trussc::Vec2 & cp1, const trussc::Vec2 & cp2, const trussc::Vec2 & to, int resolution) { return self.bezierTo(cp1, cp2, to, resolution); }); + t["quadBezierTo"] = sol::overload([](trussc::Path& self, const trussc::Vec3 & cp, const trussc::Vec3 & to, int resolution) { return self.quadBezierTo(cp, to, resolution); }, [](trussc::Path& self, float cx, float cy, float x, float y, int resolution) { return self.quadBezierTo(cx, cy, x, y, resolution); }, [](trussc::Path& self, const trussc::Vec2 & cp, const trussc::Vec2 & to, int resolution) { return self.quadBezierTo(cp, to, resolution); }); + t["curveTo"] = sol::overload([](trussc::Path& self, const trussc::Vec3 & to, int resolution) { return self.curveTo(to, resolution); }, [](trussc::Path& self, float x, float y) { return self.curveTo(x, y); }, [](trussc::Path& self, float x, float y, float z) { return self.curveTo(x, y, z); }, [](trussc::Path& self, float x, float y, float z, int resolution) { return self.curveTo(x, y, z, resolution); }, [](trussc::Path& self, const trussc::Vec2 & to, int resolution) { return self.curveTo(to, resolution); }); + t["arc"] = sol::overload([](trussc::Path& self, const trussc::Vec3 & center, float radiusX, float radiusY, float angleBegin, float angleEnd) { return self.arc(center, radiusX, radiusY, angleBegin, angleEnd); }, [](trussc::Path& self, const trussc::Vec3 & center, float radiusX, float radiusY, float angleBegin, float angleEnd, bool clockwise) { return self.arc(center, radiusX, radiusY, angleBegin, angleEnd, clockwise); }, [](trussc::Path& self, const trussc::Vec3 & center, float radiusX, float radiusY, float angleBegin, float angleEnd, bool clockwise, int circleResolution) { return self.arc(center, radiusX, radiusY, angleBegin, angleEnd, clockwise, circleResolution); }, [](trussc::Path& self, float x, float y, float radiusX, float radiusY, float angleBegin, float angleEnd) { return self.arc(x, y, radiusX, radiusY, angleBegin, angleEnd); }, [](trussc::Path& self, float x, float y, float radiusX, float radiusY, float angleBegin, float angleEnd, int circleResolution) { return self.arc(x, y, radiusX, radiusY, angleBegin, angleEnd, circleResolution); }, [](trussc::Path& self, const trussc::Vec2 & center, float radiusX, float radiusY, float angleBegin, float angleEnd) { return self.arc(center, radiusX, radiusY, angleBegin, angleEnd); }, [](trussc::Path& self, const trussc::Vec2 & center, float radiusX, float radiusY, float angleBegin, float angleEnd, int circleResolution) { return self.arc(center, radiusX, radiusY, angleBegin, angleEnd, circleResolution); }, [](trussc::Path& self, const trussc::Vec3 & center, float radius, float angleBegin, float angleEnd) { return self.arc(center, radius, angleBegin, angleEnd); }, [](trussc::Path& self, const trussc::Vec3 & center, float radius, float angleBegin, float angleEnd, bool clockwise) { return self.arc(center, radius, angleBegin, angleEnd, clockwise); }, [](trussc::Path& self, float x, float y, float radius, float angleBegin, float angleEnd) { return self.arc(x, y, radius, angleBegin, angleEnd); }, [](trussc::Path& self, float x, float y, float radius, float angleBegin, float angleEnd, bool clockwise) { return self.arc(x, y, radius, angleBegin, angleEnd, clockwise); }, [](trussc::Path& self, const trussc::Vec2 & center, float radius, float angleBegin, float angleEnd) { return self.arc(center, radius, angleBegin, angleEnd); }, [](trussc::Path& self, const trussc::Vec2 & center, float radius, float angleBegin, float angleEnd, bool clockwise) { return self.arc(center, radius, angleBegin, angleEnd, clockwise); }); + t["close"] = &trussc::Path::close; + t["setClosed"] = &trussc::Path::setClosed; + t["isClosed"] = &trussc::Path::isClosed; + t["reverseWinding"] = sol::overload([](trussc::Path& self, size_t i) -> decltype(auto) { return self.reverseWinding(i); }, [](trussc::Path& self) -> decltype(auto) { return self.reverseWinding(); }); + t["draw"] = &trussc::Path::draw; + t["buildFillTriangles"] = &trussc::Path::buildFillTriangles; + t["drawFill"] = &trussc::Path::drawFill; + t["toFillMesh"] = &trussc::Path::toFillMesh; + t["drawStroke"] = &trussc::Path::drawStroke; + t["getBounds"] = &trussc::Path::getBounds; + t["getPerimeter"] = &trussc::Path::getPerimeter; + } + lua->new_usertype("TextureUsage", + sol::meta_function::equal_to, [](trussc::TextureUsage a, trussc::TextureUsage b){ return a == b; }, + "Immutable", sol::var(trussc::TextureUsage::Immutable), + "Dynamic", sol::var(trussc::TextureUsage::Dynamic), + "Stream", sol::var(trussc::TextureUsage::Stream), + "RenderTarget", sol::var(trussc::TextureUsage::RenderTarget)); + { + sol::usertype t = lua->new_usertype("FpsSettings"); + t["updateFps"] = &trussc::FpsSettings::updateFps; + t["drawFps"] = &trussc::FpsSettings::drawFps; + t["actualVsyncFps"] = &trussc::FpsSettings::actualVsyncFps; + t["synced"] = &trussc::FpsSettings::synced; + } + lua->new_usertype("WritingMode", + sol::meta_function::equal_to, [](trussc::WritingMode a, trussc::WritingMode b){ return a == b; }, + "Horizontal", sol::var(trussc::WritingMode::Horizontal), + "VerticalRL", sol::var(trussc::WritingMode::VerticalRL)); + { + sol::usertype t = lua->new_usertype("ResizeEventArgs"); + t["width"] = &trussc::ResizeEventArgs::width; + t["height"] = &trussc::ResizeEventArgs::height; + } +} +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#pragma clang diagnostic pop +#endif diff --git a/addons/tcxLua/src/generated/trussctype_generated_02.cpp b/addons/tcxLua/src/generated/trussctype_generated_02.cpp new file mode 100644 index 00000000..fcb22408 --- /dev/null +++ b/addons/tcxLua/src/generated/trussctype_generated_02.cpp @@ -0,0 +1,116 @@ +// AUTO-GENERATED usertype bindings from reference-data.json by luagen-types.js +#include "tcxLua.h" +#include "TrussC.h" +using namespace trussc; +using namespace std; +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma clang diagnostic push +#endif +void tcxLuaGenShard_02(const std::shared_ptr& lua) { + { + sol::usertype t = lua->new_usertype("Mesh", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["setMode"] = &trussc::Mesh::setMode; + t["getMode"] = &trussc::Mesh::getMode; + t["addVertex"] = sol::overload([](trussc::Mesh& self, float x, float y) -> decltype(auto) { return self.addVertex(x, y); }, [](trussc::Mesh& self, float x, float y, float z) -> decltype(auto) { return self.addVertex(x, y, z); }, [](trussc::Mesh& self, const trussc::Vec2 & v) -> decltype(auto) { return self.addVertex(v); }, [](trussc::Mesh& self, const trussc::Vec3 & v) -> decltype(auto) { return self.addVertex(v); }); + t["addVertices"] = &trussc::Mesh::addVertices; + t["getVertices"] = [](trussc::Mesh& self) -> decltype(auto) { return self.getVertices(); }; + t["getNumVertices"] = &trussc::Mesh::getNumVertices; + t["addColor"] = sol::overload([](trussc::Mesh& self, const trussc::Color & c) -> decltype(auto) { return self.addColor(c); }, [](trussc::Mesh& self, float r, float g, float b) -> decltype(auto) { return self.addColor(r, g, b); }, [](trussc::Mesh& self, float r, float g, float b, float a) -> decltype(auto) { return self.addColor(r, g, b, a); }); + t["addColors"] = &trussc::Mesh::addColors; + t["getColors"] = [](trussc::Mesh& self) -> decltype(auto) { return self.getColors(); }; + t["getNumColors"] = &trussc::Mesh::getNumColors; + t["hasColors"] = &trussc::Mesh::hasColors; + t["addIndex"] = &trussc::Mesh::addIndex; + t["addIndices"] = &trussc::Mesh::addIndices; + t["addTriangle"] = &trussc::Mesh::addTriangle; + t["getIndices"] = [](trussc::Mesh& self) -> decltype(auto) { return self.getIndices(); }; + t["getNumIndices"] = &trussc::Mesh::getNumIndices; + t["hasIndices"] = &trussc::Mesh::hasIndices; + t["addNormal"] = sol::overload([](trussc::Mesh& self, float nx, float ny, float nz) -> decltype(auto) { return self.addNormal(nx, ny, nz); }, [](trussc::Mesh& self, const trussc::Vec3 & n) -> decltype(auto) { return self.addNormal(n); }); + t["addNormals"] = &trussc::Mesh::addNormals; + t["setNormal"] = &trussc::Mesh::setNormal; + t["getNormal"] = &trussc::Mesh::getNormal; + t["getNormals"] = [](trussc::Mesh& self) -> decltype(auto) { return self.getNormals(); }; + t["getNumNormals"] = &trussc::Mesh::getNumNormals; + t["hasNormals"] = &trussc::Mesh::hasNormals; + t["addTexCoord"] = sol::overload([](trussc::Mesh& self, float u, float v) -> decltype(auto) { return self.addTexCoord(u, v); }, [](trussc::Mesh& self, const trussc::Vec2 & t) -> decltype(auto) { return self.addTexCoord(t); }); + t["getTexCoords"] = [](trussc::Mesh& self) -> decltype(auto) { return self.getTexCoords(); }; + t["getNumTexCoords"] = &trussc::Mesh::getNumTexCoords; + t["hasTexCoords"] = &trussc::Mesh::hasTexCoords; + t["hasValidTexCoords"] = &trussc::Mesh::hasValidTexCoords; + t["addTangent"] = sol::overload([](trussc::Mesh& self, float tx, float ty, float tz) -> decltype(auto) { return self.addTangent(tx, ty, tz); }, [](trussc::Mesh& self, float tx, float ty, float tz, float tw) -> decltype(auto) { return self.addTangent(tx, ty, tz, tw); }, [](trussc::Mesh& self, const trussc::Vec4 & t) -> decltype(auto) { return self.addTangent(t); }, [](trussc::Mesh& self, const trussc::Vec3 & t) -> decltype(auto) { return self.addTangent(t); }, [](trussc::Mesh& self, const trussc::Vec3 & t, float w) -> decltype(auto) { return self.addTangent(t, w); }); + t["getTangents"] = [](trussc::Mesh& self) -> decltype(auto) { return self.getTangents(); }; + t["getNumTangents"] = &trussc::Mesh::getNumTangents; + t["hasTangents"] = &trussc::Mesh::hasTangents; + t["clear"] = &trussc::Mesh::clear; + t["clearVertices"] = &trussc::Mesh::clearVertices; + t["clearNormals"] = &trussc::Mesh::clearNormals; + t["clearColors"] = &trussc::Mesh::clearColors; + t["clearIndices"] = &trussc::Mesh::clearIndices; + t["clearTexCoords"] = &trussc::Mesh::clearTexCoords; + t["clearTangents"] = &trussc::Mesh::clearTangents; + t["translate"] = sol::overload([](trussc::Mesh& self, float x, float y, float z) -> decltype(auto) { return self.translate(x, y, z); }, [](trussc::Mesh& self, const trussc::Vec3 & offset) -> decltype(auto) { return self.translate(offset); }); + t["rotateX"] = &trussc::Mesh::rotateX; + t["rotateY"] = &trussc::Mesh::rotateY; + t["rotateZ"] = &trussc::Mesh::rotateZ; + t["scale"] = sol::overload([](trussc::Mesh& self, float x, float y, float z) -> decltype(auto) { return self.scale(x, y, z); }, [](trussc::Mesh& self, float s) -> decltype(auto) { return self.scale(s); }, [](trussc::Mesh& self, const trussc::Vec3 & s) -> decltype(auto) { return self.scale(s); }); + t["transform"] = &trussc::Mesh::transform; + t["append"] = &trussc::Mesh::append; + t["draw"] = sol::overload([](trussc::Mesh& self) { return self.draw(); }, [](trussc::Mesh& self, const trussc::Texture & texture) { return self.draw(texture); }, [](trussc::Mesh& self, const trussc::Image & image) { return self.draw(image); }); + t["drawNoLighting"] = &trussc::Mesh::drawNoLighting; + t["drawWithLighting"] = &trussc::Mesh::drawWithLighting; + t["drawNoLightingWithTexture"] = &trussc::Mesh::drawNoLightingWithTexture; + t["drawWireframe"] = &trussc::Mesh::drawWireframe; + t["markGpuDirty"] = &trussc::Mesh::markGpuDirty; + t["uploadToGpu"] = &trussc::Mesh::uploadToGpu; + t["drawGpuPbr"] = &trussc::Mesh::drawGpuPbr; + t["drawGpuPoints"] = &trussc::Mesh::drawGpuPoints; + t["uploadPointsToGpu"] = &trussc::Mesh::uploadPointsToGpu; + t["getGpuVertexBuffer"] = &trussc::Mesh::getGpuVertexBuffer; + t["getGpuIndexBuffer"] = &trussc::Mesh::getGpuIndexBuffer; + t["getGpuVertexCount"] = &trussc::Mesh::getGpuVertexCount; + t["getGpuIndexCount"] = &trussc::Mesh::getGpuIndexCount; + t["getGpuPointBuffer"] = &trussc::Mesh::getGpuPointBuffer; + t["getGpuPointCount"] = &trussc::Mesh::getGpuPointCount; + } + { + sol::usertype t = lua->new_usertype("ScrollBar"); + t["getBarColor"] = &trussc::ScrollBar::getBarColor; + t["setBarColor"] = &trussc::ScrollBar::setBarColor; + t["getBarWidth"] = &trussc::ScrollBar::getBarWidth; + t["setBarWidth"] = &trussc::ScrollBar::setBarWidth; + t["getMargin"] = &trussc::ScrollBar::getMargin; + t["setMargin"] = &trussc::ScrollBar::setMargin; + t["getOffset"] = &trussc::ScrollBar::getOffset; + t["updateFromContainer"] = &trussc::ScrollBar::updateFromContainer; + } + lua->new_usertype("LoadError", + sol::meta_function::equal_to, [](trussc::LoadError a, trussc::LoadError b){ return a == b; }, + "None", sol::var(trussc::LoadError::None), + "FileNotFound", sol::var(trussc::LoadError::FileNotFound), + "UnsupportedFormat", sol::var(trussc::LoadError::UnsupportedFormat), + "DecodeFailed", sol::var(trussc::LoadError::DecodeFailed), + "Unknown", sol::var(trussc::LoadError::Unknown)); + lua->new_usertype("KinsokuLevel", + sol::meta_function::equal_to, [](trussc::KinsokuLevel a, trussc::KinsokuLevel b){ return a == b; }, + "Off", sol::var(trussc::KinsokuLevel::Off), + "PunctuationOnly", sol::var(trussc::KinsokuLevel::PunctuationOnly), + "Standard", sol::var(trussc::KinsokuLevel::Standard)); + lua->new_usertype("EaseMode", + sol::meta_function::equal_to, [](trussc::EaseMode a, trussc::EaseMode b){ return a == b; }, + "In", sol::var(trussc::EaseMode::In), + "Out", sol::var(trussc::EaseMode::Out), + "InOut", sol::var(trussc::EaseMode::InOut)); + { + sol::usertype t = lua->new_usertype("UdpErrorEventArgs"); + t["message"] = &trussc::UdpErrorEventArgs::message; + t["errorCode"] = &trussc::UdpErrorEventArgs::errorCode; + } +} +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#pragma clang diagnostic pop +#endif diff --git a/addons/tcxLua/src/generated/trussctype_generated_03.cpp b/addons/tcxLua/src/generated/trussctype_generated_03.cpp new file mode 100644 index 00000000..1d3dba20 --- /dev/null +++ b/addons/tcxLua/src/generated/trussctype_generated_03.cpp @@ -0,0 +1,116 @@ +// AUTO-GENERATED usertype bindings from reference-data.json by luagen-types.js +#include "tcxLua.h" +#include "TrussC.h" +using namespace trussc; +using namespace std; +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma clang diagnostic push +#endif +void tcxLuaGenShard_03(const std::shared_ptr& lua) { + { + sol::usertype t = lua->new_usertype("TweenMod", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["complete"] = &trussc::TweenMod::complete; + t["moveTo"] = sol::overload([](trussc::TweenMod& self, float x, float y) -> decltype(auto) { return self.moveTo(x, y); }, [](trussc::TweenMod& self, float x, float y, float z) -> decltype(auto) { return self.moveTo(x, y, z); }, [](trussc::TweenMod& self, const trussc::Vec3 & pos) -> decltype(auto) { return self.moveTo(pos); }, [](trussc::TweenMod& self, const trussc::Vec2 & pos) -> decltype(auto) { return self.moveTo(pos); }); + t["moveBy"] = sol::overload([](trussc::TweenMod& self, float dx, float dy) -> decltype(auto) { return self.moveBy(dx, dy); }, [](trussc::TweenMod& self, float dx, float dy, float dz) -> decltype(auto) { return self.moveBy(dx, dy, dz); }, [](trussc::TweenMod& self, const trussc::Vec3 & delta) -> decltype(auto) { return self.moveBy(delta); }, [](trussc::TweenMod& self, const trussc::Vec2 & delta) -> decltype(auto) { return self.moveBy(delta); }); + t["moveFrom"] = sol::overload([](trussc::TweenMod& self, float x, float y) -> decltype(auto) { return self.moveFrom(x, y); }, [](trussc::TweenMod& self, float x, float y, float z) -> decltype(auto) { return self.moveFrom(x, y, z); }, [](trussc::TweenMod& self, const trussc::Vec3 & pos) -> decltype(auto) { return self.moveFrom(pos); }); + t["scaleTo"] = sol::overload([](trussc::TweenMod& self, float uniform) -> decltype(auto) { return self.scaleTo(uniform); }, [](trussc::TweenMod& self, float sx, float sy) -> decltype(auto) { return self.scaleTo(sx, sy); }, [](trussc::TweenMod& self, float sx, float sy, float sz) -> decltype(auto) { return self.scaleTo(sx, sy, sz); }, [](trussc::TweenMod& self, const trussc::Vec3 & s) -> decltype(auto) { return self.scaleTo(s); }); + t["scaleBy"] = sol::overload([](trussc::TweenMod& self, float factor) -> decltype(auto) { return self.scaleBy(factor); }, [](trussc::TweenMod& self, float sx, float sy) -> decltype(auto) { return self.scaleBy(sx, sy); }, [](trussc::TweenMod& self, float sx, float sy, float sz) -> decltype(auto) { return self.scaleBy(sx, sy, sz); }); + t["scaleFrom"] = sol::overload([](trussc::TweenMod& self, float uniform) -> decltype(auto) { return self.scaleFrom(uniform); }, [](trussc::TweenMod& self, float sx, float sy) -> decltype(auto) { return self.scaleFrom(sx, sy); }, [](trussc::TweenMod& self, float sx, float sy, float sz) -> decltype(auto) { return self.scaleFrom(sx, sy, sz); }); + t["rotateTo"] = sol::overload([](trussc::TweenMod& self, float radians) -> decltype(auto) { return self.rotateTo(radians); }, [](trussc::TweenMod& self, const trussc::Quaternion & q) -> decltype(auto) { return self.rotateTo(q); }); + t["rotateBy"] = &trussc::TweenMod::rotateBy; + t["rotateFrom"] = sol::overload([](trussc::TweenMod& self, float radians) -> decltype(auto) { return self.rotateFrom(radians); }, [](trussc::TweenMod& self, const trussc::Quaternion & q) -> decltype(auto) { return self.rotateFrom(q); }); + t["rotateXTo"] = &trussc::TweenMod::rotateXTo; + t["rotateXBy"] = &trussc::TweenMod::rotateXBy; + t["rotateYTo"] = &trussc::TweenMod::rotateYTo; + t["rotateYBy"] = &trussc::TweenMod::rotateYBy; + t["rotateZTo"] = &trussc::TweenMod::rotateZTo; + t["rotateZBy"] = &trussc::TweenMod::rotateZBy; + t["rotateXFrom"] = &trussc::TweenMod::rotateXFrom; + t["rotateYFrom"] = &trussc::TweenMod::rotateYFrom; + t["duration"] = &trussc::TweenMod::duration; + t["ease"] = sol::overload([](trussc::TweenMod& self, trussc::EaseType type) -> decltype(auto) { return self.ease(type); }, [](trussc::TweenMod& self, trussc::EaseType type, trussc::EaseMode mode) -> decltype(auto) { return self.ease(type, mode); }); + t["delay"] = &trussc::TweenMod::delay; + t["start"] = &trussc::TweenMod::start; + t["pause"] = &trussc::TweenMod::pause; + t["resume"] = &trussc::TweenMod::resume; + t["reset"] = &trussc::TweenMod::reset; + t["isPlaying"] = &trussc::TweenMod::isPlaying; + t["isComplete"] = &trussc::TweenMod::isComplete; + t["getProgress"] = &trussc::TweenMod::getProgress; + t["getDuration"] = &trussc::TweenMod::getDuration; + t["getDelay"] = &trussc::TweenMod::getDelay; + t["getEaseType"] = &trussc::TweenMod::getEaseType; + t["getEaseMode"] = &trussc::TweenMod::getEaseMode; + } + { + sol::usertype t = lua->new_usertype("MouseDragEventArgs"); + t["x"] = &trussc::MouseDragEventArgs::x; + t["y"] = &trussc::MouseDragEventArgs::y; + t["deltaX"] = &trussc::MouseDragEventArgs::deltaX; + t["deltaY"] = &trussc::MouseDragEventArgs::deltaY; + t["button"] = &trussc::MouseDragEventArgs::button; + t["shift"] = &trussc::MouseDragEventArgs::shift; + t["ctrl"] = &trussc::MouseDragEventArgs::ctrl; + t["alt"] = &trussc::MouseDragEventArgs::alt; + t["super"] = &trussc::MouseDragEventArgs::super; + t["pos"] = &trussc::MouseDragEventArgs::pos; + t["globalPos"] = &trussc::MouseDragEventArgs::globalPos; + t["delta"] = &trussc::MouseDragEventArgs::delta; + t["globalDelta"] = &trussc::MouseDragEventArgs::globalDelta; + t["consumed"] = &trussc::MouseDragEventArgs::consumed; + t["syncLegacy"] = &trussc::MouseDragEventArgs::syncLegacy; + } + lua->new_usertype("EaseType", + sol::meta_function::equal_to, [](trussc::EaseType a, trussc::EaseType b){ return a == b; }, + "Linear", sol::var(trussc::EaseType::Linear), + "Quad", sol::var(trussc::EaseType::Quad), + "Cubic", sol::var(trussc::EaseType::Cubic), + "Quart", sol::var(trussc::EaseType::Quart), + "Quint", sol::var(trussc::EaseType::Quint), + "Sine", sol::var(trussc::EaseType::Sine), + "Expo", sol::var(trussc::EaseType::Expo), + "Circ", sol::var(trussc::EaseType::Circ), + "Back", sol::var(trussc::EaseType::Back), + "Elastic", sol::var(trussc::EaseType::Elastic), + "Bounce", sol::var(trussc::EaseType::Bounce)); + lua->new_usertype("PrimitiveType", + sol::meta_function::equal_to, [](trussc::PrimitiveType a, trussc::PrimitiveType b){ return a == b; }, + "Points", sol::var(trussc::PrimitiveType::Points), + "Lines", sol::var(trussc::PrimitiveType::Lines), + "LineStrip", sol::var(trussc::PrimitiveType::LineStrip), + "Triangles", sol::var(trussc::PrimitiveType::Triangles), + "TriangleStrip", sol::var(trussc::PrimitiveType::TriangleStrip), + "Quads", sol::var(trussc::PrimitiveType::Quads)); + { + sol::usertype t = lua->new_usertype("VideoRecordSettings"); + t["codec"] = &trussc::VideoRecordSettings::codec; + t["fps"] = &trussc::VideoRecordSettings::fps; + t["bitrate"] = &trussc::VideoRecordSettings::bitrate; + t["keyframeInterval"] = &trussc::VideoRecordSettings::keyframeInterval; + t["duration"] = &trussc::VideoRecordSettings::duration; + } + { + sol::usertype t = lua->new_usertype("TcpServerErrorEventArgs"); + t["message"] = &trussc::TcpServerErrorEventArgs::message; + t["errorCode"] = &trussc::TcpServerErrorEventArgs::errorCode; + t["clientId"] = &trussc::TcpServerErrorEventArgs::clientId; + } + { + sol::usertype t = lua->new_usertype("TcpServerClient"); + t["getId"] = &trussc::TcpServerClient::getId; + t["getHost"] = &trussc::TcpServerClient::getHost; + t["getPort"] = &trussc::TcpServerClient::getPort; + } + { + sol::usertype t = lua->new_usertype("TcpErrorEventArgs"); + t["message"] = &trussc::TcpErrorEventArgs::message; + t["errorCode"] = &trussc::TcpErrorEventArgs::errorCode; + } +} +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#pragma clang diagnostic pop +#endif diff --git a/addons/tcxLua/src/generated/trussctype_generated_04.cpp b/addons/tcxLua/src/generated/trussctype_generated_04.cpp new file mode 100644 index 00000000..0c32d79f --- /dev/null +++ b/addons/tcxLua/src/generated/trussctype_generated_04.cpp @@ -0,0 +1,130 @@ +// AUTO-GENERATED usertype bindings from reference-data.json by luagen-types.js +#include "tcxLua.h" +#include "TrussC.h" +using namespace trussc; +using namespace std; +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma clang diagnostic push +#endif +void tcxLuaGenShard_04(const std::shared_ptr& lua) { + { + sol::usertype t = lua->new_usertype("Font", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["load"] = &trussc::Font::load; + t["isLoaded"] = &trussc::Font::isLoaded; + t["setAlign"] = sol::overload([](trussc::Font& self, trussc::Direction h, trussc::Direction v) { return self.setAlign(h, v); }, [](trussc::Font& self, trussc::Direction h) { return self.setAlign(h); }); + t["getAlignH"] = &trussc::Font::getAlignH; + t["getAlignV"] = &trussc::Font::getAlignV; + t["setLineHeight"] = &trussc::Font::setLineHeight; + t["setLineHeightEm"] = &trussc::Font::setLineHeightEm; + t["resetLineHeight"] = &trussc::Font::resetLineHeight; + t["drawString"] = sol::overload([](trussc::Font& self, const std::string & text, float x, float y) { return self.drawString(text, x, y); }, [](trussc::Font& self, const std::string & text, float x, float y, trussc::Direction h, trussc::Direction v) { return self.drawString(text, x, y, h, v); }); + t["getGlyphPath"] = &trussc::Font::getGlyphPath; + t["getStringPath"] = sol::overload([](trussc::Font& self, const std::string & text, float x, float y, trussc::Direction h, trussc::Direction v) { return self.getStringPath(text, x, y, h, v); }, [](trussc::Font& self, const std::string & text, float x, float y) { return self.getStringPath(text, x, y); }); + t["setWritingMode"] = &trussc::Font::setWritingMode; + t["getWritingMode"] = &trussc::Font::getWritingMode; + t["setTcyDigits"] = &trussc::Font::setTcyDigits; + t["setTcyLatin"] = &trussc::Font::setTcyLatin; + t["getTcyLatinMode"] = &trussc::Font::getTcyLatinMode; + t["getTcyDigitMax"] = &trussc::Font::getTcyDigitMax; + t["enableWrap"] = &trussc::Font::enableWrap; + t["isWrapEnabled"] = &trussc::Font::isWrapEnabled; + t["setMaxLineLength"] = &trussc::Font::setMaxLineLength; + t["getMaxLineLength"] = &trussc::Font::getMaxLineLength; + t["setLatinHyphenation"] = &trussc::Font::setLatinHyphenation; + t["getLatinHyphenation"] = &trussc::Font::getLatinHyphenation; + t["setHangingPunctuation"] = &trussc::Font::setHangingPunctuation; + t["getHangingPunctuation"] = &trussc::Font::getHangingPunctuation; + t["setKinsoku"] = &trussc::Font::setKinsoku; + t["getKinsoku"] = &trussc::Font::getKinsoku; + t["forEachGlyph"] = sol::overload([](trussc::Font& self, const std::string & text, float x, float y, trussc::Direction h, trussc::Direction v, const trussc::Font::GlyphVisitor & visitor) { return self.forEachGlyph(text, x, y, h, v, visitor); }, [](trussc::Font& self, const std::string & text, float x, float y, const trussc::Font::GlyphVisitor & visitor) { return self.forEachGlyph(text, x, y, visitor); }); + t["getWidth"] = &trussc::Font::getWidth; + t["stringWidth"] = &trussc::Font::stringWidth; + t["getHeight"] = &trussc::Font::getHeight; + t["getBBox"] = &trussc::Font::getBBox; + t["getLineHeight"] = &trussc::Font::getLineHeight; + t["getDefaultLineHeight"] = &trussc::Font::getDefaultLineHeight; + t["getAscent"] = &trussc::Font::getAscent; + t["getDescent"] = &trussc::Font::getDescent; + t["getSize"] = &trussc::Font::getSize; + t["getMemoryUsage"] = &trussc::Font::getMemoryUsage; + t["getAtlasMemoryUsage"] = &trussc::Font::getAtlasMemoryUsage; + t["clearAtlas"] = &trussc::Font::clearAtlas; + t["getAtlasCount"] = &trussc::Font::getAtlasCount; + t["getSampler"] = &trussc::Font::getSampler; + t["getLoadedGlyphCount"] = &trussc::Font::getLoadedGlyphCount; + t["getTotalCacheMemoryUsage"] = &trussc::Font::getTotalCacheMemoryUsage; + } + { + sol::usertype t = lua->new_usertype("Rect", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["x"] = &trussc::Rect::x; + t["y"] = &trussc::Rect::y; + t["width"] = &trussc::Rect::width; + t["height"] = &trussc::Rect::height; + t["set"] = sol::overload([](trussc::Rect& self, float x_, float y_, float w_, float h_) -> decltype(auto) { return self.set(x_, y_, w_, h_); }, [](trussc::Rect& self, const trussc::Vec2 & pos, float w_, float h_) -> decltype(auto) { return self.set(pos, w_, h_); }); + t["getRight"] = &trussc::Rect::getRight; + t["getBottom"] = &trussc::Rect::getBottom; + t["getCenter"] = &trussc::Rect::getCenter; + t["getCenterX"] = &trussc::Rect::getCenterX; + t["getCenterY"] = &trussc::Rect::getCenterY; + t["contains"] = &trussc::Rect::contains; + t["intersects"] = &trussc::Rect::intersects; + } + { + sol::usertype t = lua->new_usertype("Logger", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["onLog"] = &trussc::Logger::onLog; + t["log"] = &trussc::Logger::log; + t["setConsoleLogLevel"] = &trussc::Logger::setConsoleLogLevel; + t["getConsoleLogLevel"] = &trussc::Logger::getConsoleLogLevel; + t["setLogFile"] = &trussc::Logger::setLogFile; + t["closeFile"] = &trussc::Logger::closeFile; + t["setFileLogLevel"] = &trussc::Logger::setFileLogLevel; + t["getFileLogLevel"] = &trussc::Logger::getFileLogLevel; + t["getLogFilePath"] = &trussc::Logger::getLogFilePath; + t["isFileOpen"] = &trussc::Logger::isFileOpen; + } + { + sol::usertype> t = lua->new_usertype>("Tween_float", + sol::constructors(), trussc::Tween(float, float, float), trussc::Tween(float, float, float, trussc::EaseType), trussc::Tween(float, float, float, trussc::EaseType, trussc::EaseMode)>(), + sol::call_constructor, sol::constructors(), trussc::Tween(float, float, float), trussc::Tween(float, float, float, trussc::EaseType), trussc::Tween(float, float, float, trussc::EaseType, trussc::EaseMode)>()); + } + { + sol::usertype t = lua->new_usertype("VideoDeviceInfo"); + t["deviceId"] = &trussc::VideoDeviceInfo::deviceId; + t["deviceName"] = &trussc::VideoDeviceInfo::deviceName; + t["uniqueId"] = &trussc::VideoDeviceInfo::uniqueId; + t["getDeviceID"] = &trussc::VideoDeviceInfo::getDeviceID; + t["getDeviceName"] = &trussc::VideoDeviceInfo::getDeviceName; + t["getUniqueId"] = &trussc::VideoDeviceInfo::getUniqueId; + } + { + sol::usertype t = lua->new_usertype("LogStream", + sol::constructors(), + sol::call_constructor, sol::constructors()); + } + { + sol::usertype t = lua->new_usertype("Reflector"); + t["isReadOnly"] = &trussc::Reflector::isReadOnly; + t["pushReadOnly"] = &trussc::Reflector::pushReadOnly; + t["popReadOnly"] = &trussc::Reflector::popReadOnly; + t["endGroup"] = &trussc::Reflector::endGroup; + } + lua->new_usertype("MixMode", + sol::meta_function::equal_to, [](trussc::MixMode a, trussc::MixMode b){ return a == b; }, + "Auto", sol::var(trussc::MixMode::Auto), + "DownmixMono", sol::var(trussc::MixMode::DownmixMono)); + { + sol::usertype t = lua->new_usertype("ClipboardPastedEventArgs"); + t["text"] = &trussc::ClipboardPastedEventArgs::text; + } +} +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#pragma clang diagnostic pop +#endif diff --git a/addons/tcxLua/src/generated/trussctype_generated_05.cpp b/addons/tcxLua/src/generated/trussctype_generated_05.cpp new file mode 100644 index 00000000..be7e9e57 --- /dev/null +++ b/addons/tcxLua/src/generated/trussctype_generated_05.cpp @@ -0,0 +1,130 @@ +// AUTO-GENERATED usertype bindings from reference-data.json by luagen-types.js +#include "tcxLua.h" +#include "TrussC.h" +using namespace trussc; +using namespace std; +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma clang diagnostic push +#endif +void tcxLuaGenShard_05(const std::shared_ptr& lua) { + { + sol::usertype t = lua->new_usertype("Color", + sol::constructors(), + sol::call_constructor, sol::constructors(), + sol::meta_function::addition, [](const trussc::Color& a, const trussc::Color & b){ return a + b; }, + sol::meta_function::subtraction, [](const trussc::Color& a, const trussc::Color & b){ return a - b; }, + sol::meta_function::multiplication, [](const trussc::Color& a, float b){ return a * b; }, + sol::meta_function::division, [](const trussc::Color& a, float b){ return a / b; }, + sol::meta_function::equal_to, [](const trussc::Color& a, const trussc::Color & b){ return a == b; }); + t["r"] = &trussc::Color::r; + t["g"] = &trussc::Color::g; + t["b"] = &trussc::Color::b; + t["a"] = &trussc::Color::a; + t["set"] = sol::overload([](trussc::Color& self, float r_, float g_, float b_) -> decltype(auto) { return self.set(r_, g_, b_); }, [](trussc::Color& self, float r_, float g_, float b_, float a_) -> decltype(auto) { return self.set(r_, g_, b_, a_); }, [](trussc::Color& self, float gray) -> decltype(auto) { return self.set(gray); }, [](trussc::Color& self, float gray, float a_) -> decltype(auto) { return self.set(gray, a_); }, [](trussc::Color& self, const trussc::Color & c) -> decltype(auto) { return self.set(c); }); + t["toHex"] = sol::overload([](trussc::Color& self) { return self.toHex(); }, [](trussc::Color& self, bool includeAlpha) { return self.toHex(includeAlpha); }); + t["toLinear"] = &trussc::Color::toLinear; + t["toHSB"] = &trussc::Color::toHSB; + t["toOKLab"] = &trussc::Color::toOKLab; + t["toOKLCH"] = &trussc::Color::toOKLCH; + t["clamped"] = &trussc::Color::clamped; + t["lerpRGB"] = &trussc::Color::lerpRGB; + t["lerpLinear"] = &trussc::Color::lerpLinear; + t["lerpHSB"] = &trussc::Color::lerpHSB; + t["lerpOKLab"] = &trussc::Color::lerpOKLab; + t["lerpOKLCH"] = &trussc::Color::lerpOKLCH; + t["lerp"] = &trussc::Color::lerp; + t["fromBytes"] = sol::overload([](int r, int g, int b) { return trussc::Color::fromBytes(r, g, b); }, [](int r, int g, int b, int a) { return trussc::Color::fromBytes(r, g, b, a); }); + t["fromHex"] = sol::overload([](uint32_t hex) { return trussc::Color::fromHex(hex); }, [](uint32_t hex, bool hasAlpha) { return trussc::Color::fromHex(hex, hasAlpha); }); + t["fromHSB"] = sol::overload([](float h, float s, float b) { return trussc::Color::fromHSB(h, s, b); }, [](float h, float s, float b, float a) { return trussc::Color::fromHSB(h, s, b, a); }); + t["fromOKLCH"] = sol::overload([](float L, float C, float H) { return trussc::Color::fromOKLCH(L, C, H); }, [](float L, float C, float H, float a) { return trussc::Color::fromOKLCH(L, C, H, a); }); + t["fromOKLab"] = sol::overload([](float L, float a_lab, float b_lab) { return trussc::Color::fromOKLab(L, a_lab, b_lab); }, [](float L, float a_lab, float b_lab, float alpha) { return trussc::Color::fromOKLab(L, a_lab, b_lab, alpha); }); + t["fromLinear"] = sol::overload([](float r, float g, float b) { return trussc::Color::fromLinear(r, g, b); }, [](float r, float g, float b, float a) { return trussc::Color::fromLinear(r, g, b, a); }); + } + { + sol::usertype t = lua->new_usertype("CoreEvents"); + t["setup"] = &trussc::CoreEvents::setup; + t["update"] = &trussc::CoreEvents::update; + t["draw"] = &trussc::CoreEvents::draw; + t["onRender"] = &trussc::CoreEvents::onRender; + t["afterFrame"] = &trussc::CoreEvents::afterFrame; + t["exit"] = &trussc::CoreEvents::exit; + t["exitRequested"] = &trussc::CoreEvents::exitRequested; + t["keyPressed"] = &trussc::CoreEvents::keyPressed; + t["keyReleased"] = &trussc::CoreEvents::keyReleased; + t["mousePressed"] = &trussc::CoreEvents::mousePressed; + t["mouseReleased"] = &trussc::CoreEvents::mouseReleased; + t["mouseMoved"] = &trussc::CoreEvents::mouseMoved; + t["mouseDragged"] = &trussc::CoreEvents::mouseDragged; + t["mouseScrolled"] = &trussc::CoreEvents::mouseScrolled; + t["windowResized"] = &trussc::CoreEvents::windowResized; + t["filesDropped"] = &trussc::CoreEvents::filesDropped; + t["clipboardPasted"] = &trussc::CoreEvents::clipboardPasted; + t["console"] = &trussc::CoreEvents::console; + t["touchPressed"] = &trussc::CoreEvents::touchPressed; + t["touchMoved"] = &trussc::CoreEvents::touchMoved; + t["touchReleased"] = &trussc::CoreEvents::touchReleased; + t["rawEvent"] = &trussc::CoreEvents::rawEvent; + } + { + sol::usertype t = lua->new_usertype("MouseMoveEventArgs"); + t["x"] = &trussc::MouseMoveEventArgs::x; + t["y"] = &trussc::MouseMoveEventArgs::y; + t["deltaX"] = &trussc::MouseMoveEventArgs::deltaX; + t["deltaY"] = &trussc::MouseMoveEventArgs::deltaY; + t["shift"] = &trussc::MouseMoveEventArgs::shift; + t["ctrl"] = &trussc::MouseMoveEventArgs::ctrl; + t["alt"] = &trussc::MouseMoveEventArgs::alt; + t["super"] = &trussc::MouseMoveEventArgs::super; + t["pos"] = &trussc::MouseMoveEventArgs::pos; + t["globalPos"] = &trussc::MouseMoveEventArgs::globalPos; + t["delta"] = &trussc::MouseMoveEventArgs::delta; + t["globalDelta"] = &trussc::MouseMoveEventArgs::globalDelta; + t["consumed"] = &trussc::MouseMoveEventArgs::consumed; + t["syncLegacy"] = &trussc::MouseMoveEventArgs::syncLegacy; + } + { + sol::usertype t = lua->new_usertype("SoundStream", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["loadStream"] = sol::overload([](trussc::SoundStream& self, const fs::path & path) { return self.loadStream(path); }, [](trussc::SoundStream& self, const fs::path & path, int maxPolyphony) { return self.loadStream(path, maxPolyphony); }); + t["getDuration"] = &trussc::SoundStream::getDuration; + t["getPath"] = &trussc::SoundStream::getPath; + t["getMaxPolyphony"] = &trussc::SoundStream::getMaxPolyphony; + } + { + sol::usertype t = lua->new_usertype("LogEventArgs", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["level"] = &trussc::LogEventArgs::level; + t["message"] = &trussc::LogEventArgs::message; + t["timestamp"] = &trussc::LogEventArgs::timestamp; + } + { + sol::usertype t = lua->new_usertype("EventListener", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["disconnect"] = &trussc::EventListener::disconnect; + t["isConnected"] = &trussc::EventListener::isConnected; + } + { + sol::usertype t = lua->new_usertype("FullscreenShader", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["draw"] = &trussc::FullscreenShader::draw; + } + { + sol::usertype t = lua->new_usertype("DragDropEventArgs"); + t["files"] = &trussc::DragDropEventArgs::files; + t["x"] = &trussc::DragDropEventArgs::x; + t["y"] = &trussc::DragDropEventArgs::y; + } + { + sol::usertype t = lua->new_usertype("Mod"); + t["getOwner"] = [](trussc::Mod& self) { return self.getOwner(); }; + } +} +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#pragma clang diagnostic pop +#endif diff --git a/addons/tcxLua/src/generated/trussctype_generated_06.cpp b/addons/tcxLua/src/generated/trussctype_generated_06.cpp new file mode 100644 index 00000000..f5f451c1 --- /dev/null +++ b/addons/tcxLua/src/generated/trussctype_generated_06.cpp @@ -0,0 +1,141 @@ +// AUTO-GENERATED usertype bindings from reference-data.json by luagen-types.js +#include "tcxLua.h" +#include "TrussC.h" +using namespace trussc; +using namespace std; +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma clang diagnostic push +#endif +void tcxLuaGenShard_06(const std::shared_ptr& lua) { +#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) || defined(__EMSCRIPTEN__) + { + sol::usertype t = lua->new_usertype("VideoPlayer", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["load"] = &trussc::VideoPlayer::load; + t["close"] = &trussc::VideoPlayer::close; + t["update"] = &trussc::VideoPlayer::update; + t["play"] = &trussc::VideoPlayer::play; + t["setAutoPoster"] = &trussc::VideoPlayer::setAutoPoster; + t["getAutoPoster"] = &trussc::VideoPlayer::getAutoPoster; + t["draw"] = sol::overload([](trussc::VideoPlayer& self, float x, float y) { return self.draw(x, y); }, [](trussc::VideoPlayer& self, float x, float y, float w, float h) { return self.draw(x, y, w, h); }); + t["getDuration"] = &trussc::VideoPlayer::getDuration; + t["getPosition"] = &trussc::VideoPlayer::getPosition; + t["getCurrentFrame"] = &trussc::VideoPlayer::getCurrentFrame; + t["getTotalFrames"] = &trussc::VideoPlayer::getTotalFrames; + t["setFrame"] = &trussc::VideoPlayer::setFrame; + t["nextFrame"] = &trussc::VideoPlayer::nextFrame; + t["previousFrame"] = &trussc::VideoPlayer::previousFrame; + t["setGammaCorrection"] = &trussc::VideoPlayer::setGammaCorrection; + t["getGammaCorrection"] = &trussc::VideoPlayer::getGammaCorrection; + t["setUseHwAccel"] = &trussc::VideoPlayer::setUseHwAccel; + t["getUseHwAccel"] = &trussc::VideoPlayer::getUseHwAccel; + t["getPixels"] = [](trussc::VideoPlayer& self) { return self.getPixels(); }; + t["getPixelsY"] = &trussc::VideoPlayer::getPixelsY; + t["getPixelsUV"] = &trussc::VideoPlayer::getPixelsUV; + t["hasAudio"] = &trussc::VideoPlayer::hasAudio; +#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) + t["getAudioCodec"] = &trussc::VideoPlayer::getAudioCodec; +#endif +#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) + t["getAudioData"] = &trussc::VideoPlayer::getAudioData; +#endif +#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) + t["getAudioSampleRate"] = &trussc::VideoPlayer::getAudioSampleRate; +#endif +#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) + t["getAudioChannels"] = &trussc::VideoPlayer::getAudioChannels; +#endif + t["isUsingHwAccel"] = &trussc::VideoPlayer::isUsingHwAccel; + t["getHwAccelName"] = &trussc::VideoPlayer::getHwAccelName; + t["getPath"] = &trussc::VideoPlayer::getPath; + } +#endif +#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) + { + sol::usertype t = lua->new_usertype("Serial", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["getDeviceList"] = &trussc::Serial::getDeviceList; + t["setup"] = sol::overload([](trussc::Serial& self, const std::string & portName, int baudRate) { return self.setup(portName, baudRate); }, [](trussc::Serial& self, int deviceIndex, int baudRate) { return self.setup(deviceIndex, baudRate); }); + t["close"] = &trussc::Serial::close; + t["isInitialized"] = &trussc::Serial::isInitialized; + t["getDevicePath"] = &trussc::Serial::getDevicePath; + t["available"] = &trussc::Serial::available; + t["readByte"] = &trussc::Serial::readByte; + t["writeBytes"] = [](trussc::Serial& self, const std::string & buffer) { return self.writeBytes(buffer); }; + t["writeByte"] = &trussc::Serial::writeByte; + t["flushInput"] = &trussc::Serial::flushInput; + t["flushOutput"] = &trussc::Serial::flushOutput; + t["flush"] = &trussc::Serial::flush; + t["drain"] = &trussc::Serial::drain; + t["printDevices"] = &trussc::Serial::printDevices; + t["listDevices"] = &trussc::Serial::listDevices; + } +#endif + { + sol::usertype t = lua->new_usertype("IVec2", + sol::constructors(), + sol::call_constructor, sol::constructors(), + sol::meta_function::addition, [](const trussc::IVec2& a, const trussc::IVec2 & b){ return a + b; }, + sol::meta_function::subtraction, [](const trussc::IVec2& a, const trussc::IVec2 & b){ return a - b; }, + sol::meta_function::unary_minus, [](const trussc::IVec2& a){ return -a; }, + sol::meta_function::multiplication, [](const trussc::IVec2& a, int b){ return a * b; }, + sol::meta_function::equal_to, [](const trussc::IVec2& a, const trussc::IVec2 & b){ return a == b; }); + t["x"] = &trussc::IVec2::x; + t["y"] = &trussc::IVec2::y; + t["toVec2"] = &trussc::IVec2::toVec2; + } + { + sol::usertype t = lua->new_usertype("MouseEventArgs"); + t["x"] = &trussc::MouseEventArgs::x; + t["y"] = &trussc::MouseEventArgs::y; + t["button"] = &trussc::MouseEventArgs::button; + t["shift"] = &trussc::MouseEventArgs::shift; + t["ctrl"] = &trussc::MouseEventArgs::ctrl; + t["alt"] = &trussc::MouseEventArgs::alt; + t["super"] = &trussc::MouseEventArgs::super; + t["pos"] = &trussc::MouseEventArgs::pos; + t["globalPos"] = &trussc::MouseEventArgs::globalPos; + t["consumed"] = &trussc::MouseEventArgs::consumed; + t["syncLegacy"] = &trussc::MouseEventArgs::syncLegacy; + } + { + sol::usertype t = lua->new_usertype("CameraContext"); + t["view"] = &trussc::CameraContext::view; + t["projection"] = &trussc::CameraContext::projection; + t["viewW"] = &trussc::CameraContext::viewW; + t["viewH"] = &trussc::CameraContext::viewH; + t["pickable"] = &trussc::CameraContext::pickable; + t["screenPointToRay"] = &trussc::CameraContext::screenPointToRay; + t["worldToScreen"] = &trussc::CameraContext::worldToScreen; + } + lua->new_usertype("ThermalState", + sol::meta_function::equal_to, [](trussc::ThermalState a, trussc::ThermalState b){ return a == b; }, + "Nominal", sol::var(trussc::ThermalState::Nominal), + "Fair", sol::var(trussc::ThermalState::Fair), + "Serious", sol::var(trussc::ThermalState::Serious), + "Critical", sol::var(trussc::ThermalState::Critical)); + { + sol::usertype t = lua->new_usertype("TouchPoint"); + t["id"] = &trussc::TouchPoint::id; + t["x"] = &trussc::TouchPoint::x; + t["y"] = &trussc::TouchPoint::y; + t["pressure"] = &trussc::TouchPoint::pressure; + t["changed"] = &trussc::TouchPoint::changed; + } + lua->new_usertype("TextureFilter", + sol::meta_function::equal_to, [](trussc::TextureFilter a, trussc::TextureFilter b){ return a == b; }, + "Nearest", sol::var(trussc::TextureFilter::Nearest), + "Linear", sol::var(trussc::TextureFilter::Linear)); + { + sol::usertype t = lua->new_usertype("ConsoleEventArgs"); + t["raw"] = &trussc::ConsoleEventArgs::raw; + t["args"] = &trussc::ConsoleEventArgs::args; + } +} +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#pragma clang diagnostic pop +#endif diff --git a/addons/tcxLua/src/generated/trussctype_generated_07.cpp b/addons/tcxLua/src/generated/trussctype_generated_07.cpp new file mode 100644 index 00000000..f22c6afb --- /dev/null +++ b/addons/tcxLua/src/generated/trussctype_generated_07.cpp @@ -0,0 +1,139 @@ +// AUTO-GENERATED usertype bindings from reference-data.json by luagen-types.js +#include "tcxLua.h" +#include "TrussC.h" +using namespace trussc; +using namespace std; +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma clang diagnostic push +#endif +void tcxLuaGenShard_07(const std::shared_ptr& lua) { +#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) + { + sol::usertype t = lua->new_usertype("UdpSocket", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["onReceive"] = &trussc::UdpSocket::onReceive; + t["onError"] = &trussc::UdpSocket::onError; + t["create"] = &trussc::UdpSocket::create; + t["bind"] = sol::overload([](trussc::UdpSocket& self, int port) { return self.bind(port); }, [](trussc::UdpSocket& self, int port, bool startReceiving) { return self.bind(port, startReceiving); }); + t["connect"] = &trussc::UdpSocket::connect; + t["close"] = &trussc::UdpSocket::close; + t["sendTo"] = [](trussc::UdpSocket& self, const std::string & host, int port, const std::string & message) { return self.sendTo(host, port, message); }; + t["send"] = [](trussc::UdpSocket& self, const std::string & message) { return self.send(message); }; + t["startReceiving"] = &trussc::UdpSocket::startReceiving; + t["stopReceiving"] = &trussc::UdpSocket::stopReceiving; + t["isReceiving"] = &trussc::UdpSocket::isReceiving; + t["setNonBlocking"] = &trussc::UdpSocket::setNonBlocking; + t["setBroadcast"] = &trussc::UdpSocket::setBroadcast; + t["setReuseAddress"] = &trussc::UdpSocket::setReuseAddress; + t["setReusePort"] = &trussc::UdpSocket::setReusePort; + t["joinMulticastGroup"] = sol::overload([](trussc::UdpSocket& self, const std::string & groupAddr) { return self.joinMulticastGroup(groupAddr); }, [](trussc::UdpSocket& self, const std::string & groupAddr, const std::string & interfaceAddr) { return self.joinMulticastGroup(groupAddr, interfaceAddr); }); + t["leaveMulticastGroup"] = sol::overload([](trussc::UdpSocket& self, const std::string & groupAddr) { return self.leaveMulticastGroup(groupAddr); }, [](trussc::UdpSocket& self, const std::string & groupAddr, const std::string & interfaceAddr) { return self.leaveMulticastGroup(groupAddr, interfaceAddr); }); + t["setMulticastTTL"] = &trussc::UdpSocket::setMulticastTTL; + t["setMulticastLoopback"] = &trussc::UdpSocket::setMulticastLoopback; + t["setMulticastInterface"] = &trussc::UdpSocket::setMulticastInterface; + t["setReceiveBufferSize"] = &trussc::UdpSocket::setReceiveBufferSize; + t["setSendBufferSize"] = &trussc::UdpSocket::setSendBufferSize; + t["setReceiveTimeout"] = &trussc::UdpSocket::setReceiveTimeout; + t["setUseThread"] = &trussc::UdpSocket::setUseThread; + t["getLocalPort"] = &trussc::UdpSocket::getLocalPort; + t["processNetwork"] = &trussc::UdpSocket::processNetwork; + t["isValid"] = &trussc::UdpSocket::isValid; + t["getConnectedHost"] = &trussc::UdpSocket::getConnectedHost; + t["getConnectedPort"] = &trussc::UdpSocket::getConnectedPort; + } +#endif + { + sol::usertype t = lua->new_usertype("WindowSettings"); + t["width"] = &trussc::WindowSettings::width; + t["height"] = &trussc::WindowSettings::height; + t["title"] = &trussc::WindowSettings::title; + t["highDpi"] = &trussc::WindowSettings::highDpi; + t["pixelPerfect"] = &trussc::WindowSettings::pixelPerfect; + t["sampleCount"] = &trussc::WindowSettings::sampleCount; + t["fullscreen"] = &trussc::WindowSettings::fullscreen; + t["decorated"] = &trussc::WindowSettings::decorated; + t["clipboardSize"] = &trussc::WindowSettings::clipboardSize; + t["swapInterval"] = &trussc::WindowSettings::swapInterval; + t["uniformBufferReserve"] = &trussc::WindowSettings::uniformBufferReserve; + t["setSize"] = &trussc::WindowSettings::setSize; + t["setTitle"] = &trussc::WindowSettings::setTitle; + t["setHighDpi"] = &trussc::WindowSettings::setHighDpi; + t["setPixelPerfect"] = &trussc::WindowSettings::setPixelPerfect; + t["setSampleCount"] = &trussc::WindowSettings::setSampleCount; + t["setFullscreen"] = &trussc::WindowSettings::setFullscreen; + t["setDecorated"] = &trussc::WindowSettings::setDecorated; + t["setClipboardSize"] = &trussc::WindowSettings::setClipboardSize; + t["setSwapInterval"] = &trussc::WindowSettings::setSwapInterval; + t["reserveUniformBuffer"] = &trussc::WindowSettings::reserveUniformBuffer; + } + { + sol::usertype t = lua->new_usertype("NetworkInterface"); + t["name"] = &trussc::NetworkInterface::name; + t["address"] = &trussc::NetworkInterface::address; + t["netmask"] = &trussc::NetworkInterface::netmask; + t["mac"] = &trussc::NetworkInterface::mac; + t["isIPv4"] = &trussc::NetworkInterface::isIPv4; + t["isLoopback"] = &trussc::NetworkInterface::isLoopback; + t["isUp"] = &trussc::NetworkInterface::isUp; + t["getName"] = &trussc::NetworkInterface::getName; + t["getAddress"] = &trussc::NetworkInterface::getAddress; + t["getNetmask"] = &trussc::NetworkInterface::getNetmask; + t["getMac"] = &trussc::NetworkInterface::getMac; + t["getIsIPv4"] = &trussc::NetworkInterface::getIsIPv4; + t["getIsLoopback"] = &trussc::NetworkInterface::getIsLoopback; + t["getIsUp"] = &trussc::NetworkInterface::getIsUp; + } + { + sol::usertype t = lua->new_usertype("FileReader", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["open"] = &trussc::FileReader::open; + t["close"] = &trussc::FileReader::close; + t["isOpen"] = &trussc::FileReader::isOpen; + t["eof"] = &trussc::FileReader::eof; + t["readLine"] = [](trussc::FileReader& self) { return self.readLine(); }; + t["readChar"] = &trussc::FileReader::readChar; + t["seek"] = &trussc::FileReader::seek; + t["tell"] = &trussc::FileReader::tell; + t["remaining"] = &trussc::FileReader::remaining; + } + { + sol::usertype t = lua->new_usertype("AudioDeviceChangedArgs"); + t["deviceName"] = &trussc::AudioDeviceChangedArgs::deviceName; + t["isDefaultDevice"] = &trussc::AudioDeviceChangedArgs::isDefaultDevice; + t["sampleRate"] = &trussc::AudioDeviceChangedArgs::sampleRate; + t["channels"] = &trussc::AudioDeviceChangedArgs::channels; + t["bufferSize"] = &trussc::AudioDeviceChangedArgs::bufferSize; + t["maxPolyphony"] = &trussc::AudioDeviceChangedArgs::maxPolyphony; + } + { + sol::usertype t = lua->new_usertype("AudioSettings"); + t["sampleRate"] = &trussc::AudioSettings::sampleRate; + t["channels"] = &trussc::AudioSettings::channels; + t["bufferSize"] = &trussc::AudioSettings::bufferSize; + t["maxPolyphony"] = &trussc::AudioSettings::maxPolyphony; + t["deviceName"] = &trussc::AudioSettings::deviceName; + } + { + sol::usertype t = lua->new_usertype("TcpClientConnectEventArgs"); + t["clientId"] = &trussc::TcpClientConnectEventArgs::clientId; + t["host"] = &trussc::TcpClientConnectEventArgs::host; + t["port"] = &trussc::TcpClientConnectEventArgs::port; + } + lua->new_usertype("AxisMode", + sol::meta_function::equal_to, [](trussc::AxisMode a, trussc::AxisMode b){ return a == b; }, + "None", sol::var(trussc::AxisMode::None), + "Fill", sol::var(trussc::AxisMode::Fill), + "Content", sol::var(trussc::AxisMode::Content)); + { + sol::usertype t = lua->new_usertype("AudioDeviceInfo"); + t["name"] = &trussc::AudioDeviceInfo::name; + t["isDefault"] = &trussc::AudioDeviceInfo::isDefault; + } +} +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#pragma clang diagnostic pop +#endif diff --git a/addons/tcxLua/src/generated/trussctype_generated_08.cpp b/addons/tcxLua/src/generated/trussctype_generated_08.cpp new file mode 100644 index 00000000..0bb32c6b --- /dev/null +++ b/addons/tcxLua/src/generated/trussctype_generated_08.cpp @@ -0,0 +1,140 @@ +// AUTO-GENERATED usertype bindings from reference-data.json by luagen-types.js +#include "tcxLua.h" +#include "TrussC.h" +using namespace trussc; +using namespace std; +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma clang diagnostic push +#endif +void tcxLuaGenShard_08(const std::shared_ptr& lua) { + { + sol::usertype t = lua->new_usertype("Sound", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["load"] = &trussc::Sound::load; +#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) + t["loadStream"] = sol::overload([](trussc::Sound& self, const fs::path & path) { return self.loadStream(path); }, [](trussc::Sound& self, const fs::path & path, int maxPolyphony) { return self.loadStream(path, maxPolyphony); }); +#endif + t["loadTestTone"] = sol::overload([](trussc::Sound& self) { return self.loadTestTone(); }, [](trussc::Sound& self, float frequency) { return self.loadTestTone(frequency); }, [](trussc::Sound& self, float frequency, float duration) { return self.loadTestTone(frequency, duration); }); + t["loadFromBuffer"] = sol::overload([](trussc::Sound& self, const trussc::SoundBuffer & buf) { return self.loadFromBuffer(buf); }, [](trussc::Sound& self, std::shared_ptr buf) { return self.loadFromBuffer(buf); }); + t["isLoaded"] = &trussc::Sound::isLoaded; + t["isStreaming"] = &trussc::Sound::isStreaming; + t["play"] = &trussc::Sound::play; + t["stop"] = &trussc::Sound::stop; + t["pause"] = &trussc::Sound::pause; + t["resume"] = &trussc::Sound::resume; + t["setVolume"] = &trussc::Sound::setVolume; + t["getVolume"] = &trussc::Sound::getVolume; + t["setLoop"] = &trussc::Sound::setLoop; + t["isLoop"] = &trussc::Sound::isLoop; + t["setPan"] = &trussc::Sound::setPan; + t["getPan"] = &trussc::Sound::getPan; + t["setSpeed"] = &trussc::Sound::setSpeed; + t["getSpeed"] = &trussc::Sound::getSpeed; + t["setMixMode"] = &trussc::Sound::setMixMode; + t["getMixMode"] = &trussc::Sound::getMixMode; + t["setChannelMap"] = sol::overload([](trussc::Sound& self, const std::vector & map) { return self.setChannelMap(map); }, [](trussc::Sound& self, std::vector> map) { return self.setChannelMap(map); }); + t["getChannelMap"] = &trussc::Sound::getChannelMap; + t["setChannelGains"] = &trussc::Sound::setChannelGains; + t["getChannelGains"] = &trussc::Sound::getChannelGains; + t["clearChannelMap"] = &trussc::Sound::clearChannelMap; + t["clearChannelGains"] = &trussc::Sound::clearChannelGains; + t["isPlaying"] = &trussc::Sound::isPlaying; + t["isPaused"] = &trussc::Sound::isPaused; + t["getPosition"] = &trussc::Sound::getPosition; + t["setPosition"] = &trussc::Sound::setPosition; + t["getDuration"] = &trussc::Sound::getDuration; + } + { + sol::usertype t = lua->new_usertype("Quaternion", + sol::constructors(), + sol::call_constructor, sol::constructors(), + sol::meta_function::equal_to, [](const trussc::Quaternion& a, const trussc::Quaternion & b){ return a == b; }, + sol::meta_function::multiplication, [](const trussc::Quaternion& a, const trussc::Quaternion & b){ return a * b; }); + t["w"] = &trussc::Quaternion::w; + t["x"] = &trussc::Quaternion::x; + t["y"] = &trussc::Quaternion::y; + t["z"] = &trussc::Quaternion::z; + t["toEuler"] = &trussc::Quaternion::toEuler; + t["toMatrix"] = &trussc::Quaternion::toMatrix; + t["length"] = &trussc::Quaternion::length; + t["lengthSquared"] = &trussc::Quaternion::lengthSquared; + t["normalized"] = &trussc::Quaternion::normalized; + t["normalize"] = &trussc::Quaternion::normalize; + t["conjugate"] = &trussc::Quaternion::conjugate; + t["rotate"] = &trussc::Quaternion::rotate; + t["identity"] = &trussc::Quaternion::identity; + t["fromAxisAngle"] = &trussc::Quaternion::fromAxisAngle; + t["fromEuler"] = sol::overload([](float pitch, float yaw, float roll) { return trussc::Quaternion::fromEuler(pitch, yaw, roll); }, [](const trussc::Vec3 & euler) { return trussc::Quaternion::fromEuler(euler); }); + t["slerp"] = &trussc::Quaternion::slerp; + } + { + sol::usertype t = lua->new_usertype("ColorHSB", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["h"] = &trussc::ColorHSB::h; + t["s"] = &trussc::ColorHSB::s; + t["b"] = &trussc::ColorHSB::b; + t["a"] = &trussc::ColorHSB::a; + t["toRGB"] = &trussc::ColorHSB::toRGB; + t["toLinear"] = &trussc::ColorHSB::toLinear; + t["toOKLab"] = &trussc::ColorHSB::toOKLab; + t["toOKLCH"] = &trussc::ColorHSB::toOKLCH; + t["lerp"] = sol::overload([](trussc::ColorHSB& self, const trussc::ColorHSB & target, float t) { return self.lerp(target, t); }, [](trussc::ColorHSB& self, const trussc::ColorHSB & target, float t, bool shortestPath) { return self.lerp(target, t, shortestPath); }); + } + { + sol::usertype t = lua->new_usertype("IesProfile", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["load"] = &trussc::IesProfile::load; + t["loadFromString"] = &trussc::IesProfile::loadFromString; + t["isLoaded"] = &trussc::IesProfile::isLoaded; + t["getMaxVerticalAngle"] = &trussc::IesProfile::getMaxVerticalAngle; + t["getMaxCandela"] = &trussc::IesProfile::getMaxCandela; + t["getTextureWidth"] = &trussc::IesProfile::getTextureWidth; + t["getView"] = &trussc::IesProfile::getView; + t["getSampler"] = &trussc::IesProfile::getSampler; + } + { + sol::usertype t = lua->new_usertype("BuildInfo"); + t["date"] = &trussc::BuildInfo::date; + t["time"] = &trussc::BuildInfo::time; + t["dateTime"] = &trussc::BuildInfo::dateTime; + t["timestamp"] = &trussc::BuildInfo::timestamp; + t["year"] = &trussc::BuildInfo::year; + t["month"] = &trussc::BuildInfo::month; + t["day"] = &trussc::BuildInfo::day; + t["hour"] = &trussc::BuildInfo::hour; + t["minute"] = &trussc::BuildInfo::minute; + t["second"] = &trussc::BuildInfo::second; + } + { + sol::usertype t = lua->new_usertype("KeyEventArgs"); + t["key"] = &trussc::KeyEventArgs::key; + t["isRepeat"] = &trussc::KeyEventArgs::isRepeat; + t["shift"] = &trussc::KeyEventArgs::shift; + t["ctrl"] = &trussc::KeyEventArgs::ctrl; + t["alt"] = &trussc::KeyEventArgs::alt; + t["super"] = &trussc::KeyEventArgs::super; + t["consumed"] = &trussc::KeyEventArgs::consumed; + } + lua->new_usertype("TextureWrap", + sol::meta_function::equal_to, [](trussc::TextureWrap a, trussc::TextureWrap b){ return a == b; }, + "Repeat", sol::var(trussc::TextureWrap::Repeat), + "ClampToEdge", sol::var(trussc::TextureWrap::ClampToEdge), + "MirroredRepeat", sol::var(trussc::TextureWrap::MirroredRepeat)); + lua->new_usertype("TcyMode", + sol::meta_function::equal_to, [](trussc::TcyMode a, trussc::TcyMode b){ return a == b; }, + "Rotate", sol::var(trussc::TcyMode::Rotate), + "Upright", sol::var(trussc::TcyMode::Upright), + "Combine", sol::var(trussc::TcyMode::Combine)); + lua->new_usertype("Deliver", + sol::meta_function::equal_to, [](trussc::Deliver a, trussc::Deliver b){ return a == b; }, + "Inline", sol::var(trussc::Deliver::Inline), + "Main", sol::var(trussc::Deliver::Main)); +} +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#pragma clang diagnostic pop +#endif diff --git a/addons/tcxLua/src/generated/trussctype_generated_09.cpp b/addons/tcxLua/src/generated/trussctype_generated_09.cpp new file mode 100644 index 00000000..38d3e6bf --- /dev/null +++ b/addons/tcxLua/src/generated/trussctype_generated_09.cpp @@ -0,0 +1,130 @@ +// AUTO-GENERATED usertype bindings from reference-data.json by luagen-types.js +#include "tcxLua.h" +#include "TrussC.h" +using namespace trussc; +using namespace std; +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma clang diagnostic push +#endif +void tcxLuaGenShard_09(const std::shared_ptr& lua) { + { + sol::usertype t = lua->new_usertype("App", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["setSize"] = &trussc::App::setSize; + t["requestExit"] = &trussc::App::requestExit; + t["isExitRequested"] = &trussc::App::isExitRequested; + t["keyPressed"] = sol::overload([](trussc::App& self, const trussc::KeyEventArgs & e) { return self.keyPressed(e); }, [](trussc::App& self, int key) { return self.keyPressed(key); }); + t["keyReleased"] = sol::overload([](trussc::App& self, const trussc::KeyEventArgs & e) { return self.keyReleased(e); }, [](trussc::App& self, int key) { return self.keyReleased(key); }); + t["mousePressed"] = sol::overload([](trussc::App& self, const trussc::MouseEventArgs & e) { return self.mousePressed(e); }, [](trussc::App& self, trussc::Vec2 pos, int button) { return self.mousePressed(pos, button); }); + t["mouseReleased"] = sol::overload([](trussc::App& self, const trussc::MouseEventArgs & e) { return self.mouseReleased(e); }, [](trussc::App& self, trussc::Vec2 pos, int button) { return self.mouseReleased(pos, button); }); + t["mouseMoved"] = sol::overload([](trussc::App& self, const trussc::MouseMoveEventArgs & e) { return self.mouseMoved(e); }, [](trussc::App& self, trussc::Vec2 pos) { return self.mouseMoved(pos); }); + t["mouseDragged"] = sol::overload([](trussc::App& self, const trussc::MouseDragEventArgs & e) { return self.mouseDragged(e); }, [](trussc::App& self, trussc::Vec2 pos, int button) { return self.mouseDragged(pos, button); }); + t["mouseScrolled"] = sol::overload([](trussc::App& self, const trussc::ScrollEventArgs & e) { return self.mouseScrolled(e); }, [](trussc::App& self, trussc::Vec2 delta) { return self.mouseScrolled(delta); }); + t["touchPressed"] = &trussc::App::touchPressed; + t["touchMoved"] = &trussc::App::touchMoved; + t["touchReleased"] = &trussc::App::touchReleased; + t["windowResized"] = &trussc::App::windowResized; + t["filesDropped"] = &trussc::App::filesDropped; + t["exit"] = &trussc::App::exit; + t["audioOut"] = &trussc::App::audioOut; + t["audioIn"] = &trussc::App::audioIn; + t["handleKeyPressed"] = &trussc::App::handleKeyPressed; + t["handleKeyReleased"] = &trussc::App::handleKeyReleased; + t["handleMousePressed"] = &trussc::App::handleMousePressed; + t["handleMouseReleased"] = &trussc::App::handleMouseReleased; + t["handleMouseScrolled"] = &trussc::App::handleMouseScrolled; + t["handleWindowResized"] = &trussc::App::handleWindowResized; + t["handleUpdate"] = &trussc::App::handleUpdate; + t["handleDraw"] = &trussc::App::handleDraw; + } +#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) + { + sol::usertype t = lua->new_usertype("TcpClient", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["onConnect"] = &trussc::TcpClient::onConnect; + t["onReceive"] = &trussc::TcpClient::onReceive; + t["onDisconnect"] = &trussc::TcpClient::onDisconnect; + t["onError"] = &trussc::TcpClient::onError; + t["connect"] = &trussc::TcpClient::connect; + t["connectAsync"] = &trussc::TcpClient::connectAsync; + t["disconnect"] = &trussc::TcpClient::disconnect; + t["isConnected"] = &trussc::TcpClient::isConnected; + t["send"] = sol::overload([](trussc::TcpClient& self, const std::vector & data) { return self.send(data); }, [](trussc::TcpClient& self, const std::string & message) { return self.send(message); }); + t["setReceiveBufferSize"] = &trussc::TcpClient::setReceiveBufferSize; + t["setBlocking"] = &trussc::TcpClient::setBlocking; + t["setUseThread"] = &trussc::TcpClient::setUseThread; + t["isUsingThread"] = &trussc::TcpClient::isUsingThread; + t["processNetwork"] = &trussc::TcpClient::processNetwork; + t["getRemoteHost"] = &trussc::TcpClient::getRemoteHost; + t["getRemotePort"] = &trussc::TcpClient::getRemotePort; + } +#endif + { + sol::usertype t = lua->new_usertype("FileWriter", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["open"] = sol::overload([](trussc::FileWriter& self, const fs::path & path) { return self.open(path); }, [](trussc::FileWriter& self, const fs::path & path, bool append) { return self.open(path, append); }); + t["close"] = &trussc::FileWriter::close; + t["isOpen"] = &trussc::FileWriter::isOpen; + t["write"] = sol::overload([](trussc::FileWriter& self, const std::string & text) -> decltype(auto) { return self.write(text); }, [](trussc::FileWriter& self, char c) -> decltype(auto) { return self.write(c); }); + t["writeLine"] = sol::overload([](trussc::FileWriter& self) -> decltype(auto) { return self.writeLine(); }, [](trussc::FileWriter& self, const std::string & text) -> decltype(auto) { return self.writeLine(text); }); + t["flush"] = &trussc::FileWriter::flush; + } + { + sol::usertype t = lua->new_usertype("ScrollEventArgs"); + t["scrollX"] = &trussc::ScrollEventArgs::scrollX; + t["scrollY"] = &trussc::ScrollEventArgs::scrollY; + t["shift"] = &trussc::ScrollEventArgs::shift; + t["ctrl"] = &trussc::ScrollEventArgs::ctrl; + t["alt"] = &trussc::ScrollEventArgs::alt; + t["super"] = &trussc::ScrollEventArgs::super; + t["pos"] = &trussc::ScrollEventArgs::pos; + t["globalPos"] = &trussc::ScrollEventArgs::globalPos; + t["scroll"] = &trussc::ScrollEventArgs::scroll; + t["consumed"] = &trussc::ScrollEventArgs::consumed; + t["syncLegacy"] = &trussc::ScrollEventArgs::syncLegacy; + } + { + sol::usertype t = lua->new_usertype("JsonReadReflector", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["applied"] = &trussc::JsonReadReflector::applied; + t["skipped"] = &trussc::JsonReadReflector::skipped; + t["readOnly"] = &trussc::JsonReadReflector::readOnly; + t["unknownKeys"] = &trussc::JsonReadReflector::unknownKeys; + t["endGroup"] = &trussc::JsonReadReflector::endGroup; + } + lua->new_usertype("LogLevel", + sol::meta_function::equal_to, [](trussc::LogLevel a, trussc::LogLevel b){ return a == b; }, + "Verbose", sol::var(trussc::LogLevel::Verbose), + "Notice", sol::var(trussc::LogLevel::Notice), + "Warning", sol::var(trussc::LogLevel::Warning), + "Error", sol::var(trussc::LogLevel::Error), + "Fatal", sol::var(trussc::LogLevel::Fatal), + "Silent", sol::var(trussc::LogLevel::Silent)); + { + sol::usertype t = lua->new_usertype("AudioInBuffer"); + t["frameCount"] = &trussc::AudioInBuffer::frameCount; + t["channels"] = &trussc::AudioInBuffer::channels; + t["sampleRate"] = &trussc::AudioInBuffer::sampleRate; + t["framePosition"] = &trussc::AudioInBuffer::framePosition; + } + { + sol::usertype t = lua->new_usertype("FileDialogResult"); + t["filePath"] = &trussc::FileDialogResult::filePath; + t["fileName"] = &trussc::FileDialogResult::fileName; + t["success"] = &trussc::FileDialogResult::success; + } + { + sol::usertype t = lua->new_usertype("HeadlessSettings"); + t["targetFps"] = &trussc::HeadlessSettings::targetFps; + t["setFps"] = &trussc::HeadlessSettings::setFps; + } +} +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#pragma clang diagnostic pop +#endif diff --git a/addons/tcxLua/src/generated/trussctype_generated_10.cpp b/addons/tcxLua/src/generated/trussctype_generated_10.cpp new file mode 100644 index 00000000..873787dd --- /dev/null +++ b/addons/tcxLua/src/generated/trussctype_generated_10.cpp @@ -0,0 +1,129 @@ +// AUTO-GENERATED usertype bindings from reference-data.json by luagen-types.js +#include "tcxLua.h" +#include "TrussC.h" +using namespace trussc; +using namespace std; +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma clang diagnostic push +#endif +void tcxLuaGenShard_10(const std::shared_ptr& lua) { +#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) || defined(__EMSCRIPTEN__) + { + sol::usertype t = lua->new_usertype("VideoGrabber", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["listDevices"] = &trussc::VideoGrabber::listDevices; + t["setDeviceID"] = &trussc::VideoGrabber::setDeviceID; + t["getDeviceID"] = &trussc::VideoGrabber::getDeviceID; + t["setDesiredFrameRate"] = &trussc::VideoGrabber::setDesiredFrameRate; + t["getDesiredFrameRate"] = &trussc::VideoGrabber::getDesiredFrameRate; + t["setVerbose"] = &trussc::VideoGrabber::setVerbose; + t["isVerbose"] = &trussc::VideoGrabber::isVerbose; + t["setup"] = sol::overload([](trussc::VideoGrabber& self) { return self.setup(); }, [](trussc::VideoGrabber& self, int width) { return self.setup(width); }, [](trussc::VideoGrabber& self, int width, int height) { return self.setup(width, height); }); + t["close"] = &trussc::VideoGrabber::close; + t["update"] = &trussc::VideoGrabber::update; + t["isFrameNew"] = &trussc::VideoGrabber::isFrameNew; + t["isInitialized"] = &trussc::VideoGrabber::isInitialized; + t["isPendingPermission"] = &trussc::VideoGrabber::isPendingPermission; + t["getWidth"] = &trussc::VideoGrabber::getWidth; + t["getHeight"] = &trussc::VideoGrabber::getHeight; + t["getDeviceName"] = &trussc::VideoGrabber::getDeviceName; + t["getPixels"] = [](trussc::VideoGrabber& self) { return self.getPixels(); }; +#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) + t["setFrameQueueSize"] = &trussc::VideoGrabber::setFrameQueueSize; +#endif +#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) + t["getFrameQueueSize"] = &trussc::VideoGrabber::getFrameQueueSize; +#endif + t["copyToImage"] = &trussc::VideoGrabber::copyToImage; + t["getTexture"] = [](trussc::VideoGrabber& self) -> decltype(auto) { return self.getTexture(); }; + t["checkCameraPermission"] = &trussc::VideoGrabber::checkCameraPermission; + t["requestCameraPermission"] = &trussc::VideoGrabber::requestCameraPermission; + } +#endif +#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) + { + sol::usertype t = lua->new_usertype("ScreenRecorder", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["start"] = sol::overload([](trussc::ScreenRecorder& self, const fs::path & path) { return self.start(path); }, [](trussc::ScreenRecorder& self, const fs::path & path, const trussc::VideoRecordSettings & settings) { return self.start(path, settings); }, [](trussc::ScreenRecorder& self, const trussc::Fbo & fbo, const fs::path & path) { return self.start(fbo, path); }, [](trussc::ScreenRecorder& self, const trussc::Fbo & fbo, const fs::path & path, const trussc::VideoRecordSettings & settings) { return self.start(fbo, path, settings); }, [](trussc::ScreenRecorder& self, const fs::path & path, float durationSec) { return self.start(path, durationSec); }, [](trussc::ScreenRecorder& self, const trussc::Fbo & fbo, const fs::path & path, float durationSec) { return self.start(fbo, path, durationSec); }); + t["stop"] = &trussc::ScreenRecorder::stop; + t["isRecording"] = &trussc::ScreenRecorder::isRecording; + t["getFrameCount"] = &trussc::ScreenRecorder::getFrameCount; + t["getPath"] = &trussc::ScreenRecorder::getPath; + t["writer"] = &trussc::ScreenRecorder::writer; + } +#endif + { + sol::usertype t = lua->new_usertype("ColorOKLCH", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["L"] = &trussc::ColorOKLCH::L; + t["C"] = &trussc::ColorOKLCH::C; + t["H"] = &trussc::ColorOKLCH::H; + t["alpha"] = &trussc::ColorOKLCH::alpha; + t["toOKLab"] = &trussc::ColorOKLCH::toOKLab; + t["toLinear"] = &trussc::ColorOKLCH::toLinear; + t["toRGB"] = &trussc::ColorOKLCH::toRGB; + t["toHSB"] = &trussc::ColorOKLCH::toHSB; + t["lerp"] = sol::overload([](trussc::ColorOKLCH& self, const trussc::ColorOKLCH & target, float t) { return self.lerp(target, t); }, [](trussc::ColorOKLCH& self, const trussc::ColorOKLCH & target, float t, bool shortestPath) { return self.lerp(target, t, shortestPath); }); + } + { + sol::usertype t = lua->new_usertype("Ray", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["origin"] = &trussc::Ray::origin; + t["direction"] = &trussc::Ray::direction; + t["at"] = &trussc::Ray::at; + t["transformed"] = &trussc::Ray::transformed; + t["fromScreenPoint2D"] = sol::overload([](float screenX, float screenY) { return trussc::Ray::fromScreenPoint2D(screenX, screenY); }, [](float screenX, float screenY, float startZ) { return trussc::Ray::fromScreenPoint2D(screenX, screenY, startZ); }); + } + { + sol::usertype t = lua->new_usertype("Platform"); + t["isWeb"] = &trussc::Platform::isWeb; + t["isMacOS"] = &trussc::Platform::isMacOS; + t["isIOS"] = &trussc::Platform::isIOS; + t["isWindows"] = &trussc::Platform::isWindows; + t["isAndroid"] = &trussc::Platform::isAndroid; + t["isLinux"] = &trussc::Platform::isLinux; + t["isApple"] = &trussc::Platform::isApple; + t["isMobile"] = &trussc::Platform::isMobile; + t["isDesktop"] = &trussc::Platform::isDesktop; + t["name"] = &trussc::Platform::name; + } + lua->new_usertype("Direction", + sol::meta_function::equal_to, [](trussc::Direction a, trussc::Direction b){ return a == b; }, + "Left", sol::var(trussc::Direction::Left), + "Center", sol::var(trussc::Direction::Center), + "Right", sol::var(trussc::Direction::Right), + "Top", sol::var(trussc::Direction::Top), + "Bottom", sol::var(trussc::Direction::Bottom), + "Baseline", sol::var(trussc::Direction::Baseline)); + { + sol::usertype t = lua->new_usertype("AudioOutBuffer"); + t["frameCount"] = &trussc::AudioOutBuffer::frameCount; + t["channels"] = &trussc::AudioOutBuffer::channels; + t["sampleRate"] = &trussc::AudioOutBuffer::sampleRate; + t["framePosition"] = &trussc::AudioOutBuffer::framePosition; + } + lua->new_usertype("StrokeCap", + sol::meta_function::equal_to, [](trussc::StrokeCap a, trussc::StrokeCap b){ return a == b; }, + "Butt", sol::var(trussc::StrokeCap::Butt), + "Round", sol::var(trussc::StrokeCap::Round), + "Square", sol::var(trussc::StrokeCap::Square)); + { + sol::usertype t = lua->new_usertype("CurveStyle"); + t["mode"] = &trussc::CurveStyle::mode; + t["tolerance"] = &trussc::CurveStyle::tolerance; + t["resolution"] = &trussc::CurveStyle::resolution; + } + { + sol::usertype t = lua->new_usertype("TcpReceiveEventArgs"); + t["data"] = &trussc::TcpReceiveEventArgs::data; + } +} +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#pragma clang diagnostic pop +#endif diff --git a/addons/tcxLua/src/generated/trussctype_generated_11.cpp b/addons/tcxLua/src/generated/trussctype_generated_11.cpp new file mode 100644 index 00000000..082bc999 --- /dev/null +++ b/addons/tcxLua/src/generated/trussctype_generated_11.cpp @@ -0,0 +1,133 @@ +// AUTO-GENERATED usertype bindings from reference-data.json by luagen-types.js +#include "tcxLua.h" +#include "TrussC.h" +using namespace trussc; +using namespace std; +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma clang diagnostic push +#endif +void tcxLuaGenShard_11(const std::shared_ptr& lua) { + { + sol::usertype t = lua->new_usertype("Vec2", + sol::constructors(), + sol::call_constructor, sol::constructors(), + sol::meta_function::index, [](const trussc::Vec2& a, int b){ return a[b]; }, + sol::meta_function::addition, [](const trussc::Vec2& a, const trussc::Vec2 & b){ return a + b; }, + sol::meta_function::subtraction, [](const trussc::Vec2& a, const trussc::Vec2 & b){ return a - b; }, + sol::meta_function::unary_minus, [](const trussc::Vec2& a){ return -a; }, + sol::meta_function::multiplication, sol::overload([](const trussc::Vec2& a, float b){ return a * b; }, [](const trussc::Vec2& a, const trussc::Vec2 & b){ return a * b; }), + sol::meta_function::division, sol::overload([](const trussc::Vec2& a, float b){ return a / b; }, [](const trussc::Vec2& a, const trussc::Vec2 & b){ return a / b; }), + sol::meta_function::equal_to, [](const trussc::Vec2& a, const trussc::Vec2 & b){ return a == b; }); + t["x"] = &trussc::Vec2::x; + t["y"] = &trussc::Vec2::y; + t["set"] = sol::overload([](trussc::Vec2& self, float x_, float y_) -> decltype(auto) { return self.set(x_, y_); }, [](trussc::Vec2& self, const trussc::Vec2 & v) -> decltype(auto) { return self.set(v); }); + t["length"] = &trussc::Vec2::length; + t["lengthSquared"] = &trussc::Vec2::lengthSquared; + t["normalized"] = &trussc::Vec2::normalized; + t["normalize"] = &trussc::Vec2::normalize; + t["limit"] = &trussc::Vec2::limit; + t["dot"] = &trussc::Vec2::dot; + t["cross"] = &trussc::Vec2::cross; + t["distance"] = &trussc::Vec2::distance; + t["distanceSquared"] = &trussc::Vec2::distanceSquared; + t["angle"] = sol::overload([](trussc::Vec2& self) { return self.angle(); }, [](trussc::Vec2& self, const trussc::Vec2 & v) { return self.angle(v); }); + t["rotated"] = &trussc::Vec2::rotated; + t["rotate"] = &trussc::Vec2::rotate; + t["lerp"] = &trussc::Vec2::lerp; + t["perpendicular"] = &trussc::Vec2::perpendicular; + t["reflected"] = &trussc::Vec2::reflected; + t["fromAngle"] = sol::overload([](float radians) { return trussc::Vec2::fromAngle(radians); }, [](float radians, float length) { return trussc::Vec2::fromAngle(radians, length); }); + } + lua->new_usertype("Cursor", + sol::meta_function::equal_to, [](trussc::Cursor a, trussc::Cursor b){ return a == b; }, + "Default", sol::var(trussc::Cursor::Default), + "Arrow", sol::var(trussc::Cursor::Arrow), + "IBeam", sol::var(trussc::Cursor::IBeam), + "Crosshair", sol::var(trussc::Cursor::Crosshair), + "Hand", sol::var(trussc::Cursor::Hand), + "ResizeEW", sol::var(trussc::Cursor::ResizeEW), + "ResizeNS", sol::var(trussc::Cursor::ResizeNS), + "ResizeNWSE", sol::var(trussc::Cursor::ResizeNWSE), + "ResizeNESW", sol::var(trussc::Cursor::ResizeNESW), + "ResizeAll", sol::var(trussc::Cursor::ResizeAll), + "NotAllowed", sol::var(trussc::Cursor::NotAllowed), + "Custom0", sol::var(trussc::Cursor::Custom0), + "Custom1", sol::var(trussc::Cursor::Custom1), + "Custom2", sol::var(trussc::Cursor::Custom2), + "Custom3", sol::var(trussc::Cursor::Custom3), + "Custom4", sol::var(trussc::Cursor::Custom4), + "Custom5", sol::var(trussc::Cursor::Custom5), + "Custom6", sol::var(trussc::Cursor::Custom6), + "Custom7", sol::var(trussc::Cursor::Custom7), + "Custom8", sol::var(trussc::Cursor::Custom8), + "Custom9", sol::var(trussc::Cursor::Custom9), + "Custom10", sol::var(trussc::Cursor::Custom10), + "Custom11", sol::var(trussc::Cursor::Custom11), + "Custom12", sol::var(trussc::Cursor::Custom12), + "Custom13", sol::var(trussc::Cursor::Custom13), + "Custom14", sol::var(trussc::Cursor::Custom14), + "Custom15", sol::var(trussc::Cursor::Custom15)); + { + sol::usertype t = lua->new_usertype("ChipSoundNote", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["wave"] = &trussc::ChipSoundNote::wave; + t["hz"] = &trussc::ChipSoundNote::hz; + t["volume"] = &trussc::ChipSoundNote::volume; + t["duration"] = &trussc::ChipSoundNote::duration; + t["attack"] = &trussc::ChipSoundNote::attack; + t["decay"] = &trussc::ChipSoundNote::decay; + t["sustain"] = &trussc::ChipSoundNote::sustain; + t["release"] = &trussc::ChipSoundNote::release; + t["build"] = &trussc::ChipSoundNote::build; + t["generateBuffer"] = &trussc::ChipSoundNote::generateBuffer; + t["getTotalDuration"] = &trussc::ChipSoundNote::getTotalDuration; + } + { + sol::usertype> t = lua->new_usertype>("Tween_Vec2", + sol::constructors(), trussc::Tween(trussc::Vec2, trussc::Vec2, float), trussc::Tween(trussc::Vec2, trussc::Vec2, float, trussc::EaseType), trussc::Tween(trussc::Vec2, trussc::Vec2, float, trussc::EaseType, trussc::EaseMode)>(), + sol::call_constructor, sol::constructors(), trussc::Tween(trussc::Vec2, trussc::Vec2, float), trussc::Tween(trussc::Vec2, trussc::Vec2, float, trussc::EaseType), trussc::Tween(trussc::Vec2, trussc::Vec2, float, trussc::EaseType, trussc::EaseMode)>()); + } + lua->new_usertype("Orientation", + sol::meta_function::equal_to, [](trussc::Orientation a, trussc::Orientation b){ return a == b; }, + "Portrait", sol::var(trussc::Orientation::Portrait), + "PortraitUpsideDown", sol::var(trussc::Orientation::PortraitUpsideDown), + "LandscapeLeft", sol::var(trussc::Orientation::LandscapeLeft), + "LandscapeRight", sol::var(trussc::Orientation::LandscapeRight), + "Landscape", sol::var(trussc::Orientation::Landscape), + "All", sol::var(trussc::Orientation::All), + "AllButUpsideDown", sol::var(trussc::Orientation::AllButUpsideDown)); + { + sol::usertype t = lua->new_usertype("ShaderVertex"); + t["x"] = &trussc::ShaderVertex::x; + t["y"] = &trussc::ShaderVertex::y; + t["z"] = &trussc::ShaderVertex::z; + t["u"] = &trussc::ShaderVertex::u; + t["v"] = &trussc::ShaderVertex::v; + t["r"] = &trussc::ShaderVertex::r; + t["g"] = &trussc::ShaderVertex::g; + t["b"] = &trussc::ShaderVertex::b; + t["a"] = &trussc::ShaderVertex::a; + } + lua->new_usertype("VideoCodec", + sol::meta_function::equal_to, [](trussc::VideoCodec a, trussc::VideoCodec b){ return a == b; }, + "H264", sol::var(trussc::VideoCodec::H264), + "HEVC", sol::var(trussc::VideoCodec::HEVC), + "ProRes422", sol::var(trussc::VideoCodec::ProRes422), + "ProRes4444", sol::var(trussc::VideoCodec::ProRes4444)); + lua->new_usertype("LightType", + sol::meta_function::equal_to, [](trussc::LightType a, trussc::LightType b){ return a == b; }, + "Directional", sol::var(trussc::LightType::Directional), + "Point", sol::var(trussc::LightType::Point), + "Spot", sol::var(trussc::LightType::Spot)); + { + sol::usertype t = lua->new_usertype("TcpServerReceiveEventArgs"); + t["clientId"] = &trussc::TcpServerReceiveEventArgs::clientId; + t["data"] = &trussc::TcpServerReceiveEventArgs::data; + } +} +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#pragma clang diagnostic pop +#endif diff --git a/addons/tcxLua/src/generated/trussctype_generated_12.cpp b/addons/tcxLua/src/generated/trussctype_generated_12.cpp new file mode 100644 index 00000000..e717fbf9 --- /dev/null +++ b/addons/tcxLua/src/generated/trussctype_generated_12.cpp @@ -0,0 +1,135 @@ +// AUTO-GENERATED usertype bindings from reference-data.json by luagen-types.js +#include "tcxLua.h" +#include "TrussC.h" +using namespace trussc; +using namespace std; +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma clang diagnostic push +#endif +void tcxLuaGenShard_12(const std::shared_ptr& lua) { + { + sol::usertype t = lua->new_usertype("Vec4", + sol::constructors(), + sol::call_constructor, sol::constructors(), + sol::meta_function::index, [](const trussc::Vec4& a, int b){ return a[b]; }, + sol::meta_function::addition, [](const trussc::Vec4& a, const trussc::Vec4 & b){ return a + b; }, + sol::meta_function::subtraction, [](const trussc::Vec4& a, const trussc::Vec4 & b){ return a - b; }, + sol::meta_function::unary_minus, [](const trussc::Vec4& a){ return -a; }, + sol::meta_function::multiplication, sol::overload([](const trussc::Vec4& a, const trussc::Vec4 & b){ return a * b; }, [](const trussc::Vec4& a, float b){ return a * b; }), + sol::meta_function::division, sol::overload([](const trussc::Vec4& a, const trussc::Vec4 & b){ return a / b; }, [](const trussc::Vec4& a, float b){ return a / b; }), + sol::meta_function::equal_to, [](const trussc::Vec4& a, const trussc::Vec4 & b){ return a == b; }); + t["x"] = &trussc::Vec4::x; + t["y"] = &trussc::Vec4::y; + t["z"] = &trussc::Vec4::z; + t["w"] = &trussc::Vec4::w; + t["set"] = sol::overload([](trussc::Vec4& self, float x_, float y_, float z_, float w_) -> decltype(auto) { return self.set(x_, y_, z_, w_); }, [](trussc::Vec4& self, const trussc::Vec4 & v) -> decltype(auto) { return self.set(v); }); + t["length"] = &trussc::Vec4::length; + t["lengthSquared"] = &trussc::Vec4::lengthSquared; + t["normalized"] = &trussc::Vec4::normalized; + t["normalize"] = &trussc::Vec4::normalize; + t["dot"] = &trussc::Vec4::dot; + t["lerp"] = &trussc::Vec4::lerp; + t["xy"] = &trussc::Vec4::xy; + t["xyz"] = &trussc::Vec4::xyz; + } + { + sol::usertype t = lua->new_usertype("ScrollContainer", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["setContent"] = &trussc::ScrollContainer::setContent; + t["getContent"] = &trussc::ScrollContainer::getContent; + t["getContentRect"] = &trussc::ScrollContainer::getContentRect; + t["getScrollX"] = &trussc::ScrollContainer::getScrollX; + t["getScrollY"] = &trussc::ScrollContainer::getScrollY; + t["getScroll"] = &trussc::ScrollContainer::getScroll; + t["setScrollX"] = &trussc::ScrollContainer::setScrollX; + t["setScrollY"] = &trussc::ScrollContainer::setScrollY; + t["setScroll"] = sol::overload([](trussc::ScrollContainer& self, float x, float y) { return self.setScroll(x, y); }, [](trussc::ScrollContainer& self, trussc::Vec2 pos) { return self.setScroll(pos); }); + t["getMaxScrollX"] = &trussc::ScrollContainer::getMaxScrollX; + t["getMaxScrollY"] = &trussc::ScrollContainer::getMaxScrollY; + t["updateScrollBounds"] = &trussc::ScrollContainer::updateScrollBounds; + t["isHorizontalScrollEnabled"] = &trussc::ScrollContainer::isHorizontalScrollEnabled; + t["isVerticalScrollEnabled"] = &trussc::ScrollContainer::isVerticalScrollEnabled; + t["setHorizontalScrollEnabled"] = &trussc::ScrollContainer::setHorizontalScrollEnabled; + t["setVerticalScrollEnabled"] = &trussc::ScrollContainer::setVerticalScrollEnabled; + t["getScrollSpeed"] = &trussc::ScrollContainer::getScrollSpeed; + t["setScrollSpeed"] = &trussc::ScrollContainer::setScrollSpeed; + } + { + sol::usertype t = lua->new_usertype("HasTexture"); + t["getTexture"] = [](trussc::HasTexture& self) -> decltype(auto) { return self.getTexture(); }; + t["hasTexture"] = &trussc::HasTexture::hasTexture; + t["draw"] = sol::overload([](trussc::HasTexture& self, float x, float y) { return self.draw(x, y); }, [](trussc::HasTexture& self, float x, float y, float w, float h) { return self.draw(x, y, w, h); }); + t["setMinFilter"] = &trussc::HasTexture::setMinFilter; + t["setMagFilter"] = &trussc::HasTexture::setMagFilter; + t["setFilter"] = &trussc::HasTexture::setFilter; + t["getMinFilter"] = &trussc::HasTexture::getMinFilter; + t["getMagFilter"] = &trussc::HasTexture::getMagFilter; + t["setWrapU"] = &trussc::HasTexture::setWrapU; + t["setWrapV"] = &trussc::HasTexture::setWrapV; + t["setWrap"] = &trussc::HasTexture::setWrap; + t["getWrapU"] = &trussc::HasTexture::getWrapU; + t["getWrapV"] = &trussc::HasTexture::getWrapV; + t["save"] = &trussc::HasTexture::save; + } + lua->new_usertype("TextureFormat", + sol::meta_function::equal_to, [](trussc::TextureFormat a, trussc::TextureFormat b){ return a == b; }, + "RGBA8", sol::var(trussc::TextureFormat::RGBA8), + "RGBA16F", sol::var(trussc::TextureFormat::RGBA16F), + "RGBA32F", sol::var(trussc::TextureFormat::RGBA32F), + "R8", sol::var(trussc::TextureFormat::R8), + "R16F", sol::var(trussc::TextureFormat::R16F), + "R32F", sol::var(trussc::TextureFormat::R32F), + "RG8", sol::var(trussc::TextureFormat::RG8), + "RG16F", sol::var(trussc::TextureFormat::RG16F), + "RG32F", sol::var(trussc::TextureFormat::RG32F), + "BGRA8", sol::var(trussc::TextureFormat::BGRA8), + "RGBA16", sol::var(trussc::TextureFormat::RGBA16)); + { + sol::usertype t = lua->new_usertype("RectNodeButton", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["normalColor"] = &trussc::RectNodeButton::normalColor; + t["hoverColor"] = &trussc::RectNodeButton::hoverColor; + t["pressColor"] = &trussc::RectNodeButton::pressColor; + t["label"] = &trussc::RectNodeButton::label; + t["isPressed"] = &trussc::RectNodeButton::isPressed; + t["draw"] = &trussc::RectNodeButton::draw; + } + { + sol::usertype t = lua->new_usertype("GraphicsBackend"); + t["isWebGPU"] = &trussc::GraphicsBackend::isWebGPU; + t["isWebGL2"] = &trussc::GraphicsBackend::isWebGL2; + t["isMetal"] = &trussc::GraphicsBackend::isMetal; + t["isD3D11"] = &trussc::GraphicsBackend::isD3D11; + t["isVulkan"] = &trussc::GraphicsBackend::isVulkan; + t["isOpenGL"] = &trussc::GraphicsBackend::isOpenGL; + t["name"] = &trussc::GraphicsBackend::name; + } + lua->new_usertype("MouseButton", + sol::meta_function::equal_to, [](trussc::MouseButton a, trussc::MouseButton b){ return a == b; }, + "Left", sol::var(trussc::MouseButton::Left), + "Right", sol::var(trussc::MouseButton::Right), + "Middle", sol::var(trussc::MouseButton::Middle), + "None", sol::var(trussc::MouseButton::None)); + { + sol::usertype t = lua->new_usertype("Location"); + t["latitude"] = &trussc::Location::latitude; + t["longitude"] = &trussc::Location::longitude; + t["altitude"] = &trussc::Location::altitude; + t["accuracy"] = &trussc::Location::accuracy; + } + lua->new_usertype("ImageType", + sol::meta_function::equal_to, [](trussc::ImageType a, trussc::ImageType b){ return a == b; }, + "Color", sol::var(trussc::ImageType::Color), + "Grayscale", sol::var(trussc::ImageType::Grayscale)); + { + sol::usertype t = lua->new_usertype("ExitRequestEventArgs"); + t["cancel"] = &trussc::ExitRequestEventArgs::cancel; + } +} +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#pragma clang diagnostic pop +#endif diff --git a/addons/tcxLua/src/generated/trussctype_generated_13.cpp b/addons/tcxLua/src/generated/trussctype_generated_13.cpp new file mode 100644 index 00000000..1fce7cde --- /dev/null +++ b/addons/tcxLua/src/generated/trussctype_generated_13.cpp @@ -0,0 +1,123 @@ +// AUTO-GENERATED usertype bindings from reference-data.json by luagen-types.js +#include "tcxLua.h" +#include "TrussC.h" +using namespace trussc; +using namespace std; +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma clang diagnostic push +#endif +void tcxLuaGenShard_13(const std::shared_ptr& lua) { + { + sol::usertype t = lua->new_usertype("Vec3", + sol::constructors(), + sol::call_constructor, sol::constructors(), + sol::meta_function::index, [](const trussc::Vec3& a, int b){ return a[b]; }, + sol::meta_function::addition, [](const trussc::Vec3& a, const trussc::Vec3 & b){ return a + b; }, + sol::meta_function::subtraction, [](const trussc::Vec3& a, const trussc::Vec3 & b){ return a - b; }, + sol::meta_function::unary_minus, [](const trussc::Vec3& a){ return -a; }, + sol::meta_function::multiplication, sol::overload([](const trussc::Vec3& a, float b){ return a * b; }, [](const trussc::Vec3& a, const trussc::Vec3 & b){ return a * b; }), + sol::meta_function::division, sol::overload([](const trussc::Vec3& a, float b){ return a / b; }, [](const trussc::Vec3& a, const trussc::Vec3 & b){ return a / b; }), + sol::meta_function::equal_to, [](const trussc::Vec3& a, const trussc::Vec3 & b){ return a == b; }); + t["x"] = &trussc::Vec3::x; + t["y"] = &trussc::Vec3::y; + t["z"] = &trussc::Vec3::z; + t["set"] = sol::overload([](trussc::Vec3& self, float x_, float y_, float z_) -> decltype(auto) { return self.set(x_, y_, z_); }, [](trussc::Vec3& self, const trussc::Vec3 & v) -> decltype(auto) { return self.set(v); }); + t["length"] = &trussc::Vec3::length; + t["lengthSquared"] = &trussc::Vec3::lengthSquared; + t["normalized"] = &trussc::Vec3::normalized; + t["normalize"] = &trussc::Vec3::normalize; + t["limit"] = &trussc::Vec3::limit; + t["dot"] = &trussc::Vec3::dot; + t["cross"] = &trussc::Vec3::cross; + t["distance"] = &trussc::Vec3::distance; + t["distanceSquared"] = &trussc::Vec3::distanceSquared; + t["lerp"] = &trussc::Vec3::lerp; + t["reflected"] = &trussc::Vec3::reflected; + t["xy"] = &trussc::Vec3::xy; + } + { + sol::usertype t = lua->new_usertype("ColorLinear", + sol::constructors(), + sol::call_constructor, sol::constructors(), + sol::meta_function::addition, [](const trussc::ColorLinear& a, const trussc::ColorLinear & b){ return a + b; }, + sol::meta_function::subtraction, [](const trussc::ColorLinear& a, const trussc::ColorLinear & b){ return a - b; }, + sol::meta_function::multiplication, sol::overload([](const trussc::ColorLinear& a, float b){ return a * b; }, [](const trussc::ColorLinear& a, const trussc::ColorLinear & b){ return a * b; }), + sol::meta_function::division, [](const trussc::ColorLinear& a, float b){ return a / b; }, + sol::meta_function::equal_to, [](const trussc::ColorLinear& a, const trussc::ColorLinear & b){ return a == b; }); + t["r"] = &trussc::ColorLinear::r; + t["g"] = &trussc::ColorLinear::g; + t["b"] = &trussc::ColorLinear::b; + t["a"] = &trussc::ColorLinear::a; + t["toSRGB"] = &trussc::ColorLinear::toSRGB; + t["toHSB"] = &trussc::ColorLinear::toHSB; + t["toOKLab"] = &trussc::ColorLinear::toOKLab; + t["toOKLCH"] = &trussc::ColorLinear::toOKLCH; + t["clamped"] = &trussc::ColorLinear::clamped; + t["clampedLDR"] = &trussc::ColorLinear::clampedLDR; + t["lerp"] = &trussc::ColorLinear::lerp; + } + { + sol::usertype t = lua->new_usertype("IVec3", + sol::constructors(), + sol::call_constructor, sol::constructors(), + sol::meta_function::addition, [](const trussc::IVec3& a, const trussc::IVec3 & b){ return a + b; }, + sol::meta_function::subtraction, [](const trussc::IVec3& a, const trussc::IVec3 & b){ return a - b; }, + sol::meta_function::unary_minus, [](const trussc::IVec3& a){ return -a; }, + sol::meta_function::multiplication, [](const trussc::IVec3& a, int b){ return a * b; }, + sol::meta_function::equal_to, [](const trussc::IVec3& a, const trussc::IVec3 & b){ return a == b; }); + t["x"] = &trussc::IVec3::x; + t["y"] = &trussc::IVec3::y; + t["z"] = &trussc::IVec3::z; + t["toVec3"] = &trussc::IVec3::toVec3; + t["xy"] = &trussc::IVec3::xy; + } + { + sol::usertype> t = lua->new_usertype>("Tween_Vec3", + sol::constructors(), trussc::Tween(trussc::Vec3, trussc::Vec3, float), trussc::Tween(trussc::Vec3, trussc::Vec3, float, trussc::EaseType), trussc::Tween(trussc::Vec3, trussc::Vec3, float, trussc::EaseType, trussc::EaseMode)>(), + sol::call_constructor, sol::constructors(), trussc::Tween(trussc::Vec3, trussc::Vec3, float), trussc::Tween(trussc::Vec3, trussc::Vec3, float, trussc::EaseType), trussc::Tween(trussc::Vec3, trussc::Vec3, float, trussc::EaseType, trussc::EaseMode)>()); + } + lua->new_usertype("PrimitiveMode", + sol::meta_function::equal_to, [](trussc::PrimitiveMode a, trussc::PrimitiveMode b){ return a == b; }, + "Triangles", sol::var(trussc::PrimitiveMode::Triangles), + "TriangleStrip", sol::var(trussc::PrimitiveMode::TriangleStrip), + "TriangleFan", sol::var(trussc::PrimitiveMode::TriangleFan), + "Lines", sol::var(trussc::PrimitiveMode::Lines), + "LineStrip", sol::var(trussc::PrimitiveMode::LineStrip), + "LineLoop", sol::var(trussc::PrimitiveMode::LineLoop), + "Points", sol::var(trussc::PrimitiveMode::Points)); + { + sol::usertype t = lua->new_usertype("SerialDeviceInfo"); + t["deviceId"] = &trussc::SerialDeviceInfo::deviceId; + t["devicePath"] = &trussc::SerialDeviceInfo::devicePath; + t["deviceName"] = &trussc::SerialDeviceInfo::deviceName; + t["getDeviceID"] = &trussc::SerialDeviceInfo::getDeviceID; + t["getDevicePath"] = &trussc::SerialDeviceInfo::getDevicePath; + t["getDeviceName"] = &trussc::SerialDeviceInfo::getDeviceName; + } + { + sol::usertype t = lua->new_usertype("TouchEventArgs"); + t["numTouches"] = &trussc::TouchEventArgs::numTouches; + t["cancelled"] = &trussc::TouchEventArgs::cancelled; + t["x"] = &trussc::TouchEventArgs::x; + t["y"] = &trussc::TouchEventArgs::y; + t["id"] = &trussc::TouchEventArgs::id; + } + lua->new_usertype("PointStyle", + sol::meta_function::equal_to, [](trussc::PointStyle a, trussc::PointStyle b){ return a == b; }, + "Square", sol::var(trussc::PointStyle::Square), + "Round", sol::var(trussc::PointStyle::Round), + "Pixel", sol::var(trussc::PointStyle::Pixel)); + lua->new_usertype("PixelFormat", + sol::meta_function::equal_to, [](trussc::PixelFormat a, trussc::PixelFormat b){ return a == b; }, + "U8", sol::var(trussc::PixelFormat::U8), + "F32", sol::var(trussc::PixelFormat::F32)); + lua->new_usertype("Codec", + sol::meta_function::equal_to, [](trussc::Codec a, trussc::Codec b){ return a == b; }, + "None", sol::var(trussc::Codec::None), + "LZ4", sol::var(trussc::Codec::LZ4)); +} +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#pragma clang diagnostic pop +#endif diff --git a/addons/tcxLua/src/generated/trussctype_generated_14.cpp b/addons/tcxLua/src/generated/trussctype_generated_14.cpp new file mode 100644 index 00000000..647c9092 --- /dev/null +++ b/addons/tcxLua/src/generated/trussctype_generated_14.cpp @@ -0,0 +1,136 @@ +// AUTO-GENERATED usertype bindings from reference-data.json by luagen-types.js +#include "tcxLua.h" +#include "TrussC.h" +using namespace trussc; +using namespace std; +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma clang diagnostic push +#endif +void tcxLuaGenShard_14(const std::shared_ptr& lua) { + { + sol::usertype t = lua->new_usertype("LayoutMod", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["getDirection"] = &trussc::LayoutMod::getDirection; + t["setDirection"] = &trussc::LayoutMod::setDirection; + t["getSpacing"] = &trussc::LayoutMod::getSpacing; + t["setSpacing"] = &trussc::LayoutMod::setSpacing; + t["getCrossAxis"] = &trussc::LayoutMod::getCrossAxis; + t["setCrossAxis"] = &trussc::LayoutMod::setCrossAxis; + t["getMainAxis"] = &trussc::LayoutMod::getMainAxis; + t["setMainAxis"] = &trussc::LayoutMod::setMainAxis; + t["getPaddingLeft"] = &trussc::LayoutMod::getPaddingLeft; + t["getPaddingTop"] = &trussc::LayoutMod::getPaddingTop; + t["getPaddingRight"] = &trussc::LayoutMod::getPaddingRight; + t["getPaddingBottom"] = &trussc::LayoutMod::getPaddingBottom; + t["setPadding"] = sol::overload([](trussc::LayoutMod& self, float padding) -> decltype(auto) { return self.setPadding(padding); }, [](trussc::LayoutMod& self, float vertical, float horizontal) -> decltype(auto) { return self.setPadding(vertical, horizontal); }, [](trussc::LayoutMod& self, float top, float right, float bottom, float left) -> decltype(auto) { return self.setPadding(top, right, bottom, left); }); + t["setPaddingLeft"] = &trussc::LayoutMod::setPaddingLeft; + t["setPaddingTop"] = &trussc::LayoutMod::setPaddingTop; + t["setPaddingRight"] = &trussc::LayoutMod::setPaddingRight; + t["setPaddingBottom"] = &trussc::LayoutMod::setPaddingBottom; + t["updateLayout"] = &trussc::LayoutMod::updateLayout; + } + { + sol::usertype t = lua->new_usertype("StrokeMesh", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["setWidth"] = &trussc::StrokeMesh::setWidth; + t["setColor"] = &trussc::StrokeMesh::setColor; + t["setCapType"] = &trussc::StrokeMesh::setCapType; + t["setJoinType"] = &trussc::StrokeMesh::setJoinType; + t["setMiterLimit"] = &trussc::StrokeMesh::setMiterLimit; + t["addVertex"] = sol::overload([](trussc::StrokeMesh& self, float x, float y) -> decltype(auto) { return self.addVertex(x, y); }, [](trussc::StrokeMesh& self, float x, float y, float z) -> decltype(auto) { return self.addVertex(x, y, z); }, [](trussc::StrokeMesh& self, const trussc::Vec3 & p) -> decltype(auto) { return self.addVertex(p); }, [](trussc::StrokeMesh& self, const trussc::Vec2 & p) -> decltype(auto) { return self.addVertex(p); }); + t["addVertexWithWidth"] = sol::overload([](trussc::StrokeMesh& self, float x, float y, float width) -> decltype(auto) { return self.addVertexWithWidth(x, y, width); }, [](trussc::StrokeMesh& self, const trussc::Vec3 & p, float width) -> decltype(auto) { return self.addVertexWithWidth(p, width); }); + t["setWidths"] = &trussc::StrokeMesh::setWidths; + t["setShape"] = &trussc::StrokeMesh::setShape; + t["setClosed"] = &trussc::StrokeMesh::setClosed; + t["clear"] = &trussc::StrokeMesh::clear; + t["update"] = &trussc::StrokeMesh::update; + t["draw"] = &trussc::StrokeMesh::draw; + t["getMesh"] = &trussc::StrokeMesh::getMesh; + t["getPolylines"] = &trussc::StrokeMesh::getPolylines; + } + { + sol::usertype t = lua->new_usertype("RectNode"); + t["mousePressed"] = &trussc::RectNode::mousePressed; + t["mouseReleased"] = &trussc::RectNode::mouseReleased; + t["mouseDragged"] = &trussc::RectNode::mouseDragged; + t["mouseScrolled"] = &trussc::RectNode::mouseScrolled; + t["getWidth"] = &trussc::RectNode::getWidth; + t["getHeight"] = &trussc::RectNode::getHeight; + t["getSize"] = &trussc::RectNode::getSize; + t["setWidth"] = &trussc::RectNode::setWidth; + t["setHeight"] = &trussc::RectNode::setHeight; + t["setSize"] = sol::overload([](trussc::RectNode& self, float w, float h) { return self.setSize(w, h); }, [](trussc::RectNode& self, float size) { return self.setSize(size); }, [](trussc::RectNode& self, const trussc::Vec2 & s) { return self.setSize(s); }); + t["setRect"] = &trussc::RectNode::setRect; + t["setClipping"] = &trussc::RectNode::setClipping; + t["isClipping"] = &trussc::RectNode::isClipping; + t["getLeft"] = &trussc::RectNode::getLeft; + t["getRight"] = &trussc::RectNode::getRight; + t["getTop"] = &trussc::RectNode::getTop; + t["getBottom"] = &trussc::RectNode::getBottom; + t["hitTest"] = [](trussc::RectNode& self, trussc::Vec2 local) { return self.hitTest(local); }; + t["draw"] = &trussc::RectNode::draw; + } + { + sol::usertype t = lua->new_usertype("ColorOKLab", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["L"] = &trussc::ColorOKLab::L; + t["a"] = &trussc::ColorOKLab::a; + t["b"] = &trussc::ColorOKLab::b; + t["alpha"] = &trussc::ColorOKLab::alpha; + t["toLinear"] = &trussc::ColorOKLab::toLinear; + t["toRGB"] = &trussc::ColorOKLab::toRGB; + t["toHSB"] = &trussc::ColorOKLab::toHSB; + t["toOKLCH"] = &trussc::ColorOKLab::toOKLCH; + t["lerp"] = &trussc::ColorOKLab::lerp; + } + { + sol::usertype t = lua->new_usertype("PlayingSound"); + t["buffer"] = &trussc::PlayingSound::buffer; + t["volume"] = &trussc::PlayingSound::volume; + t["pan"] = &trussc::PlayingSound::pan; + t["speed"] = &trussc::PlayingSound::speed; + t["loop"] = &trussc::PlayingSound::loop; + t["playing"] = &trussc::PlayingSound::playing; + t["paused"] = &trussc::PlayingSound::paused; + t["mixMode"] = &trussc::PlayingSound::mixMode; + t["positionF"] = &trussc::PlayingSound::positionF; + t["rateRatio"] = &trussc::PlayingSound::rateRatio; + } + { + sol::usertype t = lua->new_usertype("LoadResult"); + t["error"] = &trussc::LoadResult::error; + t["message"] = &trussc::LoadResult::message; + t["ok"] = &trussc::LoadResult::ok; + t["success"] = &trussc::LoadResult::success; + t["fail"] = sol::overload([](trussc::LoadError e) { return trussc::LoadResult::fail(e); }, [](trussc::LoadError e, std::string msg) { return trussc::LoadResult::fail(e, msg); }); + } + lua->new_usertype("WindowType", + sol::meta_function::equal_to, [](trussc::WindowType a, trussc::WindowType b){ return a == b; }, + "Rect", sol::var(trussc::WindowType::Rect), + "Hanning", sol::var(trussc::WindowType::Hanning), + "Hamming", sol::var(trussc::WindowType::Hamming), + "Blackman", sol::var(trussc::WindowType::Blackman)); + { + sol::usertype t = lua->new_usertype("UdpReceiveEventArgs"); + t["data"] = &trussc::UdpReceiveEventArgs::data; + t["remoteHost"] = &trussc::UdpReceiveEventArgs::remoteHost; + t["remotePort"] = &trussc::UdpReceiveEventArgs::remotePort; + } + { + sol::usertype t = lua->new_usertype("TcpDisconnectEventArgs"); + t["reason"] = &trussc::TcpDisconnectEventArgs::reason; + t["wasClean"] = &trussc::TcpDisconnectEventArgs::wasClean; + } + { + sol::usertype t = lua->new_usertype("EnumLabelSpan"); + t["count"] = &trussc::EnumLabelSpan::count; + } +} +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#pragma clang diagnostic pop +#endif diff --git a/addons/tcxLua/src/generated/trussctype_generated_15.cpp b/addons/tcxLua/src/generated/trussctype_generated_15.cpp new file mode 100644 index 00000000..c3de9210 --- /dev/null +++ b/addons/tcxLua/src/generated/trussctype_generated_15.cpp @@ -0,0 +1,132 @@ +// AUTO-GENERATED usertype bindings from reference-data.json by luagen-types.js +#include "tcxLua.h" +#include "TrussC.h" +using namespace trussc; +using namespace std; +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma clang diagnostic push +#endif +void tcxLuaGenShard_15(const std::shared_ptr& lua) { +#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) + { + sol::usertype t = lua->new_usertype("VideoWriter", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["open"] = sol::overload([](trussc::VideoWriter& self, const fs::path & path, int width, int height) { return self.open(path, width, height); }, [](trussc::VideoWriter& self, const fs::path & path, int width, int height, const trussc::VideoRecordSettings & settings) { return self.open(path, width, height, settings); }); + t["close"] = &trussc::VideoWriter::close; + t["isOpen"] = &trussc::VideoWriter::isOpen; + t["getFrameCount"] = &trussc::VideoWriter::getFrameCount; + t["getWidth"] = &trussc::VideoWriter::getWidth; + t["getHeight"] = &trussc::VideoWriter::getHeight; + t["getFps"] = &trussc::VideoWriter::getFps; + t["getPath"] = &trussc::VideoWriter::getPath; + t["getSettings"] = &trussc::VideoWriter::getSettings; + t["addFrame"] = sol::overload([](trussc::VideoWriter& self, const trussc::Fbo & fbo) { return self.addFrame(fbo); }, [](trussc::VideoWriter& self, const trussc::Pixels & pixels) { return self.addFrame(pixels); }); + t["addFrameAt"] = sol::overload([](trussc::VideoWriter& self, const trussc::Fbo & fbo, double timeSec) { return self.addFrameAt(fbo, timeSec); }, [](trussc::VideoWriter& self, const trussc::Pixels & pixels, double timeSec) { return self.addFrameAt(pixels, timeSec); }); +#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) + t["submitFrame"] = &trussc::VideoWriter::submitFrame; +#endif + } +#endif +#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) + { + sol::usertype t = lua->new_usertype("TcpServer", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["onClientConnect"] = &trussc::TcpServer::onClientConnect; + t["onReceive"] = &trussc::TcpServer::onReceive; + t["onClientDisconnect"] = &trussc::TcpServer::onClientDisconnect; + t["onError"] = &trussc::TcpServer::onError; + t["start"] = sol::overload([](trussc::TcpServer& self, int port) { return self.start(port); }, [](trussc::TcpServer& self, int port, int maxClients) { return self.start(port, maxClients); }); + t["stop"] = &trussc::TcpServer::stop; + t["isRunning"] = &trussc::TcpServer::isRunning; + t["disconnectClient"] = &trussc::TcpServer::disconnectClient; + t["disconnectAllClients"] = &trussc::TcpServer::disconnectAllClients; + t["getClientCount"] = &trussc::TcpServer::getClientCount; + t["getClientIds"] = &trussc::TcpServer::getClientIds; + t["getClient"] = &trussc::TcpServer::getClient; + t["send"] = sol::overload([](trussc::TcpServer& self, int clientId, const std::vector & data) { return self.send(clientId, data); }, [](trussc::TcpServer& self, int clientId, const std::string & message) { return self.send(clientId, message); }); + t["broadcast"] = sol::overload([](trussc::TcpServer& self, const std::vector & data) { return self.broadcast(data); }, [](trussc::TcpServer& self, const std::string & message) { return self.broadcast(message); }); + t["setReceiveBufferSize"] = &trussc::TcpServer::setReceiveBufferSize; + t["getPort"] = &trussc::TcpServer::getPort; + } +#endif +#if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) + { + sol::usertype t = lua->new_usertype("Window", + sol::constructors(), + sol::call_constructor, sol::constructors()); + t["setApp"] = &trussc::Window::setApp; + t["getApp"] = &trussc::Window::getApp; + t["events"] = &trussc::Window::events; + t["close"] = &trussc::Window::close; + t["isOpen"] = &trussc::Window::isOpen; + t["setTitle"] = &trussc::Window::setTitle; + t["getTitle"] = &trussc::Window::getTitle; + t["getWidth"] = &trussc::Window::getWidth; + t["getHeight"] = &trussc::Window::getHeight; + t["setClearColor"] = &trussc::Window::setClearColor; + t["dispatchMousePressToTree"] = &trussc::Window::dispatchMousePressToTree; + t["dispatchMouseReleaseToTree"] = &trussc::Window::dispatchMouseReleaseToTree; + t["tickTree"] = &trussc::Window::tickTree; + t["drawTreeNow"] = &trussc::Window::drawTreeNow; + t["syncRootSize"] = &trussc::Window::syncRootSize; + } +#endif + { + sol::usertype t = lua->new_usertype("ChipSoundBundle"); + t["entries"] = &trussc::ChipSoundBundle::entries; + t["volume"] = &trussc::ChipSoundBundle::volume; + t["add"] = sol::overload([](trussc::ChipSoundBundle& self, const trussc::ChipSoundNote & note, float time) -> decltype(auto) { return self.add(note, time); }, [](trussc::ChipSoundBundle& self, trussc::ChipSoundNote::Wave wave, float hz, float duration, float time) -> decltype(auto) { return self.add(wave, hz, duration, time); }, [](trussc::ChipSoundBundle& self, trussc::ChipSoundNote::Wave wave, float hz, float duration, float time, float vol) -> decltype(auto) { return self.add(wave, hz, duration, time, vol); }); + t["clear"] = &trussc::ChipSoundBundle::clear; + t["getDuration"] = &trussc::ChipSoundBundle::getDuration; + t["build"] = &trussc::ChipSoundBundle::build; + } + lua->new_usertype("Beep", + sol::meta_function::equal_to, [](trussc::Beep a, trussc::Beep b){ return a == b; }, + "ping", sol::var(trussc::Beep::ping), + "success", sol::var(trussc::Beep::success), + "complete", sol::var(trussc::Beep::complete), + "coin", sol::var(trussc::Beep::coin), + "error", sol::var(trussc::Beep::error), + "warning", sol::var(trussc::Beep::warning), + "cancel", sol::var(trussc::Beep::cancel), + "click", sol::var(trussc::Beep::click), + "typing", sol::var(trussc::Beep::typing), + "notify", sol::var(trussc::Beep::notify), + "sweep", sol::var(trussc::Beep::sweep)); + lua->new_usertype("BlendMode", + sol::meta_function::equal_to, [](trussc::BlendMode a, trussc::BlendMode b){ return a == b; }, + "Alpha", sol::var(trussc::BlendMode::Alpha), + "Add", sol::var(trussc::BlendMode::Add), + "Multiply", sol::var(trussc::BlendMode::Multiply), + "Screen", sol::var(trussc::BlendMode::Screen), + "Subtract", sol::var(trussc::BlendMode::Subtract), + "Disabled", sol::var(trussc::BlendMode::Disabled)); + { + sol::usertype t = lua->new_usertype("TcpClientDisconnectEventArgs"); + t["clientId"] = &trussc::TcpClientDisconnectEventArgs::clientId; + t["reason"] = &trussc::TcpClientDisconnectEventArgs::reason; + t["wasClean"] = &trussc::TcpClientDisconnectEventArgs::wasClean; + } + lua->new_usertype("StrokeJoin", + sol::meta_function::equal_to, [](trussc::StrokeJoin a, trussc::StrokeJoin b){ return a == b; }, + "Miter", sol::var(trussc::StrokeJoin::Miter), + "Round", sol::var(trussc::StrokeJoin::Round), + "Bevel", sol::var(trussc::StrokeJoin::Bevel)); + { + sol::usertype t = lua->new_usertype("TcpConnectEventArgs"); + t["success"] = &trussc::TcpConnectEventArgs::success; + t["message"] = &trussc::TcpConnectEventArgs::message; + } + { + sol::usertype t = lua->new_usertype("GrabberFrame"); + t["pixels"] = &trussc::GrabberFrame::pixels; + t["timestampUs"] = &trussc::GrabberFrame::timestampUs; + } +} +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#pragma clang diagnostic pop +#endif diff --git a/addons/tcxLua/src/tcxLua.cpp b/addons/tcxLua/src/tcxLua.cpp index df536466..93e73f83 100644 --- a/addons/tcxLua/src/tcxLua.cpp +++ b/addons/tcxLua/src/tcxLua.cpp @@ -233,6 +233,12 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ float m20, float m21, float m22, float m23, float m30, float m31, float m32, float m33), Mat4(const Mat4&)>(), + sol::call_constructor, sol::constructors(), sol::meta_function::multiplication, sol::overload( [](const Mat4& a, const Mat4& b){ return a * b; }, @@ -270,6 +276,11 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ float m10, float m11, float m12, float m20, float m21, float m22), Mat3(const Mat3&)>(), + sol::call_constructor, sol::constructors(), sol::meta_function::multiplication, sol::overload( [](const Mat3& a, const Mat3& b){ return a * b; }, @@ -322,7 +333,8 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ sol::usertype fbo_type = lua->new_usertype("Fbo", - sol::constructors() // FIXME: move constructor? + sol::constructors(), + sol::call_constructor, sol::constructors() // FIXME: move constructor? ); fbo_type["allocate"] = sol::overload( @@ -362,7 +374,8 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ fbo_type["getSampler"] = &Fbo::getSampler; sol::usertype tex_type = lua->new_usertype("Texture", - sol::constructors() // FIXME: move constructor? + sol::constructors(), + sol::call_constructor, sol::constructors() // FIXME: move constructor? ); tex_type["allocate"] = sol::overload( [](Texture& f, int w, int h){ return f.allocate(w, h); }, @@ -428,7 +441,8 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ sol::usertype img_type = lua->new_usertype("Image", - sol::constructors() // FIXME: move constructor? + sol::constructors(), + sol::call_constructor, sol::constructors() // FIXME: move constructor? ); img_type["load"] = sol::overload( &Image::load, @@ -461,7 +475,8 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ img_type["getTexture"] = [](Image& f) -> Texture& { return f.getTexture(); }; sol::usertype pix_type = lua->new_usertype("Pixels", - sol::constructors() // FIXME: move constructor? + sol::constructors(), + sol::call_constructor, sol::constructors() // FIXME: move constructor? ); pix_type["allocate"] = sol::overload( [](Pixels& f, int w, int h){ return f.allocate(w, h); }, @@ -506,7 +521,8 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ sol::usertype shader_type = lua->new_usertype("Shader", - sol::constructors() // FIXME: move constructor? + sol::constructors(), + sol::call_constructor, sol::constructors() // FIXME: move constructor? ); shader_type["load"] = &Shader::load; shader_type["clear"] = &Shader::clear; @@ -533,7 +549,8 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ shader_type["submitVertices"] = &Shader::submitVertices; sol::usertype easycam_t = lua->new_usertype("EasyCam", - sol::constructors() + sol::constructors(), + sol::call_constructor, sol::constructors() ); easycam_t["begin"] = &EasyCam::begin; @@ -572,7 +589,8 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ easycam_t["mouseScrolled"] = &EasyCam::mouseScrolled; sol::usertype light_t = lua->new_usertype("Light", - sol::constructors() + sol::constructors(), + sol::call_constructor, sol::constructors() ); light_t["setDirectional"] = sol::overload( [](Light& m, float x, float y, float z){ return m.setDirectional(x, y, z); }, @@ -649,7 +667,8 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ sol::usertype material_t = lua->new_usertype("Material", - sol::constructors() + sol::constructors(), + sol::call_constructor, sol::constructors() ); material_t["getBaseColor"] = &Material::getBaseColor; material_t["setBaseColor"] = sol::overload( @@ -710,6 +729,7 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ // luagen in trussc_generated.cpp. Only the Json usertype is hand-written here. sol::usertype json_t = lua->new_usertype("Json", sol::constructors(), + sol::call_constructor, sol::constructors(), "get_string", [](Json& j){ return j.get(); }, "get_double", [](Json& j){ return j.get(); }, "get_float", [](Json& j){ return j.get(); }, @@ -749,6 +769,7 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ // loadXml/parseXml are free functions -> generated by luagen. Xml usertype is hand-written. sol::usertype xml_t = lua->new_usertype("Xml", sol::constructors(), + sol::call_constructor, sol::constructors(), "load", &Xml::load, "parse", &Xml::parse, "save", sol::overload( @@ -773,6 +794,7 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ sol::usertype xmlattr_t = lua->new_usertype("XmlAttribute", sol::constructors(), + sol::call_constructor, sol::constructors(), "set", sol::overload( // WORKAROUND [](XmlAttribute& x, pugi::string_view_t s){ return (x = s); }, [](XmlAttribute& x, const pugi::char_t* s){ return (x = s); }, @@ -788,6 +810,7 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ sol::usertype xmltext_t = lua->new_usertype("XmlText", sol::constructors(), + sol::call_constructor, sol::constructors(), "set", sol::overload( // WORKAROUND [](XmlText& x, pugi::string_view_t s){ return (x = s); }, [](XmlText& x, const pugi::char_t* s){ return (x = s); }, @@ -800,6 +823,7 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ sol::usertype xmlnode_t = lua->new_usertype("XmlNode", sol::constructors(), + sol::call_constructor, sol::constructors(), "append_attribute", sol::overload( [](XmlNode& x, pugi::string_view_t n){ return x.append_attribute(n); }, [](XmlNode& x, const pugi::char_t* n){ return x.append_attribute(n); } @@ -871,6 +895,8 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ lua->new_usertype("SoundBuffer", sol::constructors(), + sol::call_constructor, sol::constructors(), sol::base_classes, sol::bases(), "loadOgg", &SoundBuffer::loadOgg, "loadWav", &SoundBuffer::loadWav, @@ -918,6 +944,7 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ lua->new_usertype("MicInput", sol::constructors(), + sol::call_constructor, sol::constructors(), // MicInput(const MicInput&), MicInput(MicInput&&)>(), "start", &MicInput::start, "stop", &MicInput::stop, diff --git a/addons/tcxLua/src/tcxLua.h b/addons/tcxLua/src/tcxLua.h index 5f64cbd3..5deae013 100644 --- a/addons/tcxLua/src/tcxLua.h +++ b/addons/tcxLua/src/tcxLua.h @@ -61,6 +61,12 @@ class tcxLua { TweenT(TweenValue, TweenValue, float, EaseType, EaseMode) // FIXME: move constructor? >(), + sol::call_constructor, sol::constructors< + TweenT(), + TweenT(TweenValue, TweenValue, float), + TweenT(TweenValue, TweenValue, float, EaseType), + TweenT(TweenValue, TweenValue, float, EaseType, EaseMode) + >(), "from", &TweenT::from, "to", &TweenT::to, "duration", &TweenT::duration, diff --git a/addons/tcxLua/src/tcxLuaPathAdapter.h b/addons/tcxLua/src/tcxLuaPathAdapter.h index e3545bfa..aa309d97 100644 --- a/addons/tcxLua/src/tcxLuaPathAdapter.h +++ b/addons/tcxLua/src/tcxLuaPathAdapter.h @@ -41,6 +41,24 @@ #include "sol/sol.hpp" namespace sol { + + // std::filesystem::path has begin()/end() (component iteration), so sol2's + // container detection would otherwise treat it as a Lua container and try + // to instantiate container_traits (which needs operator[] -> hard compile + // error, and would be wrong semantics anyway). A path is a VALUE here -- + // pushed/read as a UTF-8 Lua string via the stack specializations below. + template <> + struct is_container : std::false_type {}; + + // Tell sol2 a path IS a Lua string (value semantics). Without this the + // default lua_type_of = userdata makes usertype member fields of type + // fs::path (e.g. FileDialogResult.filePath) go through the by-reference + // userdata machinery, which cannot bind our by-value getter result + // (non-const lvalue ref to temporary) and fails to compile. + template <> + struct lua_type_of + : std::integral_constant {}; + namespace stack { // Lua string (LUA_TSTRING) -> std::filesystem::path. @@ -62,7 +80,7 @@ namespace stack { // Accept a Lua string where a std::filesystem::path is expected. Anything // else is rejected through the handler with a useful message. template <> - struct unqualified_checker { + struct unqualified_checker { template static bool check(lua_State* L, int index, Handler&& handler, record& tracking) { tracking.use(1); diff --git a/addons/tcxLua/tools/implicit-conv-audit.js b/addons/tcxLua/tools/implicit-conv-audit.js new file mode 100644 index 00000000..246c7c6e --- /dev/null +++ b/addons/tcxLua/tools/implicit-conv-audit.js @@ -0,0 +1,133 @@ +#!/usr/bin/env node +/** + * implicit-conv-audit.js — enumerate every bound function/method argument that + * relies on a C++ implicit conversion Lua cannot perform. + * + * A C++ call like drawLine(vec2a, vec2b) against drawLine(Vec3, Vec3) works + * because Vec3 has a non-explicit Vec3(const Vec2&) constructor. sol2 knows + * nothing about C++ converting constructors: with safeties off the wrong-type + * argument is UB (garbage reinterpret for userdata, NULL deref for + * non-userdata like numbers/strings). This script cross-references + * reference-data.json (all bound signatures x all type constructors) to list + * every such site, so adapters (see tcxLuaPathAdapter.h) can be added where + * the conversion is worth supporting. + * + * node implicit-conv-audit.js [path/to/reference-data.json] + * + * `explicit` is not captured in the AST extraction, so it is recovered by + * scanning core/include headers for `explicit TypeName(...)` and matching the + * first parameter's base type. + */ +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +const RDJ = process.argv[2] || + path.join(__dirname, '../../../docs/reference/reference-data.json'); +const CORE_INCLUDE = path.join(__dirname, '../../../core/include'); +const data = JSON.parse(fs.readFileSync(RDJ, 'utf8')); + +// ---- helpers ----------------------------------------------------------------- +const baseType = (t) => (t || '') + .replace(/&&?/g, '').replace(/\bconst\b/g, '').replace(/\btrussc::/g, '') + .replace(/\bstd::filesystem::/g, 'fs::').trim(); +const NUMBER = new Set(['float', 'double', 'int', 'unsigned', 'unsigned int', + 'long', 'size_t', 'int8_t', 'int16_t', 'int32_t', 'int64_t', + 'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t', 'short', 'char']); +const STRING = new Set(['std::string', 'string', 'std::string_view', + 'string_view', 'const char *', 'char *']); +const luaKind = (bt) => NUMBER.has(bt) ? 'number' + : STRING.has(bt) ? 'string' + : bt === 'bool' ? 'bool' + : bt === 'fs::path' || bt === 'path' ? 'path' + : null; // null = not natively representable (class type / other) + +// ---- collect bound top-level types + their converting constructors ------------ +const types = {}; // name -> entry +for (const id in data) { + const e = data[id]; + if (e.kind === 'type' && !e.owner && !e.ns && !e.hidden) types[e.name] = e; +} +const enums = new Set(); +for (const id in data) { + const e = data[id]; + if (e.kind === 'enum' && !e.owner && !e.ns) enums.add(e.name); +} + +// explicit-ctor recovery: "explicit TypeName(" + first param base type +const explicitSet = new Set(); // "Type<-FirstParamBase" +try { + const out = execSync( + `grep -rhoE "explicit +[A-Za-z_][A-Za-z0-9_]* *\\([^,)]*" ${JSON.stringify(CORE_INCLUDE)}`, + { encoding: 'utf8', maxBuffer: 1 << 24 }); + for (const line of out.split('\n')) { + const m = line.match(/explicit +([A-Za-z_][A-Za-z0-9_]*) *\(([^,)]*)/); + if (!m) continue; + const first = baseType(m[2].replace(/[A-Za-z_][A-Za-z0-9_]*$/, '').trim() || m[2]); + explicitSet.add(`${m[1]}<-${first}`); + } +} catch (e) { /* grep found nothing */ } + +// converting ctors: exactly 1 required arg, source != self +const convSources = {}; // TypeName -> [{from, fromKind, params, explicit}] +for (const name in types) { + for (const c of (types[name].constructors || [])) { + const args = c.args || []; + const required = args.filter(a => !a.hasDefault); + if (required.length !== 1) continue; + const from = baseType(required[0].type); + if (from === name) continue; // copy/move + const kind = luaKind(from) || (types[from] ? 'usertype' : (enums.has(from) ? 'enum' : 'other')); + const explicit = explicitSet.has(`${name}<-${from}`); + (convSources[name] ||= []).push({ from, fromKind: kind, params: c.params, explicit }); + } +} + +// ---- walk every bound callable's args ----------------------------------------- +// Mirrors luagen bindability: skip template sigs, pointer/array args. +const sites = {}; // "To<-From" -> {explicit, examples: [], count} +let usertypeArgSites = 0; // context stat for the SOL-safeties decision +function scanSignature(ownerLabel, fnName, sig) { + if (sig.tmpl) return; + for (const a of (sig.args || [])) { + if (a.isPointer || a.isArray) return; // sig not bound at all + } + for (const a of (sig.args || [])) { + const bt = baseType(a.type); + if (!types[bt]) continue; // not a class-type arg + usertypeArgSites++; + for (const cs of (convSources[bt] || [])) { + if (cs.fromKind === 'other') continue; // not reachable from Lua anyway + const key = `${bt} <- ${cs.from}${cs.explicit ? ' [explicit in C++]' : ''}`; + const s = (sites[key] ||= { count: 0, examples: [], explicit: cs.explicit }); + s.count++; + if (s.examples.length < 8) + s.examples.push(`${ownerLabel}${fnName}(${(sig.args || []).map(x => baseType(x.type)).join(', ')})`); + } + } +} +for (const id in data) { + const e = data[id]; + if (e.hidden || e.deprecated) continue; + if (e.kind === 'func' && !e.owner && !e.ns) { + for (const sig of (e.signatures || [])) scanSignature('', e.name, sig); + } else if (e.kind === 'method' && e.owner) { + if (e.access && e.access !== 'public') continue; + const owner = data[e.owner]; + if (!owner || owner.hidden) continue; + for (const sig of (e.signatures || [])) scanSignature(owner.name + ':', e.name, sig); + } +} + +// ---- report -------------------------------------------------------------------- +const entries = Object.entries(sites).sort((a, b) => b[1].count - a[1].count); +console.log('== Implicit-conversion reliance sites (bound args whose C++ type has a'); +console.log(' converting ctor from a Lua-representable source) ==\n'); +for (const [key, s] of entries) { + console.log(`${key} — ${s.count} arg site(s)`); + for (const ex of s.examples) console.log(` ${ex}`); + if (s.count > s.examples.length) console.log(` ... +${s.count - s.examples.length} more`); + console.log(''); +} +console.log(`(context: ${usertypeArgSites} bound arg sites take a usertype at all — every one`); +console.log(` of them is UB on a wrong-typed argument while sol safeties are off)`); diff --git a/addons/tcxLua/tools/luagen-types/luagen-types.js b/addons/tcxLua/tools/luagen-types/luagen-types.js index c22dee66..d83cbc6c 100644 --- a/addons/tcxLua/tools/luagen-types/luagen-types.js +++ b/addons/tcxLua/tools/luagen-types/luagen-types.js @@ -2,13 +2,22 @@ // luagen-types.js — Phase 2: generate tcxLua Sol2 USERTYPE bindings (constructors, // properties, methods, static methods, operators) from reference-data.json. // -// node luagen-types.js > trussctype_generated.cpp +// node luagen-types.js [colors.json] # writes src/generated/ (16 shards + aggregator) // // Pairs with luagen.js (Phase 1 = free functions). Custom Lua glue (Json/Xml // get_string etc.) is NOT in reference-data, so those stay hand-written; this only // emits the structural surface. Correctness is verified by compiling the output. const fs = require('fs'); +const nodePath = require('path'); +// Output: SHARDS shard TUs + an aggregator, written straight into +// src/generated/. Splitting the sol2 template instantiations across TUs keeps +// peak compiler memory sane: the old single 100-usertype TU needed ~8 GB RSS +// (unbuildable on an 8 GB Raspberry Pi 5 even at -j1); one shard needs ~1.3 GB. +// 16 balances Mac (-j16 ~ 20 GB) and RPi5 (-j4 ~ 5 GB); to change it, edit the +// constant and regenerate. +const SHARDS = 16; +const OUTDIR = nodePath.join(__dirname, '../../src/generated'); const path = process.argv[2]; if (!path) { console.error('usage: node luagen-types.js [colors.json]'); process.exit(1); } const data = JSON.parse(fs.readFileSync(path, 'utf8')); @@ -181,7 +190,14 @@ function emitType(typeEntry, cppType, luaName, T) { } let s = ` {\n sol::usertype<${Q}> t = lua->new_usertype<${Q}>("${luaName}"`; - if (ctorTypes.length) s += `,\n sol::constructors<${ctorTypes.join(', ')}>()`; + // Positional constructors provide Type.new(...); sol::call_constructor + // additionally enables the Type(...) call form (without it the usertype + // table has no __call and e.g. `Vec3(1, 2, 3)` fails with "attempt to + // call a table value"). + if (ctorTypes.length) { + s += `,\n sol::constructors<${ctorTypes.join(', ')}>()`; + s += `,\n sol::call_constructor, sol::constructors<${ctorTypes.join(', ')}>()`; + } for (const meta in ops) { const lams = [...new Set(ops[meta])]; s += `,\n sol::meta_function::${meta}, ` + (lams.length === 1 ? lams[0] : `sol::overload(${lams.join(', ')})`); @@ -237,6 +253,10 @@ const EXCLUDE = new Set([ 'MicInput', 'Pixels', 'Shader', 'SoundBuffer', 'Texture', ]); +// Heavy blocks (usertypes + enums) are collected individually so they can be +// bin-packed across shard TUs; light tail bindings (constants, colors) stay in +// the aggregator. `body` remains the concatenation for single-file output. +const blocks = []; let body = '', count = 0; // wrap a whole usertype block when the TYPE itself is platform-restricted function guardedType(code, e) { @@ -252,12 +272,12 @@ for (const id in data) { if (!e.lua_bind || !e.lua_bind.length) { report.push(`${e.name}: templated, no lua_bind (skipped)`); continue; } for (const T of e.lua_bind) { const base = e.name.replace(/<.*>/, ''); - try { body += guardedType(emitType(e, `${base}<${T}>`, `${base}_${T.replace(/[^A-Za-z0-9]/g, '')}`, T), e); count++; } + try { blocks.push(guardedType(emitType(e, `${base}<${T}>`, `${base}_${T.replace(/[^A-Za-z0-9]/g, '')}`, T), e)); count++; } catch (err) { report.push(`${e.name}<${T}>: ${err.message}`); } } continue; } - try { body += guardedType(emitType(e, e.name, e.name, null), e); count++; } + try { blocks.push(guardedType(emitType(e, e.name, e.name, null), e)); count++; } catch (err) { report.push(`${e.name}: ${err.message}`); } } @@ -277,7 +297,7 @@ for (const id in data) { s2 += ` sol::meta_function::equal_to, [](${Q} a, ${Q} b){ return a == b; }`; for (const m of e.members) s2 += `,\n "${m.name}", sol::var(${Q}::${m.name})`; s2 += `);\n`; - body += s2; enumCount++; + blocks.push(s2); enumCount++; } if (nestedEnums.length) report.push(`nested enums (not yet emitted): ${nestedEnums.join(', ')}`); @@ -285,11 +305,12 @@ if (nestedEnums.length) report.push(`nested enums (not yet emitted): ${nestedEnu // Top-level non-hidden constants (TAU, KEY_*, MOUSE_BUTTON_*, VSYNC, Direction // shorthands Left/Center/...). Plain assignments; sol converts values as usual. let constCount = 0; -body += ` // constants\n`; +let tail = ''; // light bindings kept in the aggregator TU (constants, colors) +tail += ` // constants\n`; for (const id in data) { const e = data[id]; if (e.kind !== 'var' || e.owner || e.ns || e.hidden) continue; - body += ` (*lua)["${e.name}"] = trussc::${e.name};\n`; + tail += ` (*lua)["${e.name}"] = trussc::${e.name};\n`; constCount++; } @@ -300,30 +321,62 @@ let colorCount = 0; if (colorsData) { // `colors` is just a named table; use a file-local tag struct (emitted in the // prelude below) as the usertype key — there is no trussc::Colors. - body += ` {\n sol::usertype t = lua->new_usertype("colors");\n`; + tail += ` {\n sol::usertype t = lua->new_usertype("colors");\n`; for (const group of colorsData) for (const item of group.items) { - body += ` t["${item.name}"] = sol::var(trussc::colors::${item.name});\n`; + tail += ` t["${item.name}"] = sol::var(trussc::colors::${item.name});\n`; colorCount++; } - body += ` }\n`; + tail += ` }\n`; } -process.stdout.write(`// AUTO-GENERATED usertype bindings from reference-data.json by luagen-types.js +const PRELUDE = (needsColorsTag) => `// AUTO-GENERATED usertype bindings from reference-data.json by luagen-types.js #include "tcxLua.h" #include "TrussC.h" using namespace trussc; using namespace std; -namespace { struct TcxLuaColorsTable {}; } // tag for the \`colors\` constant table -#ifndef _MSC_VER +${needsColorsTag ? 'namespace { struct TcxLuaColorsTable {}; } // tag for the `colors` constant table\n' : ''}#ifndef _MSC_VER #pragma GCC diagnostic push #pragma clang diagnostic push #endif -void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { -${body}} -#ifndef _MSC_VER +`; +const POSTLUDE = `#ifndef _MSC_VER #pragma GCC diagnostic pop #pragma clang diagnostic pop #endif -`); +`; + +{ + // Shard output: greedy bin-packing of the heavy blocks (block size is a + // fair proxy for sol2 template load) into N TUs, plus an aggregator that + // defines setGeneratedTypeBindings, calls each shard, and carries the + // light tail (constants + colors). + const buckets = Array.from({ length: SHARDS }, () => ({ size: 0, parts: [] })); + for (const b of [...blocks].sort((a, c) => c.length - a.length)) { + const min = buckets.reduce((x, y) => (y.size < x.size ? y : x)); + min.parts.push(b); min.size += b.length; + } + const outdir = OUTDIR; + // Remove stale shard files from a previous (different-N) run. + for (const f of fs.readdirSync(outdir)) { + if (/^trussctype_generated_\d+\.cpp$/.test(f)) fs.unlinkSync(nodePath.join(outdir, f)); + } + const decls = []; + buckets.forEach((bk, i) => { + const nn = String(i).padStart(2, '0'); + const fn = `tcxLuaGenShard_${nn}`; + decls.push(fn); + fs.writeFileSync(nodePath.join(outdir, `trussctype_generated_${nn}.cpp`), + `${PRELUDE(false)}void ${fn}(const std::shared_ptr& lua) { +${bk.parts.join('')}} +${POSTLUDE}`); + }); + fs.writeFileSync(nodePath.join(outdir, 'trussctype_generated.cpp'), + `${PRELUDE(true)}${decls.map((d) => `void ${d}(const std::shared_ptr& lua);`).join('\n')} +void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { +${decls.map((d) => ` ${d}(lua);`).join('\n')} +${tail}} +${POSTLUDE}`); + console.error(`[luagen-types] wrote ${SHARDS} shard TUs + aggregator to ${outdir} (largest shard ${Math.max(...buckets.map(b => b.size))} bytes)`); +} console.error(`[luagen-types] usertypes: ${count} | enums: ${enumCount} | colors: ${colorCount} | consts: ${constCount} | skipped members: template ${skip.template}, unbindable ${skip.unbindable}, unsupported-op ${skip.op}`); for (const r of report) console.error(' ' + r); diff --git a/addons/tcxLua/tools/luagen/luagen.js b/addons/tcxLua/tools/luagen/luagen.js index 13925eb7..67e6f35e 100644 --- a/addons/tcxLua/tools/luagen/luagen.js +++ b/addons/tcxLua/tools/luagen/luagen.js @@ -119,9 +119,14 @@ process.stdout.write(`// AUTO-GENERATED from reference-data.json by luagen.js using namespace trussc; using namespace std; +// The Lua API deliberately keeps binding deprecated C++ compat names +// (colorFromHSB, tcLog*, ...) so existing Lua scripts stay stable — this shim +// layer is exempt from the CI deprecation gate (-Werror=deprecated-declarations). #ifndef _MSC_VER #pragma GCC diagnostic push #pragma clang diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#pragma clang diagnostic ignored "-Wdeprecated-declarations" #endif void tcxLua::setTrussCGeneratedBindings(const std::shared_ptr& lua) { diff --git a/addons/tcxObj/src/tcxObjLoader.cpp b/addons/tcxObj/src/tcxObjLoader.cpp index 1a6055dc..be794216 100644 --- a/addons/tcxObj/src/tcxObjLoader.cpp +++ b/addons/tcxObj/src/tcxObjLoader.cpp @@ -16,7 +16,7 @@ namespace tcx::obj { bool ObjLoader::load(const fs::path& path) { clear(); - fs::path objPath = path.is_absolute() ? path : fs::path(getDataPath(path.string())); + fs::path objPath = getDataPath(path); // absolute paths pass through if (!fs::exists(objPath)) { logError() << "ObjLoader::load: file not found: " << objPath; diff --git a/addons/tcxTls/example-tls/src/tcApp.cpp b/addons/tcxTls/example-tls/src/tcApp.cpp index 7ea8b9e2..eefa52be 100644 --- a/addons/tcxTls/example-tls/src/tcApp.cpp +++ b/addons/tcxTls/example-tls/src/tcApp.cpp @@ -8,12 +8,12 @@ using namespace std; void tcApp::setup() { - tcLogNotice() << "=== TLS (HTTPS) Client Example ==="; - tcLogNotice() << "Press C to connect to httpbin.org (HTTPS)"; - tcLogNotice() << "Press SPACE to send HTTP GET request"; - tcLogNotice() << "Press D to disconnect"; - tcLogNotice() << "Press X to clear log"; - tcLogNotice() << "=================================="; + logNotice() << "=== TLS (HTTPS) Client Example ==="; + logNotice() << "Press C to connect to httpbin.org (HTTPS)"; + logNotice() << "Press SPACE to send HTTP GET request"; + logNotice() << "Press D to disconnect"; + logNotice() << "Press X to clear log"; + logNotice() << "=================================="; // TLS configuration (no certificate verification - for testing) client.setVerifyNone(); @@ -190,7 +190,7 @@ void tcApp::cleanup() { } void tcApp::addSent(const string& msg) { - tcLogNotice() << "[SENT] " << msg; + logNotice() << "[SENT] " << msg; lock_guard lock(sentMutex); sentMessages.push_back(msg); if (sentMessages.size() > 30) { @@ -199,7 +199,7 @@ void tcApp::addSent(const string& msg) { } void tcApp::addReceived(const string& msg) { - tcLogNotice() << "[RECV] " << msg; + logNotice() << "[RECV] " << msg; lock_guard lock(receivedMutex); receivedMessages.push_back(msg); if (receivedMessages.size() > 30) { diff --git a/addons/tcxTls/src/tcTlsClient.cpp b/addons/tcxTls/src/tcTlsClient.cpp index 862162c3..29c743b9 100644 --- a/addons/tcxTls/src/tcTlsClient.cpp +++ b/addons/tcxTls/src/tcTlsClient.cpp @@ -138,7 +138,7 @@ TlsClient::TlsClient() { reinterpret_cast(pers), strlen(pers)); if (ret != 0) { - tcLogError() << "TlsClient: Failed to seed random generator: " << ret; + logError() << "TlsClient: Failed to seed random generator: " << ret; } } @@ -157,7 +157,7 @@ bool TlsClient::setCACertificate(const std::string& pemData) { if (ret < 0) { char errBuf[256]; mbedtls_strerror(ret, errBuf, sizeof(errBuf)); - tcLogError() << "TlsClient: Failed to parse CA certificate: " << errBuf; + logError() << "TlsClient: Failed to parse CA certificate: " << errBuf; return false; } // Explicit user intent: skip the default-CA auto-load entirely. @@ -170,16 +170,16 @@ bool TlsClient::setCACertificateFile(const std::string& path) { // call — works portably on POSIX and Windows. std::ifstream file(path, std::ios::binary | std::ios::ate); if (!file) { - tcLogError() << "TlsClient: Failed to open CA certificate file: " << path; + logError() << "TlsClient: Failed to open CA certificate file: " << path; return false; } std::streamsize size = file.tellg(); if (size < 0) { - tcLogError() << "TlsClient: Failed to size CA certificate file: " << path; + logError() << "TlsClient: Failed to size CA certificate file: " << path; return false; } if (size > kMaxCaPemBytes) { - tcLogError() << "TlsClient: CA certificate file too large (" + logError() << "TlsClient: CA certificate file too large (" << size << " bytes, limit " << kMaxCaPemBytes << "): " << path; return false; @@ -220,7 +220,7 @@ void TlsClient::ensureDefaultCAsLoaded() { } CertCloseStore(store, 0); if (parsed > 0) { - tcLogNotice() << "TlsClient: loaded " << parsed + logNotice() << "TlsClient: loaded " << parsed << " CAs from Windows Cert Store (ROOT)"; return; } @@ -240,7 +240,7 @@ void TlsClient::ensureDefaultCAsLoaded() { struct stat st; if (stat(*p, &st) != 0) continue; if (st.st_size > kMaxCaPemBytes) { - tcLogWarning() << "TlsClient: skipping " << *p + logWarning() << "TlsClient: skipping " << *p << " (size " << st.st_size << " > limit " << kMaxCaPemBytes << ")"; continue; @@ -257,7 +257,7 @@ void TlsClient::ensureDefaultCAsLoaded() { if (ret < 0) continue; // try the next path const size_t loaded = countCerts(&ctx_->cacert) - before; if (loaded > 0) { - tcLogNotice() << "TlsClient: loaded " << loaded + logNotice() << "TlsClient: loaded " << loaded << " CAs from " << *p; return; } @@ -273,14 +273,14 @@ void TlsClient::ensureDefaultCAsLoaded() { if (ret >= 0) { const size_t loaded = countCerts(&ctx_->cacert) - before; if (loaded > 0) { - tcLogNotice() << "TlsClient: loaded " << loaded + logNotice() << "TlsClient: loaded " << loaded << " CAs from bundled cacert.pem (" << tls_internal::bundledCaBundleDate() << ")"; return; } } - tcLogError() << "TlsClient: failed to load any default CA certificates. " + logError() << "TlsClient: failed to load any default CA certificates. " << "TLS handshakes will fail unless setCACertificate() or " << "setVerifyNone() is called explicitly."; } @@ -377,7 +377,7 @@ bool TlsClient::connect(const std::string& host, int port) { // TCP Connected immediately running_ = true; handshakePending_ = true; - tcLogNotice() << "TCP connected to " << host << ":" << port << ", starting TLS handshake..."; + logNotice() << "TCP connected to " << host << ":" << port << ", starting TLS handshake..."; } if (running_) { @@ -497,7 +497,7 @@ void TlsClient::processNetwork() { #endif connectPending_ = false; handshakePending_ = true; - tcLogNotice() << "TCP connected (async), starting TLS handshake..."; + logNotice() << "TCP connected (async), starting TLS handshake..."; } else { return; // Still connecting } @@ -509,7 +509,7 @@ void TlsClient::processNetwork() { handshakePending_ = false; connected_ = true; - tcLogNotice() << "TLS connected to " << remoteHost_ << ":" << remotePort_ + logNotice() << "TLS connected to " << remoteHost_ << ":" << remotePort_ << " [" << getTlsVersion() << ", " << getCipherSuite() << "]"; tc::TcpConnectEventArgs args; diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 9ca3ffa3..40d63959 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -152,6 +152,20 @@ add_library(tc::TrussC ALIAS TrussC) # PUBLIC so user apps and addons see the same value. target_compile_definitions(TrussC PUBLIC TC_VERSION_STRING="${TC_VERSION}") +# CI gate: promote uses of [[deprecated]] TrussC API to errors in the +# framework's OWN sources (core lib TUs here; bundled examples/tests get the +# same flag via trussc_app.cmake). Opt-in through -DTC_DEPRECATED_ERRORS=ON or +# the TC_DEPRECATED_ERRORS env var (set in CI) — never on by default, so user +# builds are unaffected. GCC/Clang only: MSVC folds CRT "unsafe function" +# warnings into the same C4996, so /we4996 would misfire on fopen etc. +if(NOT DEFINED TC_DEPRECATED_ERRORS AND DEFINED ENV{TC_DEPRECATED_ERRORS}) + set(TC_DEPRECATED_ERRORS "$ENV{TC_DEPRECATED_ERRORS}") +endif() +if(TC_DEPRECATED_ERRORS) + target_compile_options(TrussC PRIVATE + $<$>:-Werror=deprecated-declarations>) +endif() + # iOS: sound sources include ObjC headers (AVFoundation) via miniaudio, # so they must be compiled as Objective-C++ if(TC_PLATFORM_IOS AND TC_SOUND_SOURCES) @@ -684,7 +698,10 @@ if(TC_SHADER_SOURCES) set(_TC_SHADER_OUTPUT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/include/tc/gpu/shaders") file(MAKE_DIRECTORY "${_TC_SHADER_OUTPUT_DIR}") - set(_TC_SOKOL_SLANG "metal_macos:metal_ios:hlsl5:glsl300es:glsl430:wgsl") + # metal_sim keeps apps runnable on the iOS simulator (sg_query_backend() + # reports SG_BACKEND_METAL_SIMULATOR there; without it shader lookup + # returns null sources and crashes in sg_make_shader). + set(_TC_SOKOL_SLANG "metal_macos:metal_ios:metal_sim:hlsl5:glsl300es:glsl430:wgsl") set(_TC_SHADER_OUTPUTS "") foreach(_shader_src ${TC_SHADER_SOURCES}) diff --git a/core/cmake/trussc_app.cmake b/core/cmake/trussc_app.cmake index 8098e673..e71b32ce 100644 --- a/core/cmake/trussc_app.cmake +++ b/core/cmake/trussc_app.cmake @@ -396,6 +396,12 @@ foreach(ENTRY \${ENTRIES}) # NULL thunk data elseif(SYM MATCHES \"^_RTC_\") # Internal symbol for runtime checks + elseif(SYM MATCHES \"^(size|mode|uid|gid)$\") + # Archive member header metadata, not a symbol. The header lines + # (' 2A4B8 size', ' 0 mode', ...) only match the + # hex-address regex once a member grows to a 6-hex-digit size + # field (>= 1 MB object file), which is why small libs never hit + # this. else() list(APPEND SYMBOLS \"\${SYM}\") endif() @@ -489,6 +495,23 @@ message(\" [HotReload] Generated \${DEF_FILE} with \${SYM_COUNT} symbols\") endif() endif() + # CI gate for the repo's OWN bundled projects (examples/tests): uses of + # [[deprecated]] TrussC API become errors so stale API can't ship in + # examples. Opt-in via -DTC_DEPRECATED_ERRORS=ON or the + # TC_DEPRECATED_ERRORS env var (set in CI); never on for user builds. + # GCC/Clang only — MSVC's C4996 also covers CRT "unsafe" warnings. + if(NOT DEFINED TC_DEPRECATED_ERRORS AND DEFINED ENV{TC_DEPRECATED_ERRORS}) + set(TC_DEPRECATED_ERRORS "$ENV{TC_DEPRECATED_ERRORS}") + endif() + if(TC_DEPRECATED_ERRORS) + set(_TC_DEPR_FLAG + $<$>:-Werror=deprecated-declarations>) + target_compile_options(${PROJECT_NAME} PRIVATE ${_TC_DEPR_FLAG}) + if(TARGET guest) + target_compile_options(guest PRIVATE ${_TC_DEPR_FLAG}) + endif() + endif() + # Silence the macOS ld-prime "ignoring duplicate libraries" warning. It # fires on the legitimate diamond dependency (app -> TrussC and # app -> addon -> TrussC), where libTrussC.a appears twice in the link @@ -684,8 +707,11 @@ message(\" [HotReload] Generated \${DEF_FILE} with \${SYM_COUNT} symbols\") message(STATUS "[${PROJECT_NAME}] sokol-shdc downloaded successfully") endif() - # Output languages: Metal (macOS/iOS), HLSL (Windows), GLSL (Linux), WGSL (Web/WebGPU) - set(_TC_SOKOL_SLANG "metal_macos:metal_ios:hlsl5:glsl300es:wgsl") + # Output languages: Metal (macOS/iOS/iOS-simulator), HLSL (Windows), + # GLSL (Linux), WGSL (Web/WebGPU). metal_sim keeps apps runnable on the + # iOS simulator (sg_query_backend() reports SG_BACKEND_METAL_SIMULATOR + # there; without it shader lookup returns null sources and crashes). + set(_TC_SOKOL_SLANG "metal_macos:metal_ios:metal_sim:hlsl5:glsl300es:wgsl") set(_TC_SHADER_OUTPUTS "") foreach(_shader_src ${_TC_SHADER_SOURCES}) @@ -965,6 +991,14 @@ message(\" [HotReload] Generated \${DEF_FILE} with \${SYM_COUNT} symbols\") -sASYNCIFY=1 --shell-file=${_TC_SHELL_FILE} ) + # Emscripten 6.x flipped GROWABLE_ARRAYBUFFERS on by default, which backs + # the wasm heap with a resizable ArrayBuffer. Current browsers reject + # views over resizable buffers in TextDecoder and WebGL2 texture uploads, + # so apps die at runtime (WGPU: inside the emdawnwebgpu JS bindings; + # WebGL2: texSubImage2D). Force the pre-6.x grow-and-detach behavior. + if(EMSCRIPTEN_VERSION VERSION_GREATER_EQUAL "6.0") + target_link_options(${PROJECT_NAME} PRIVATE -sGROWABLE_ARRAYBUFFERS=0) + endif() # Backend-specific link options # TC_WEB_BACKEND is set in core/CMakeLists.txt (defaults to WGPU) if(NOT DEFINED TC_WEB_BACKEND) diff --git a/core/include/TrussC.h b/core/include/TrussC.h index ac19ae5c..f39510c1 100644 --- a/core/include/TrussC.h +++ b/core/include/TrussC.h @@ -13,7 +13,7 @@ // sokol headers #include "sokol/sokol_log.h" #define SOKOL_NO_ENTRY // We define our own main() -#include "sokol/sokol_app.h" +#include "sokol/sokol_app_tc.h" #include "sokol/sokol_gfx.h" #include "sokol/sokol_glue.h" #include "sokol/util/sokol_gl_tc.h" @@ -84,6 +84,7 @@ #include "tc/events/tcCoreEvents.h" // TrussC utilities +#include "tc/utils/tcFileIO.h" // fs::path boundary helpers (before all path consumers) #include "tc/utils/tcUtils.h" #include "tc/utils/tcMainThread.h" // runOnMainThread / drainMainThreadQueue #include "tc/utils/tcTime.h" @@ -170,43 +171,24 @@ namespace internal { inline float nearClipOverride = 0.0f; inline float farClipOverride = 0.0f; - // Current screen setup state (for 2D drawing in perspective mode) - inline float currentScreenFov = 45.0f; - inline float currentViewW = 0.0f; - inline float currentViewH = 0.0f; - inline float currentCameraDist = 0.0f; // Distance from camera to Z=0 plane - - // View/Projection matrix tracking (for worldToScreen/screenToWorld) - inline Mat4 currentViewMatrix = Mat4::identity(); - inline Mat4 currentProjectionMatrix = Mat4::identity(); + // Screen setup / view-projection tracking state (currentScreenFov, + // currentViewW/H, currentCameraDist, currentView/ProjectionMatrix) and the + // current 2D blend mode moved to WindowContext (tc/app/tcWindowContext.h). // Forward declaration of setupScreenFov functions (defined later, after TAU is available) // `pickable` flows into the registered CameraContext (false for FBO scopes). inline void setupScreenFovWithSize(float fovDeg, float viewW, float viewH, float nearDist = 0.0f, float farDist = 0.0f, bool pickable = true); inline void setupScreenFov(float fovDeg, float nearDist = 0.0f, float farDist = 0.0f); - - - // Current 2D blend mode (the "role"). The actual sgl pipeline for it lives in - // the active RenderTarget's lazy cache — see tcRenderTarget.h active2D(). - inline BlendMode currentBlendMode = BlendMode::Alpha; } } // namespace trussc (temporarily closed) // RenderTarget: single source of truth for sgl pipeline selection (swapchain/FBO). -// Included before restoreCurrentPipeline so it can use the active*() helpers. #include "tc/graphics/tcRenderTarget.h" -// Forward declarations for FBO pipeline switching (used in tcRenderContext.h) -namespace trussc { namespace internal { - inline bool inFboPass = false; - - // Restore the current blend pipeline after temporary pipeline changes. - // FBO uses its accumulating Fill2D; swapchain honors the current blend mode. - inline void restoreCurrentPipeline() { - loadPipeline(inFboPass ? activeFill2D() : active2D(currentBlendMode)); - } -}} +// Per-window state container (input/hover/camera/pass state) + the active*() +// pipeline helpers and restoreCurrentPipeline() (used in tcRenderContext.h). +#include "tc/app/tcWindowContext.h" // VertexWriter abstraction (for shader integration) #include "tc/graphics/tcVertexWriter.h" @@ -233,8 +215,8 @@ namespace trussc { // Drawing state has been moved to RenderContext // --------------------------------------------------------------------------- namespace internal { - // Clipboard buffer size (for overflow check) - inline int clipboardSize = 65536; + // clipboardSize / ScissorRect / scissorStack / currentScissor moved to + // WindowContext (tc/app/tcWindowContext.h). // sokol_gl vertex buffer management (auto-grows on overflow) inline int sglMaxVertices = 65536; @@ -247,16 +229,6 @@ namespace internal { // reserveUniformBuffer for scenes with very high draw-call counts. inline int gpuUniformBufferReserve = 0; - // --------------------------------------------------------------------------- - // Scissor Clipping stack - // --------------------------------------------------------------------------- - struct ScissorRect { - float x, y, w, h; - bool active; // Whether a valid range exists in the stack - }; - inline std::vector scissorStack; - inline ScissorRect currentScissor = {0, 0, 0, 0, false}; - // --------------------------------------------------------------------------- // Loop Architecture (Decoupled Update/Draw) // --------------------------------------------------------------------------- @@ -281,11 +253,8 @@ namespace internal { inline bool lastDrawTimeInitialized = false; inline double drawAccumulator = 0.0; - // Mouse position state (mouseX/Y, pmouseX/Y) + window-space getters now live - // in tc/app/tcMouseGlobal.h (included above) so lower-level headers can use - // them directly. Button/pressed state stays here. - inline int mouseButton = -1; // Currently pressed button (-1 = none) - inline bool mousePressed = false; + // Mouse position/button state + keyboard state moved to WindowContext + // (tc/app/tcWindowContext.h); window-space getters in tc/app/tcMouseGlobal.h. // Touch-as-mouse mapping // Default ON everywhere — the first touch synthesizes mouse press/drag, so @@ -298,18 +267,7 @@ namespace internal { inline EventListener touchMovedListener; inline EventListener touchReleasedListener; - // Keyboard state - inline std::unordered_set keysPressed; - - // Delta time (actual elapsed time since last update call) - inline double updateDeltaTime = 0.0; - inline std::chrono::high_resolution_clock::time_point lastUpdateCallTime; - inline bool lastUpdateCallTimeInitialized = false; - - // Frame rate measurement (10-frame moving average) - inline double frameTimeBuffer[10] = {}; - inline int frameTimeIndex = 0; - inline bool frameTimeBufferFilled = false; + // Delta time / frame-rate state lives in WindowContext (per window). // Frame count (number of update calls) inline uint64_t updateFrameCount = 0; @@ -318,22 +276,8 @@ namespace internal { inline std::chrono::high_resolution_clock::time_point startTime; inline bool startTimeInitialized = false; - // Pass state (for suspending swapchain pass for FBO) - inline bool inSwapchainPass = false; - - // Saved clear color for resume after FBO suspend (set by clear()) - inline sg_color swapchainClearValue = { 0.0f, 0.0f, 0.0f, 1.0f }; - - // The Metal CAMetalDrawable acquired for THIS frame's swapchain pass. sokol's - // sapp_get_swapchain() advances to the next drawable on every call (it must be - // called exactly once per frame), so end-of-frame capture must NOT call it - // again — it would grab a different, unrendered drawable. Instead the swapchain - // pass setup records the drawable it actually rendered into here, and - // captureWindow() reads back from this one. (Metal-only; on D3D11/GL the - // swapchain handle is a stable backbuffer/FBO so this stays null and is unused.) - inline const void* lastSwapchainDrawable = nullptr; - - // inFboPass is declared earlier (before tcRenderContext.h) + // Pass state (inSwapchainPass / swapchainClearValue / lastSwapchainDrawable / + // inFboPass) moved to WindowContext (tc/app/tcWindowContext.h). // FBO clearColor function pointer (set in tcFbo.h) inline void (*fboClearColorFunc)(float, float, float, float) = nullptr; @@ -365,17 +309,21 @@ void cleanup(); // --------------------------------------------------------------------------- // Get DPI scale (e.g., 2.0 for Retina displays) +// Routed through the active window context (secondary windows keep their own). inline float getDpiScale() { - return sapp_dpi_scale(); + auto& ctx = internal::currentWindowContext(); + return ctx.isMain ? sapp_dpi_scale() : ctx.dpiScale; } // Get actual framebuffer size (in pixels) inline int getFramebufferWidth() { - return sapp_width(); + auto& ctx = internal::currentWindowContext(); + return ctx.isMain ? sapp_width() : ctx.fbWidth; } inline int getFramebufferHeight() { - return sapp_height(); + auto& ctx = internal::currentWindowContext(); + return ctx.isMain ? sapp_height() : ctx.fbHeight; } // Call at frame start (before clear) @@ -552,7 +500,7 @@ inline void intersectRect(float x1, float y1, float w1, float h1, // Set scissor rectangle (screen coordinates) inline void setScissor(float x, float y, float w, float h) { - internal::currentScissor = {x, y, w, h, true}; + internal::currentWindowContext().currentScissor = {x, y, w, h, true}; sgl_scissor_rectf(x, y, w, h, true); // origin_top_left = true } @@ -563,20 +511,20 @@ inline void setScissor(int x, int y, int w, int h) { // Reset scissor to entire window inline void resetScissor() { - internal::currentScissor.active = false; - sgl_scissor_rect(0, 0, sapp_width(), sapp_height(), true); + internal::currentWindowContext().currentScissor.active = false; + sgl_scissor_rect(0, 0, getFramebufferWidth(), getFramebufferHeight(), true); } // Save scissor to stack and set new range (intersection with current range) inline void pushScissor(float x, float y, float w, float h) { // Save current state to stack - internal::scissorStack.push_back(internal::currentScissor); + internal::currentWindowContext().scissorStack.push_back(internal::currentWindowContext().currentScissor); // Calculate new range (intersection with current range) - if (internal::currentScissor.active) { + if (internal::currentWindowContext().currentScissor.active) { float nx, ny, nw, nh; - intersectRect(internal::currentScissor.x, internal::currentScissor.y, - internal::currentScissor.w, internal::currentScissor.h, + intersectRect(internal::currentWindowContext().currentScissor.x, internal::currentWindowContext().currentScissor.y, + internal::currentWindowContext().currentScissor.w, internal::currentWindowContext().currentScissor.h, x, y, w, h, nx, ny, nw, nh); setScissor(nx, ny, nw, nh); @@ -587,19 +535,19 @@ inline void pushScissor(float x, float y, float w, float h) { // Restore scissor from stack inline void popScissor() { - if (internal::scissorStack.empty()) { + if (internal::currentWindowContext().scissorStack.empty()) { resetScissor(); return; } - internal::currentScissor = internal::scissorStack.back(); - internal::scissorStack.pop_back(); + internal::currentWindowContext().currentScissor = internal::currentWindowContext().scissorStack.back(); + internal::currentWindowContext().scissorStack.pop_back(); - if (internal::currentScissor.active) { - sgl_scissor_rectf(internal::currentScissor.x, internal::currentScissor.y, - internal::currentScissor.w, internal::currentScissor.h, true); + if (internal::currentWindowContext().currentScissor.active) { + sgl_scissor_rectf(internal::currentWindowContext().currentScissor.x, internal::currentWindowContext().currentScissor.y, + internal::currentWindowContext().currentScissor.w, internal::currentWindowContext().currentScissor.h, true); } else { - sgl_scissor_rect(0, 0, sapp_width(), sapp_height(), true); + sgl_scissor_rect(0, 0, getFramebufferWidth(), getFramebufferHeight(), true); } } @@ -613,16 +561,16 @@ inline void popScissor() { // Set blend mode // Alpha channel is additive in all modes (to prevent transparency when drawing to FBO) inline void setBlendMode(BlendMode mode) { - if (internal::swapchainTarget.context.id == 0) return; // renderer not set up yet + if (internal::currentWindowContext().swapchainTarget.context.id == 0) return; // renderer not set up yet // Skip in FBO - FBO uses its own pipeline - if (internal::inFboPass) return; - internal::currentBlendMode = mode; + if (internal::currentWindowContext().inFboPass) return; + internal::currentWindowContext().currentBlendMode = mode; internal::loadPipeline(internal::active2D(mode)); } // Get current blend mode inline BlendMode getBlendMode() { - return internal::currentBlendMode; + return internal::currentWindowContext().currentBlendMode; } // Reset to default blend mode (Alpha) @@ -632,7 +580,7 @@ inline void resetBlendMode() { // Restore current blend mode pipeline (use after temporary pipeline changes) inline void restoreBlendPipeline() { - internal::loadPipeline(internal::active2D(internal::currentBlendMode)); + internal::loadPipeline(internal::active2D(internal::currentWindowContext().currentBlendMode)); } // --------------------------------------------------------------------------- @@ -681,10 +629,10 @@ namespace internal { float dist = viewH / (2.0f * theTan); // Save current screen state for 2D drawing - currentScreenFov = fovDeg; - currentViewW = viewW; - currentViewH = viewH; - currentCameraDist = dist; + internal::currentWindowContext().currentScreenFov = fovDeg; + internal::currentWindowContext().currentViewW = viewW; + internal::currentWindowContext().currentViewH = viewH; + internal::currentWindowContext().currentCameraDist = dist; // Apply clip overrides or auto-calculate if (nearDist == 0.0f) nearDist = (nearClipOverride > 0.0f) ? nearClipOverride : dist / 10.0f; @@ -714,8 +662,8 @@ namespace internal { ); // Save matrices for worldToScreen/screenToWorld - currentProjectionMatrix = Mat4::ortho(-viewW / 2.0f, viewW / 2.0f, viewH / 2.0f, -viewH / 2.0f, -farDist, farDist); - currentViewMatrix = Mat4::lookAt( + internal::currentWindowContext().currentProjectionMatrix = Mat4::ortho(-viewW / 2.0f, viewW / 2.0f, viewH / 2.0f, -viewH / 2.0f, -farDist, farDist); + internal::currentWindowContext().currentViewMatrix = Mat4::lookAt( Vec3(eyeX, eyeY, dist), Vec3(eyeX, eyeY, 0.0f), Vec3(0.0f, 1.0f, 0.0f) @@ -747,8 +695,8 @@ namespace internal { ); // Save matrices for worldToScreen/screenToWorld - currentProjectionMatrix = Mat4::frustum(left, right, top, bottom, nearDist, farDist); - currentViewMatrix = Mat4::lookAt( + internal::currentWindowContext().currentProjectionMatrix = Mat4::frustum(left, right, top, bottom, nearDist, farDist); + internal::currentWindowContext().currentViewMatrix = Mat4::lookAt( Vec3(eyeX, eyeY, dist), Vec3(eyeX, eyeY, 0.0f), Vec3(0.0f, 1.0f, 0.0f) @@ -757,14 +705,16 @@ namespace internal { // Register this camera scope so nodes drawn from here on stamp it // (draw-time stamping → per-context pick rays; see tcCameraContext.h). - registerCameraContext(currentViewMatrix, currentProjectionMatrix, viewW, viewH, pickable); + registerCameraContext(internal::currentWindowContext().currentViewMatrix, internal::currentWindowContext().currentProjectionMatrix, viewW, viewH, pickable); } // Wrapper that uses main screen size inline void setupScreenFov(float fovDeg, float nearDist, float farDist) { - float dpiScale = sapp_dpi_scale(); - float viewW = pixelPerfectMode ? (float)sapp_width() : (float)sapp_width() / dpiScale; - float viewH = pixelPerfectMode ? (float)sapp_height() : (float)sapp_height() / dpiScale; + // Per-window: framebuffer size and dpi come from the ACTIVE window + // context (a secondary window on another display has its own scale). + float dpiScale = getDpiScale(); + float viewW = pixelPerfectMode ? (float)getFramebufferWidth() : getFramebufferWidth() / dpiScale; + float viewH = pixelPerfectMode ? (float)getFramebufferHeight() : getFramebufferHeight() / dpiScale; setupScreenFovWithSize(fovDeg, viewW, viewH, nearDist, farDist); } } // namespace internal @@ -822,13 +772,13 @@ inline float getFarClip() { /// Returns Vec3: x, y = screen position (pixels from top-left), z = normalized depth [0, 1] inline Vec3 worldToScreen(const Vec3& worldPos) { // Get current viewport dimensions - float viewW = internal::currentViewW; - float viewH = internal::currentViewH; + float viewW = internal::currentWindowContext().currentViewW; + float viewH = internal::currentWindowContext().currentViewH; if (viewW == 0) viewW = (float)sapp_width() / sapp_dpi_scale(); if (viewH == 0) viewH = (float)sapp_height() / sapp_dpi_scale(); // Transform world -> clip space - Mat4 mvp = internal::currentProjectionMatrix * internal::currentViewMatrix; + Mat4 mvp = internal::currentWindowContext().currentProjectionMatrix * internal::currentWindowContext().currentViewMatrix; Vec4 clip = mvp * Vec4(worldPos.x, worldPos.y, worldPos.z, 1.0f); // Perspective division @@ -851,8 +801,8 @@ inline Vec3 worldToScreen(const Vec3& worldPos) { /// worldZ: target Z plane (default = 0) inline Vec3 screenToWorld(const Vec2& screenPos, float worldZ = 0.0f) { // Get current viewport dimensions - float viewW = internal::currentViewW; - float viewH = internal::currentViewH; + float viewW = internal::currentWindowContext().currentViewW; + float viewH = internal::currentWindowContext().currentViewH; if (viewW == 0) viewW = (float)sapp_width() / sapp_dpi_scale(); if (viewH == 0) viewH = (float)sapp_height() / sapp_dpi_scale(); @@ -861,7 +811,7 @@ inline Vec3 screenToWorld(const Vec2& screenPos, float worldZ = 0.0f) { float ndcY = 1.0f - (screenPos.y / viewH) * 2.0f; // Flip Y // Create inverse MVP matrix - Mat4 mvp = internal::currentProjectionMatrix * internal::currentViewMatrix; + Mat4 mvp = internal::currentWindowContext().currentProjectionMatrix * internal::currentWindowContext().currentViewMatrix; Mat4 invMvp = mvp.inverted(); // Unproject two points: near plane (z=-1) and a middle point (z=0) @@ -1311,7 +1261,7 @@ inline void drawBitmapStringHighlight(const std::string& text, float x, float y, sgl_matrix_mode_projection(); sgl_push_matrix(); sgl_load_identity(); - sgl_ortho(0.0f, internal::currentViewW, internal::currentViewH, 0.0f, -10000.0f, 10000.0f); + sgl_ortho(0.0f, internal::currentWindowContext().currentViewW, internal::currentWindowContext().currentViewH, 0.0f, -10000.0f, 10000.0f); sgl_matrix_mode_modelview(); sgl_push_matrix(); sgl_load_identity(); @@ -1361,8 +1311,8 @@ inline void drawBitmapStringHighlight(const std::string& text, float x, float y, // Restore current blend pipeline (not default, which has blend disabled). // FBO uses its accumulating Fill2D; swapchain honors the current blend mode. - internal::loadPipeline(internal::inFboPass ? internal::activeFill2D() - : internal::active2D(internal::currentBlendMode)); + internal::loadPipeline(internal::currentWindowContext().inFboPass ? internal::activeFill2D() + : internal::active2D(internal::currentWindowContext().currentBlendMode)); // Restore matrices sgl_pop_matrix(); @@ -1466,9 +1416,9 @@ inline void unbindCursorImage(Cursor cursor) { // Copy string to clipboard inline void setClipboardString(const std::string& text) { - if (static_cast(text.size()) >= internal::clipboardSize) { + if (static_cast(text.size()) >= internal::currentWindowContext().clipboardSize) { logWarning("Clipboard") << "Text truncated (" << text.size() << " bytes > " - << internal::clipboardSize << " buffer). Use WindowSettings::setClipboardSize() to increase."; + << internal::currentWindowContext().clipboardSize << " buffer). Use WindowSettings::setClipboardSize() to increase."; } sapp_set_clipboard_string(text.c_str()); } @@ -1545,17 +1495,17 @@ TC_PLATFORMS("android,ios") void setOrientation(Orientation mask); // Get window width (size corresponding to coordinate system) inline int getWindowWidth() { if (internal::pixelPerfectMode) { - return sapp_width(); // Framebuffer size + return getFramebufferWidth(); // Framebuffer size } - return static_cast(sapp_width() / sapp_dpi_scale()); // Logical size + return static_cast(getFramebufferWidth() / getDpiScale()); // Logical size } // Get window height (size corresponding to coordinate system) inline int getWindowHeight() { if (internal::pixelPerfectMode) { - return sapp_height(); // Framebuffer size + return getFramebufferHeight(); // Framebuffer size } - return static_cast(sapp_height() / sapp_dpi_scale()); // Logical size + return static_cast(getFramebufferHeight() / getDpiScale()); // Logical size } // Get window size as Vec2 @@ -1604,17 +1554,17 @@ inline void releaseSglBuffers() { // Is mouse button pressed inline bool isMousePressed() { - return internal::mousePressed; + return internal::currentWindowContext().mousePressed; } // Currently pressed mouse button (-1 = none) inline int getMouseButton() { - return internal::mouseButton; + return internal::currentWindowContext().mouseButton; } // Is specific key currently pressed inline bool isKeyPressed(int key) { - return internal::keysPressed.count(key) > 0; + return internal::currentWindowContext().keysPressed.count(key) > 0; } // "Either-side" modifier checks. Return true while either the left or @@ -1849,8 +1799,7 @@ namespace internal { // Relative paths resolve against the data path. Supported formats: png/jpg/bmp. inline bool saveScreenshot(const std::filesystem::path& path) { // Resolve relative paths up front so the deferred worker gets an absolute one. - std::filesystem::path resolved = - path.is_relative() ? std::filesystem::path(getDataPath(path.string())) : path; + std::filesystem::path resolved = getDataPath(path); // absolute passes through // Auto-create the parent directory (mirrors VideoRecorder). This is the // failure users want to catch synchronously (missing/unwritable folder). @@ -2062,6 +2011,17 @@ namespace internal { // key off this. (sokol's init_cb runs on the main thread.) getMainThreadId(); + // Ops integration: a supervisor (e.g. `anchorbolt start`) injects a log + // file path via the environment so the app needs zero code changes. + // Opened BEFORE setup() so setup-time log lines land in the file too. + #ifndef __EMSCRIPTEN__ + if (const char* envLog = std::getenv("TRUSSC_LOG_FILE")) { + if (envLog[0] != '\0' && !setLogFile(envLog)) { + logWarning("System") << "TRUSSC_LOG_FILE: cannot open '" << envLog << "'"; + } + } + #endif + setup(); // The Apple data path root is chosen lazily on first getDataPath() use @@ -2175,16 +2135,19 @@ namespace internal { mcp::processHttpQueue(); #endif - // Compute update delta time (actual elapsed since last update call) + // Compute update delta time (actual elapsed since last update call). + // Written into the main window's context; secondary windows measure + // their own delta in their tick (tcWindowMac.mm). auto computeUpdateDelta = [&]() { - if (!lastUpdateCallTimeInitialized) { - lastUpdateCallTimeInitialized = true; - lastUpdateCallTime = now; - updateDeltaTime = sapp_frame_duration(); // first frame: use sokol's estimate + auto& wctx = internal::currentWindowContext(); + if (!wctx.lastUpdateCallTimeInitialized) { + wctx.lastUpdateCallTimeInitialized = true; + wctx.lastUpdateCallTime = now; + wctx.updateDeltaTime = sapp_frame_duration(); // first frame: use sokol's estimate } else { auto callNow = std::chrono::high_resolution_clock::now(); - updateDeltaTime = std::chrono::duration(callNow - lastUpdateCallTime).count(); - lastUpdateCallTime = callNow; + wctx.updateDeltaTime = std::chrono::duration(callNow - wctx.lastUpdateCallTime).count(); + wctx.lastUpdateCallTime = callNow; } }; @@ -2238,7 +2201,7 @@ namespace internal { } // Force a frame when a capture is pending so present()/afterFrame runs - // and the deferred screenshot (or MCP get_screenshot) actually fires — + // and the deferred screenshot (or MCP tc_get_screenshot) actually fires — // otherwise a request arriving in a paused/event-driven app would never // be served and a blocked MCP HTTP worker would hang. if (!pendingScreenshotPaths.empty() @@ -2279,8 +2242,8 @@ namespace internal { } // Save previous frame's mouse position - pmouseX = mouseX; - pmouseY = mouseY; + internal::currentWindowContext().pmouseX = internal::currentWindowContext().mouseX; + internal::currentWindowContext().pmouseY = internal::currentWindowContext().mouseY; frameReentryGuard = false; } @@ -2325,7 +2288,7 @@ namespace internal { // Track key state (only on first press, not auto-repeat) if (!ev->key_repeat) { - keysPressed.insert(ev->key_code); + internal::currentWindowContext().keysPressed.insert(ev->key_code); } // Forward to App / Node tree. Fire on auto-repeat too @@ -2344,21 +2307,21 @@ namespace internal { events().keyReleased.notify(args); // Track key state - keysPressed.erase(ev->key_code); + internal::currentWindowContext().keysPressed.erase(ev->key_code); if (appKeyReleasedFunc) appKeyReleasedFunc(args); break; } case SAPP_EVENTTYPE_MOUSE_DOWN: { currentMouseButton = ev->mouse_button; - mouseX = ev->mouse_x * scale; - mouseY = ev->mouse_y * scale; - mouseButton = ev->mouse_button; - mousePressed = true; + internal::currentWindowContext().mouseX = ev->mouse_x * scale; + internal::currentWindowContext().mouseY = ev->mouse_y * scale; + internal::currentWindowContext().mouseButton = ev->mouse_button; + internal::currentWindowContext().mousePressed = true; // App-level args: no node transform, so pos == globalPos. MouseEventArgs args; - args.pos = args.globalPos = Vec2(mouseX, mouseY); + args.pos = args.globalPos = Vec2(internal::currentWindowContext().mouseX, internal::currentWindowContext().mouseY); args.button = ev->mouse_button; args.shift = hasModShift; args.ctrl = hasModCtrl; @@ -2372,13 +2335,13 @@ namespace internal { } case SAPP_EVENTTYPE_MOUSE_UP: { currentMouseButton = -1; - mouseX = ev->mouse_x * scale; - mouseY = ev->mouse_y * scale; - mouseButton = -1; - mousePressed = false; + internal::currentWindowContext().mouseX = ev->mouse_x * scale; + internal::currentWindowContext().mouseY = ev->mouse_y * scale; + internal::currentWindowContext().mouseButton = -1; + internal::currentWindowContext().mousePressed = false; MouseEventArgs args; - args.pos = args.globalPos = Vec2(mouseX, mouseY); + args.pos = args.globalPos = Vec2(internal::currentWindowContext().mouseX, internal::currentWindowContext().mouseY); args.button = ev->mouse_button; args.shift = hasModShift; args.ctrl = hasModCtrl; @@ -2391,16 +2354,16 @@ namespace internal { break; } case SAPP_EVENTTYPE_MOUSE_MOVE: { - float prevX = mouseX; - float prevY = mouseY; - mouseX = ev->mouse_x * scale; - mouseY = ev->mouse_y * scale; + float prevX = internal::currentWindowContext().mouseX; + float prevY = internal::currentWindowContext().mouseY; + internal::currentWindowContext().mouseX = ev->mouse_x * scale; + internal::currentWindowContext().mouseY = ev->mouse_y * scale; // Rich carrier (internal::MouseEventRaw); the per-kind public type is // built from it at the boundary (internal::toDragArgs / internal::toMoveArgs). internal::MouseEventRaw args; - args.pos = args.globalPos = Vec2(mouseX, mouseY); - args.delta = args.globalDelta = Vec2(mouseX - prevX, mouseY - prevY); + args.pos = args.globalPos = Vec2(internal::currentWindowContext().mouseX, internal::currentWindowContext().mouseY); + args.delta = args.globalDelta = Vec2(internal::currentWindowContext().mouseX - prevX, internal::currentWindowContext().mouseY - prevY); args.shift = hasModShift; args.ctrl = hasModCtrl; args.alt = hasModAlt; @@ -2424,7 +2387,7 @@ namespace internal { } case SAPP_EVENTTYPE_MOUSE_SCROLL: { ScrollEventArgs args; - args.pos = args.globalPos = Vec2(mouseX, mouseY); + args.pos = args.globalPos = Vec2(internal::currentWindowContext().mouseX, internal::currentWindowContext().mouseY); args.scroll = Vec2(ev->scroll_x, ev->scroll_y); args.shift = hasModShift; args.ctrl = hasModCtrl; @@ -2470,9 +2433,9 @@ namespace internal { if (ev->type == SAPP_EVENTTYPE_TOUCHES_BEGAN) { currentMouseButton = 0; - mouseX = tx; mouseY = ty; - mouseButton = 0; - mousePressed = true; + internal::currentWindowContext().mouseX = tx; internal::currentWindowContext().mouseY = ty; + internal::currentWindowContext().mouseButton = 0; + internal::currentWindowContext().mousePressed = true; MouseEventArgs margs; margs.pos = margs.globalPos = Vec2(tx, ty); @@ -2481,8 +2444,8 @@ namespace internal { events().mousePressed.notify(margs); if (appMousePressedFunc) appMousePressedFunc(margs); } else if (ev->type == SAPP_EVENTTYPE_TOUCHES_MOVED) { - float prevX = mouseX, prevY = mouseY; - mouseX = tx; mouseY = ty; + float prevX = internal::currentWindowContext().mouseX, prevY = internal::currentWindowContext().mouseY; + internal::currentWindowContext().mouseX = tx; internal::currentWindowContext().mouseY = ty; internal::MouseEventRaw margs; margs.pos = margs.globalPos = Vec2(tx, ty); @@ -2494,9 +2457,9 @@ namespace internal { if (appMouseDraggedFunc) appMouseDraggedFunc(margs); } else { currentMouseButton = -1; - mouseX = tx; mouseY = ty; - mouseButton = -1; - mousePressed = false; + internal::currentWindowContext().mouseX = tx; internal::currentWindowContext().mouseY = ty; + internal::currentWindowContext().mouseButton = -1; + internal::currentWindowContext().mousePressed = false; MouseEventArgs margs; margs.pos = margs.globalPos = Vec2(tx, ty); @@ -2527,8 +2490,8 @@ namespace internal { } case SAPP_EVENTTYPE_FILES_DROPPED: { DragDropEventArgs args; - args.x = mouseX; // Last mouse position - args.y = mouseY; + args.x = internal::currentWindowContext().mouseX; // Last mouse position + args.y = internal::currentWindowContext().mouseY; int numFiles = sapp_get_num_dropped_files(); for (int i = 0; i < numFiles; i++) { args.files.push_back(sapp_get_dropped_file_path(i)); @@ -2591,7 +2554,7 @@ sapp_desc buildAppDescriptor(const WindowSettings& settings = WindowSettings()) internal::updateFrameCount++; // Update frame count events().update.notify(); if (app) { - app->handleUpdate(internal::mouseX, internal::mouseY); + app->handleUpdate(internal::currentWindowContext().mouseX, internal::currentWindowContext().mouseY); } }; internal::appDrawFunc = []() { @@ -2679,7 +2642,7 @@ sapp_desc buildAppDescriptor(const WindowSettings& settings = WindowSettings()) // Enable clipboard desc.enable_clipboard = true; desc.clipboard_size = settings.clipboardSize; - internal::clipboardSize = settings.clipboardSize; + internal::currentWindowContext().clipboardSize = settings.clipboardSize; return desc; } @@ -2956,5 +2919,8 @@ namespace tc = trussc; // using namespace std; // using namespace tc; +// Secondary windows (multi-window; needs Node/CoreEvents/WindowSettings above) +#include "tc/app/tcWindow.h" + // TrussC standard MCP tools (included last to resolve dependencies) #include "tc/utils/tcStandardTools.h" diff --git a/core/include/impl/stb_impl.cpp b/core/include/impl/stb_impl.cpp index 8cca4008..193a1da7 100644 --- a/core/include/impl/stb_impl.cpp +++ b/core/include/impl/stb_impl.cpp @@ -10,6 +10,12 @@ # pragma GCC diagnostic ignored "-Wunused-function" #endif +// Treat filenames as UTF-8 on Windows (stb converts to the wide API +// internally). No effect on other platforms. Callers pass UTF-8 via +// internal::pathToUtf8() — see tc/utils/tcFileIO.h. +#define STBI_WINDOWS_UTF8 +#define STBIW_WINDOWS_UTF8 + #define STB_IMAGE_IMPLEMENTATION #include "stb/stb_image.h" diff --git a/core/include/sokol/TRUSSC_MODIFICATIONS.md b/core/include/sokol/TRUSSC_MODIFICATIONS.md index 3703608e..c972d648 100644 --- a/core/include/sokol/TRUSSC_MODIFICATIONS.md +++ b/core/include/sokol/TRUSSC_MODIFICATIONS.md @@ -13,7 +13,9 @@ Search for `tettou771` or `Modified by` or `[TrussC` to find all modified sectio ``` sokol/ -├── sokol_app.h # Modified (10 patches) +├── sokol_app_tc.h # TrussC-owned fork: full sapp_* implementation on +│ # every platform + multi-window API (sokol_app.h +│ # no longer exists in this tree — see below) ├── sokol_gfx.h # Untouched (direct copy from upstream) ├── sokol_glue.h # Modified (1 patch) ├── sokol_log.h # Untouched @@ -31,7 +33,25 @@ Matches upstream directory layout: core headers at root, utility headers in `uti --- -## sokol_app.h +## sokol_app_tc.h (replaces sokol_app.h) + +**sokol_app.h was deleted from this tree (2026-07, "sokol_app graduation").** +`sokol_app_tc.h` is a self-contained TrussC-owned fork that implements the +complete `sapp_*` public API (declarations absorbed verbatim, zlib license +kept) plus the multi-window API on every platform: macOS (multi-window, +NSWindow+CAMetalLayer+CADisplayLink), Windows (multi-window, HWND+D3D11+DXGI +waitable), desktop Linux (multi-window, X11+GLX), GLES3 Linux / Raspberry Pi +(single-window, X11+EGL), web (single-window, rAF + WebGPU/WebGL2), iOS +(single-window, UIWindowScene+Metal) and Android (single-window, +NativeActivity+EGL+Choreographer). Design + per-platform behavioral contracts: +`docs/dev/sokol_app_tc-design.md` and `docs/dev/sapp-*-impl-spec.md`. + +It is NOT updated from upstream by 3-way merge — it is a permanent fork; +upstream sokol_app changes are cherry-picked deliberately when wanted. + +The former sokol_app.h patches below are **native behavior** of +sokol_app_tc.h now (kept here as historical record of what differs from +upstream semantics): ### 1. Skip Present (D3D11 flickering fix) @@ -203,10 +223,15 @@ These functions do NOT exist in upstream sokol_gl. They are TrussC additions. ## How to Update Sokol -For files with TrussC patches (sokol_app.h, sokol_glue.h, util/sokol_gl_tc.h), +For files with TrussC patches (sokol_glue.h, util/sokol_gl_tc.h), **use `git merge-file` as a 3-way merge** instead of overwriting and manually re-applying patches. This avoids slip bugs from manual patch transcription. +`sokol_app_tc.h` is NOT part of this process — it is a permanent fork, not a +patched upstream copy. Pull upstream sokol_app improvements into it by +deliberate cherry-pick (the per-platform impl specs in docs/dev/ map upstream +line ranges to the tc sections). + ### Recommended: 3-way merge for patched files ```bash @@ -214,20 +239,19 @@ re-applying patches. This avoids slip bugs from manual patch transcription. # BASE — upstream sokol at the commit recorded above (pre-patch) # OURS — current TrussC copy with all patches # THEIRS — new upstream master we want to update to -(cd ; git show :sokol_app.h) > /tmp/BASE.h -cp /sokol_app.h /tmp/THEIRS.h -cp core/include/sokol/sokol_app.h /tmp/OURS.h +(cd ; git show :sokol_glue.h) > /tmp/BASE.h +cp /sokol_glue.h /tmp/THEIRS.h +cp core/include/sokol/sokol_glue.h /tmp/OURS.h # Run merge; conflicts (if any) end up as <<<<<<< / ======= / >>>>>>> blocks -cp /tmp/OURS.h /tmp/MERGED.h git merge-file --diff-algorithm=histogram -p /tmp/OURS.h /tmp/BASE.h /tmp/THEIRS.h > /tmp/MERGED.h # Resolve conflicts in /tmp/MERGED.h, then copy back -cp /tmp/MERGED.h core/include/sokol/sokol_app.h +cp /tmp/MERGED.h core/include/sokol/sokol_glue.h ``` -Repeat for `sokol_glue.h` and `util/sokol_gl_tc.h` (use upstream `util/sokol_gl.h` -as both BASE and THEIRS, since sokol_gl_tc.h is the renamed fork). +Repeat for `util/sokol_gl_tc.h` (use upstream `util/sokol_gl.h` as BASE and +THEIRS, since sokol_gl_tc.h is the renamed fork). ### Direct overwrite (for files without TrussC patches) diff --git a/core/include/sokol/sokol_app.h b/core/include/sokol/sokol_app.h deleted file mode 100644 index 825210be..00000000 --- a/core/include/sokol/sokol_app.h +++ /dev/null @@ -1,14602 +0,0 @@ -#if defined(SOKOL_IMPL) && !defined(SOKOL_APP_IMPL) -#define SOKOL_APP_IMPL -#endif -#ifndef SOKOL_APP_INCLUDED -/* - sokol_app.h -- cross-platform application wrapper - - Project URL: https://github.com/floooh/sokol - - Do this: - #define SOKOL_IMPL or - #define SOKOL_APP_IMPL - before you include this file in *one* C or C++ file to create the - implementation. - - In the same place define one of the following to select the 3D-API - which should be initialized by sokol_app.h (this must also match - the backend selected for sokol_gfx.h if both are used in the same - project): - - #define SOKOL_GLCORE - #define SOKOL_GLES3 - #define SOKOL_D3D11 - #define SOKOL_METAL - #define SOKOL_WGPU - #define SOKOL_VULKAN - #define SOKOL_NOAPI - - Optionally provide the following defines with your own implementations: - - SOKOL_ASSERT(c) - your own assert macro (default: assert(c)) - SOKOL_UNREACHABLE() - a guard macro for unreachable code (default: assert(false)) - SOKOL_WIN32_FORCE_MAIN - define this on Win32 to add a main() entry point - SOKOL_WIN32_FORCE_WINMAIN - define this on Win32 to add a WinMain() entry point (enabled by default unless - SOKOL_WIN32_FORCE_MAIN or SOKOL_NO_ENTRY is defined) - SOKOL_NO_ENTRY - define this if sokol_app.h shouldn't "hijack" the main() function - SOKOL_APP_API_DECL - public function declaration prefix (default: extern) - SOKOL_API_DECL - same as SOKOL_APP_API_DECL - SOKOL_API_IMPL - public function implementation prefix (default: -) - - Optionally define the following to force debug checks and validations - even in release mode: - - SOKOL_DEBUG - by default this is defined if NDEBUG is not defined - - If sokol_app.h is compiled as a DLL, define the following before - including the declaration or implementation: - - SOKOL_DLL - - On Windows, SOKOL_DLL will define SOKOL_APP_API_DECL as __declspec(dllexport) - or __declspec(dllimport) as needed. - - if SOKOL_WIN32_FORCE_MAIN and SOKOL_WIN32_FORCE_WINMAIN are both defined, - it is up to the developer to define the desired subsystem. - - On Linux, SOKOL_GLCORE can use either GLX or EGL. - GLX is default, set SOKOL_FORCE_EGL to override. - - For example code, see https://github.com/floooh/sokol-samples/tree/master/sapp - - Portions of the Windows and Linux GL initialization, event-, icon- etc... code - have been taken from GLFW (http://www.glfw.org/). - - iOS onscreen keyboard support 'inspired' by libgdx. - - Link with the following system libraries: - - - on macOS: - - all backends: AppKit, QuartzCore - - with SOKOL_METAL: Metal - - with SOKOL_GLCORE: OpenGL - - with SOKOL_WGPU: a WebGPU implementation library (tested with webgpu_dawn) - - on iOS: - - all backends: Foundation, UIKit, QuartzCore - - with SOKOL_METAL: Metal - - with SOKOL_GLES3: OpenGLES, GLKit - - on Linux: - - all backends: X11, Xi, Xcursor, dl, pthread, m - - with SOKOL_GLCORE: GL - - with SOKOL_GLES3: GLESv2 - - with SOKOL_WGPU: a WebGPU implementation library (tested with webgpu_dawn) - - with SOKOL_VULKAN: vulkan - - with EGL: EGL - - on Android: GLESv3, EGL, log, android - - on Windows: - - with MSVC or Clang: library dependencies are defined via `#pragma comment` - - with SOKOL_WGPU: a WebGPU implementation library (tested with webgpu_dawn) - - with SOKOL_VULKAN: - - install the Vulkan SDK - - set a header search path to $VULKAN_SDK/Include - - set a library search path to $VULKAN_SDK/Lib - - link with vulkan-1.lib - - with MINGW/MSYS2 gcc: - - compile with '-mwin32' so that _WIN32 is defined - - link with the following libs: -lkernel32 -luser32 -lshell32 - - additionally with the GL backend: -lgdi32 - - additionally with the D3D11 backend: -ld3d11 -ldxgi - - On Linux, you also need to use the -pthread compiler and linker option, otherwise weird - things will happen, see here for details: https://github.com/floooh/sokol/issues/376 - - For Linux+Vulkan install the following packages (or equivalents): - - libvulkan-dev - - vulkan-validationlayers - - vulkan-tools - - On macOS and iOS, the implementation must be compiled as Objective-C. - - On Emscripten: - - for WebGL2: add the linker option `-s USE_WEBGL2=1` - - for WebGPU: compile and link with `--use-port=emdawnwebgpu` - (for more exotic situations read: https://dawn.googlesource.com/dawn/+/refs/heads/main/src/emdawnwebgpu/pkg/README.md) - - FEATURE OVERVIEW - ================ - sokol_app.h provides a minimalistic cross-platform API which - implements the 'application-wrapper' parts of a 3D application: - - - a common application entry function - - creates a window and 3D-API context/device with a swapchain - surface, depth-stencil-buffer surface and optionally MSAA surface - - makes the rendered frame visible - - provides keyboard-, mouse- and low-level touch-events - - platforms: MacOS, iOS, HTML5, Win32, Linux/RaspberryPi, Android - - 3D-APIs: Metal, D3D11, GL4.1, GL4.3, GLES3, WebGL2, WebGPU, NOAPI - - FEATURE/PLATFORM MATRIX - ======================= - | Windows | macOS | Linux | iOS | Android | HTML5 - --------------------+---------+-------+-------+-------+---------+-------- - gl 4.x | YES | YES | YES | --- | --- | --- - gles3/webgl2 | --- | --- | YES(2)| YES | YES | YES - metal | --- | YES | --- | YES | --- | --- - d3d11 | YES | --- | --- | --- | --- | --- - webgpu | YES(4) | YES(4)| YES(4)| NO | NO | YES - noapi | YES | TODO | TODO | --- | TODO | --- - KEY_DOWN | YES | YES | YES | SOME | TODO | YES - KEY_UP | YES | YES | YES | SOME | TODO | YES - CHAR | YES | YES | YES | YES | TODO | YES - MOUSE_DOWN | YES | YES | YES | --- | --- | YES - MOUSE_UP | YES | YES | YES | --- | --- | YES - MOUSE_SCROLL | YES | YES | YES | --- | --- | YES - MOUSE_MOVE | YES | YES | YES | --- | --- | YES - MOUSE_ENTER | YES | YES | YES | --- | --- | YES - MOUSE_LEAVE | YES | YES | YES | --- | --- | YES - TOUCHES_BEGAN | --- | --- | --- | YES | YES | YES - TOUCHES_MOVED | --- | --- | --- | YES | YES | YES - TOUCHES_ENDED | --- | --- | --- | YES | YES | YES - TOUCHES_CANCELLED | --- | --- | --- | YES | YES | YES - RESIZED | YES | YES | YES | YES | YES | YES - ICONIFIED | YES | YES | YES | --- | --- | --- - RESTORED | YES | YES | YES | --- | --- | --- - FOCUSED | YES | YES | YES | --- | --- | YES - UNFOCUSED | YES | YES | YES | --- | --- | YES - SUSPENDED | --- | --- | --- | YES | YES | TODO - RESUMED | --- | --- | --- | YES | YES | TODO - QUIT_REQUESTED | YES | YES | YES | --- | --- | YES - IME | TODO | TODO? | TODO | ??? | TODO | ??? - key repeat flag | YES | YES | YES | --- | --- | YES - windowed | YES | YES | YES | --- | --- | YES - fullscreen | YES | YES | YES | YES | YES | YES(3) - mouse hide | YES | YES | YES | --- | --- | YES - mouse lock | YES | YES | YES | --- | --- | YES - set cursor type | YES | YES | YES | --- | --- | YES - screen keyboard | --- | --- | --- | YES | TODO | YES - swap interval | YES | YES | YES | YES | TODO | YES - high-dpi | YES | YES | TODO | YES | YES | YES - clipboard | YES | YES | YES | --- | --- | YES - MSAA | YES | YES | YES | YES | YES | YES - drag'n'drop | YES | YES | YES | --- | --- | YES - window icon | YES | YES(1)| YES | --- | --- | YES - - (1) macOS has no regular window icons, instead the dock icon is changed - (2) supported with EGL only (not GLX) - (3) fullscreen in the browser not supported on iphones - (4) WebGPU on native desktop platforms should be considered experimental - and mainly useful for debugging and benchmarking - - STEP BY STEP - ============ - --- Add a sokol_main() function to your code which returns a sapp_desc structure - with initialization parameters and callback function pointers. This - function is called very early, usually at the start of the - platform's entry function (e.g. main or WinMain). You should do as - little as possible here, since the rest of your code might be called - from another thread (this depends on the platform): - - sapp_desc sokol_main(int argc, char* argv[]) { - return (sapp_desc) { - .width = 640, - .height = 480, - .init_cb = my_init_func, - .frame_cb = my_frame_func, - .cleanup_cb = my_cleanup_func, - .event_cb = my_event_func, - ... - }; - } - - To get any logging output in case of errors you need to provide a log - callback. The easiest way is via sokol_log.h: - - #include "sokol_log.h" - - sapp_desc sokol_main(int argc, char* argv[]) { - return (sapp_desc) { - ... - .logger.func = slog_func, - }; - } - - There are many more setup parameters, but these are the most important. - For a complete list search for the sapp_desc structure declaration - below. - - DO NOT call any sokol-app function from inside sokol_main(), since - sokol-app will not be initialized at this point. - - The .width and .height parameters are the preferred size of the 3D - rendering canvas. The actual size may differ from this depending on - platform and other circumstances. Also the canvas size may change at - any time (for instance when the user resizes the application window, - or rotates the mobile device). You can just keep .width and .height - zero-initialized to open a default-sized window (what "default-size" - exactly means is platform-specific, but usually it's a size that covers - most of, but not all, of the display). - - All provided function callbacks will be called from the same thread, - but this may be different from the thread where sokol_main() was called. - - .init_cb (void (*)(void)) - This function is called once after the application window, - 3D rendering context and swap chain have been created. The - function takes no arguments and has no return value. - .frame_cb (void (*)(void)) - This is the per-frame callback, which is usually called 60 - times per second. This is where your application would update - most of its state and perform all rendering. - .cleanup_cb (void (*)(void)) - The cleanup callback is called once right before the application - quits. - .event_cb (void (*)(const sapp_event* event)) - The event callback is mainly for input handling, but is also - used to communicate other types of events to the application. Keep the - event_cb struct member zero-initialized if your application doesn't require - event handling. - - As you can see, those 'standard callbacks' don't have a user_data - argument, so any data that needs to be preserved between callbacks - must live in global variables. If keeping state in global variables - is not an option, there's an alternative set of callbacks with - an additional user_data pointer argument: - - .user_data (void*) - The user-data argument for the callbacks below - .init_userdata_cb (void (*)(void* user_data)) - .frame_userdata_cb (void (*)(void* user_data)) - .cleanup_userdata_cb (void (*)(void* user_data)) - .event_userdata_cb (void(*)(const sapp_event* event, void* user_data)) - - The function sapp_userdata() can be used to query the user_data - pointer provided in the sapp_desc struct. - - You can also call sapp_query_desc() to get a copy of the - original sapp_desc structure. - - NOTE that there's also an alternative compile mode where sokol_app.h - doesn't "hijack" the main() function. Search below for SOKOL_NO_ENTRY. - - --- Implement the initialization callback function (init_cb), this is called - once after the rendering surface, 3D API and swap chain have been - initialized by sokol_app. All sokol-app functions can be called - from inside the initialization callback, the most useful functions - at this point are: - - int sapp_width(void) - int sapp_height(void) - Returns the current width and height of the default framebuffer in pixels, - this may change from one frame to the next, and it may be different - from the initial size provided in the sapp_desc struct. - - float sapp_widthf(void) - float sapp_heightf(void) - These are alternatives to sapp_width() and sapp_height() which return - the default framebuffer size as float values instead of integer. This - may help to prevent casting back and forth between int and float - in more strongly typed languages than C and C++. - - double sapp_frame_duration(void) - Returns a smoothed frame duration. - - double sapp_frame_duration_unfiltered(void) - Returns the unfiltered frame duration with varying degree of - jitter (depending on platform and backend). - - int sapp_color_format(void) - int sapp_depth_format(void) - The color and depth-stencil pixelformats of the default framebuffer, - as integer values which are compatible with sokol-gfx's - sg_pixel_format enum (so that they can be plugged directly in places - where sg_pixel_format is expected). Possible values are: - - 23 == SG_PIXELFORMAT_RGBA8 - 28 == SG_PIXELFORMAT_BGRA8 - 42 == SG_PIXELFORMAT_DEPTH - 43 == SG_PIXELFORMAT_DEPTH_STENCIL - - int sapp_sample_count(void) - Return the MSAA sample count of the default framebuffer. - - const void* sapp_metal_get_device(void) - const void* sapp_metal_get_current_drawable(void) - const void* sapp_metal_get_depth_stencil_texture(void) - const void* sapp_metal_get_msaa_color_texture(void) - If the Metal backend has been selected, these functions return pointers - to various Metal API objects required for rendering, otherwise - they return a null pointer. These void pointers are actually - Objective-C ids converted with a (ARC) __bridge cast so that - the ids can be tunneled through C code. Also note that the returned - pointers may change from one frame to the next, only the Metal device - object is guaranteed to stay the same. - - const void* sapp_macos_get_window(void) - On macOS, get the NSWindow object pointer, otherwise a null pointer. - Before being used as Objective-C object, the void* must be converted - back with a (ARC) __bridge cast. - - const void* sapp_ios_get_window(void) - On iOS, get the UIWindow object pointer, otherwise a null pointer. - Before being used as Objective-C object, the void* must be converted - back with a (ARC) __bridge cast. - - const void* sapp_d3d11_get_device(void) - const void* sapp_d3d11_get_device_context(void) - const void* sapp_d3d11_get_render_view(void) - const void* sapp_d3d11_get_resolve_view(void); - const void* sapp_d3d11_get_depth_stencil_view(void) - Similar to the sapp_metal_* functions, the sapp_d3d11_* functions - return pointers to D3D11 API objects required for rendering, - only if the D3D11 backend has been selected. Otherwise they - return a null pointer. Note that the returned pointers to the - render-target-view and depth-stencil-view may change from one - frame to the next! - - const void* sapp_win32_get_hwnd(void) - On Windows, get the window's HWND, otherwise a null pointer. The - HWND has been cast to a void pointer in order to be tunneled - through code which doesn't include Windows.h. - - const void* sapp_x11_get_window(void) - On Linux, get the X11 Window, otherwise a null pointer. The - Window has been cast to a void pointer in order to be tunneled - through code which doesn't include X11/Xlib.h. - - const void* sapp_x11_get_display(void) - On Linux, get the X11 Display, otherwise a null pointer. The - Display has been cast to a void pointer in order to be tunneled - through code which doesn't include X11/Xlib.h. - - const void* sapp_wgpu_get_device(void) - const void* sapp_wgpu_get_render_view(void) - const void* sapp_wgpu_get_resolve_view(void) - const void* sapp_wgpu_get_depth_stencil_view(void) - These are the WebGPU-specific functions to get the WebGPU - objects and values required for rendering. If sokol_app.h - is not compiled with SOKOL_WGPU, these functions return null. - - uint32_t sapp_gl_get_framebuffer(void) - This returns the 'default framebuffer' of the GL context. - Typically this will be zero. - - int sapp_gl_get_major_version(void) - int sapp_gl_get_minor_version(void) - bool sapp_gl_is_gles(void) - Returns the major and minor version of the GL context and - whether the GL context is a GLES context - - const void* sapp_android_get_native_activity(void); - On Android, get the native activity ANativeActivity pointer, otherwise - a null pointer. - - --- Implement the frame-callback function, this function will be called - on the same thread as the init callback, but might be on a different - thread than the sokol_main() function. Note that the size of - the rendering framebuffer might have changed since the frame callback - was called last. Call the functions sapp_width() and sapp_height() - each frame to get the current size. - - --- Optionally implement the event-callback to handle input events. - sokol-app provides the following type of input events: - - a 'virtual key' was pressed down or released - - a single text character was entered (provided as UTF-32 encoded - UNICODE code point) - - a mouse button was pressed down or released (left, right, middle) - - mouse-wheel or 2D scrolling events - - the mouse was moved - - the mouse has entered or left the application window boundaries - - low-level, portable multi-touch events (began, moved, ended, cancelled) - - the application window was resized, iconified or restored - - the application was suspended or restored (on mobile platforms) - - the user or application code has asked to quit the application - - a string was pasted to the system clipboard - - one or more files have been dropped onto the application window - - To explicitly 'consume' an event and prevent that the event is - forwarded for further handling to the operating system, call - sapp_consume_event() from inside the event handler (NOTE that - this behaviour is currently only implemented for some HTML5 - events, support for other platforms and event types will - be added as needed, please open a GitHub ticket and/or provide - a PR if needed). - - NOTE: Do *not* call any 3D API rendering functions in the event - callback function, since the 3D API context may not be active when the - event callback is called (it may work on some platforms and 3D APIs, - but not others, and the exact behaviour may change between - sokol-app versions). - - --- Implement the cleanup-callback function, this is called once - after the user quits the application (see the section - "APPLICATION QUIT" for detailed information on quitting - behaviour, and how to intercept a pending quit - for instance to show a - "Really Quit?" dialog box). Note that the cleanup-callback isn't - guaranteed to be called on the web and mobile platforms. - - MOUSE CURSOR TYPE AND VISIBILITY - ================================ - You can show and hide the mouse cursor with - - void sapp_show_mouse(bool show) - - And to get the current shown status: - - bool sapp_mouse_shown(void) - - NOTE that hiding the mouse cursor is different and independent from - the MOUSE/POINTER LOCK feature which will also hide the mouse pointer when - active (MOUSE LOCK is described below). - - To change the mouse cursor to one of several predefined types, call - the function: - - void sapp_set_mouse_cursor(sapp_mouse_cursor cursor) - - Setting the default mouse cursor SAPP_MOUSECURSOR_DEFAULT will restore - the standard look. - - To get the currently active mouse cursor type, call: - - sapp_mouse_cursor sapp_get_mouse_cursor(void) - - MOUSE LOCK (AKA POINTER LOCK, AKA MOUSE CAPTURE) - ================================================ - In normal mouse mode, no mouse movement events are reported when the - mouse leaves the windows client area or hits the screen border (whether - it's one or the other depends on the platform), and the mouse move events - (SAPP_EVENTTYPE_MOUSE_MOVE) contain absolute mouse positions in - framebuffer pixels in the sapp_event items mouse_x and mouse_y, and - relative movement in framebuffer pixels in the sapp_event items mouse_dx - and mouse_dy. - - To get continuous mouse movement (also when the mouse leaves the window - client area or hits the screen border), activate mouse-lock mode - by calling: - - sapp_lock_mouse(true) - - When mouse lock is activated, the mouse pointer is hidden, the - reported absolute mouse position (sapp_event.mouse_x/y) appears - frozen, and the relative mouse movement in sapp_event.mouse_dx/dy - no longer has a direct relation to framebuffer pixels but instead - uses "raw mouse input" (what "raw mouse input" exactly means also - differs by platform). - - To deactivate mouse lock and return to normal mouse mode, call - - sapp_lock_mouse(false) - - And finally, to check if mouse lock is currently active, call - - if (sapp_mouse_locked()) { ... } - - Note that mouse-lock state may not change immediately after sapp_lock_mouse(true/false) - is called, instead on some platforms the actual state switch may be delayed - to the end of the current frame or even to a later frame. - - The mouse may also be unlocked automatically without calling sapp_lock_mouse(false), - most notably when the application window becomes inactive. - - On the web platform there are further restrictions to be aware of, caused - by the limitations of the HTML5 Pointer Lock API: - - - sapp_lock_mouse(true) can be called at any time, but it will - only take effect in a 'short-lived input event handler of a specific - type', meaning when one of the following events happens: - - SAPP_EVENTTYPE_MOUSE_DOWN - - SAPP_EVENTTYPE_MOUSE_UP - - SAPP_EVENTTYPE_MOUSE_SCROLL - - SAPP_EVENTTYPE_KEY_UP - - SAPP_EVENTTYPE_KEY_DOWN - - The mouse lock/unlock action on the web platform is asynchronous, - this means that sapp_mouse_locked() won't immediately return - the new status after calling sapp_lock_mouse(), instead the - reported status will only change when the pointer lock has actually - been activated or deactivated in the browser. - - On the web, mouse lock can be deactivated by the user at any time - by pressing the Esc key. When this happens, sokol_app.h behaves - the same as if sapp_lock_mouse(false) is called. - - For things like camera manipulation it's most straightforward to lock - and unlock the mouse right from the sokol_app.h event handler, for - instance the following code enters and leaves mouse lock when the - left mouse button is pressed and released, and then uses the relative - movement information to manipulate a camera (taken from the - cgltf-sapp.c sample in the sokol-samples repository - at https://github.com/floooh/sokol-samples): - - static void input(const sapp_event* ev) { - switch (ev->type) { - case SAPP_EVENTTYPE_MOUSE_DOWN: - if (ev->mouse_button == SAPP_MOUSEBUTTON_LEFT) { - sapp_lock_mouse(true); - } - break; - - case SAPP_EVENTTYPE_MOUSE_UP: - if (ev->mouse_button == SAPP_MOUSEBUTTON_LEFT) { - sapp_lock_mouse(false); - } - break; - - case SAPP_EVENTTYPE_MOUSE_MOVE: - if (sapp_mouse_locked()) { - cam_orbit(&state.camera, ev->mouse_dx * 0.25f, ev->mouse_dy * 0.25f); - } - break; - - default: - break; - } - } - - For a 'first person shooter mouse' the following code inside the sokol-app event handler - is recommended somewhere in your frame callback: - - if (!sapp_mouse_locked()) { - sapp_lock_mouse(true); - } - - CLIPBOARD SUPPORT - ================= - Applications can send and receive UTF-8 encoded text data from and to the - system clipboard. By default, clipboard support is disabled and - must be enabled at startup via the following sapp_desc struct - members: - - sapp_desc.enable_clipboard - set to true to enable clipboard support - sapp_desc.clipboard_size - size of the internal clipboard buffer in bytes - - Enabling the clipboard will dynamically allocate a clipboard buffer - for UTF-8 encoded text data of the requested size in bytes, the default - size is 8 KBytes. Strings that don't fit into the clipboard buffer - (including the terminating zero) will be silently clipped, so it's - important that you provide a big enough clipboard size for your - use case. - - To send data to the clipboard, call sapp_set_clipboard_string() with - a pointer to an UTF-8 encoded, null-terminated C-string. - - NOTE that on the HTML5 platform, sapp_set_clipboard_string() must be - called from inside a 'short-lived event handler', and there are a few - other HTML5-specific caveats to workaround. You'll basically have to - tinker until it works in all browsers :/ (maybe the situation will - improve when all browsers agree on and implement the new - HTML5 navigator.clipboard API). - - To get data from the clipboard, check for the SAPP_EVENTTYPE_CLIPBOARD_PASTED - event in your event handler function, and then call sapp_get_clipboard_string() - to obtain the pasted UTF-8 encoded text. - - NOTE that behaviour of sapp_get_clipboard_string() is slightly different - depending on platform: - - - on the HTML5 platform, the internal clipboard buffer will only be updated - right before the SAPP_EVENTTYPE_CLIPBOARD_PASTED event is sent, - and sapp_get_clipboard_string() will simply return the current content - of the clipboard buffer - - on 'native' platforms, the call to sapp_get_clipboard_string() will - update the internal clipboard buffer with the most recent data - from the system clipboard - - Portable code should check for the SAPP_EVENTTYPE_CLIPBOARD_PASTED event, - and then call sapp_get_clipboard_string() right in the event handler. - - The SAPP_EVENTTYPE_CLIPBOARD_PASTED event will be generated by sokol-app - as follows: - - - on macOS: when the Cmd+V key is pressed down - - on HTML5: when the browser sends a 'paste' event to the global 'window' object - - on all other platforms: when the Ctrl+V key is pressed down - - DRAG AND DROP SUPPORT - ===================== - PLEASE NOTE: the drag'n'drop feature works differently on WASM/HTML5 - and on the native desktop platforms (Win32, Linux and macOS) because - of security-related restrictions in the HTML5 drag'n'drop API. The - WASM/HTML5 specifics are described at the end of this documentation - section: - - Like clipboard support, drag'n'drop support must be explicitly enabled - at startup in the sapp_desc struct. - - sapp_desc sokol_main(void) { - return (sapp_desc) { - .enable_dragndrop = true, // default is false - ... - }; - } - - You can also adjust the maximum number of files that are accepted - in a drop operation, and the maximum path length in bytes if needed: - - sapp_desc sokol_main(void) { - return (sapp_desc) { - .enable_dragndrop = true, // default is false - .max_dropped_files = 8, // default is 1 - .max_dropped_file_path_length = 8192, // in bytes, default is 2048 - ... - }; - } - - When drag'n'drop is enabled, the event callback will be invoked with an - event of type SAPP_EVENTTYPE_FILES_DROPPED whenever the user drops files on - the application window. - - After the SAPP_EVENTTYPE_FILES_DROPPED is received, you can query the - number of dropped files, and their absolute paths by calling separate - functions: - - void on_event(const sapp_event* ev) { - if (ev->type == SAPP_EVENTTYPE_FILES_DROPPED) { - - // the mouse position where the drop happened - float x = ev->mouse_x; - float y = ev->mouse_y; - - // get the number of files and their paths like this: - const int num_dropped_files = sapp_get_num_dropped_files(); - for (int i = 0; i < num_dropped_files; i++) { - const char* path = sapp_get_dropped_file_path(i); - ... - } - } - } - - The returned file paths are UTF-8 encoded strings. - - You can call sapp_get_num_dropped_files() and sapp_get_dropped_file_path() - anywhere, also outside the event handler callback, but be aware that the - file path strings will be overwritten with the next drop operation. - - In any case, sapp_get_dropped_file_path() will never return a null pointer, - instead an empty string "" will be returned if the drag'n'drop feature - hasn't been enabled, the last drop-operation failed, or the file path index - is out of range. - - Drag'n'drop caveats: - - - if more files are dropped in a single drop-action - than sapp_desc.max_dropped_files, the additional - files will be silently ignored - - if any of the file paths is longer than - sapp_desc.max_dropped_file_path_length (in number of bytes, after UTF-8 - encoding) the entire drop operation will be silently ignored (this - needs some sort of error feedback in the future) - - no mouse positions are reported while the drag is in - process, this may change in the future - - Drag'n'drop on HTML5/WASM: - - The HTML5 drag'n'drop API doesn't return file paths, but instead - black-box 'file objects' which must be used to load the content - of dropped files. This is the reason why sokol_app.h adds two - HTML5-specific functions to the drag'n'drop API: - - uint32_t sapp_html5_get_dropped_file_size(int index) - Returns the size in bytes of a dropped file. - - void sapp_html5_fetch_dropped_file(const sapp_html5_fetch_request* request) - Asynchronously loads the content of a dropped file into a - provided memory buffer (which must be big enough to hold - the file content) - - To start loading the first dropped file after an SAPP_EVENTTYPE_FILES_DROPPED - event is received: - - sapp_html5_fetch_dropped_file(&(sapp_html5_fetch_request){ - .dropped_file_index = 0, - .callback = fetch_cb - .buffer = { - .ptr = buf, - .size = sizeof(buf) - }, - .user_data = ... - }); - - Make sure that the memory pointed to by 'buf' stays valid until the - callback function is called! - - As result of the asynchronous loading operation (no matter if succeeded or - failed) the 'fetch_cb' function will be called: - - void fetch_cb(const sapp_html5_fetch_response* response) { - // IMPORTANT: check if the loading operation actually succeeded: - if (response->succeeded) { - // the size of the loaded file: - const size_t num_bytes = response->data.size; - // and the pointer to the data (same as 'buf' in the fetch-call): - const void* ptr = response->data.ptr; - } else { - // on error check the error code: - switch (response->error_code) { - case SAPP_HTML5_FETCH_ERROR_BUFFER_TOO_SMALL: - ... - break; - case SAPP_HTML5_FETCH_ERROR_OTHER: - ... - break; - } - } - } - - Check the droptest-sapp example for a real-world example which works - both on native platforms and the web: - - https://github.com/floooh/sokol-samples/blob/master/sapp/droptest-sapp.c - - HIGH-DPI RENDERING - ================== - You can set the sapp_desc.high_dpi flag during initialization to request - a full-resolution framebuffer on HighDPI displays. The default behaviour - is sapp_desc.high_dpi=false, this means that the application will - render to a lower-resolution framebuffer on HighDPI displays and the - rendered content will be upscaled by the window system composer. - - In a HighDPI scenario, you still request the same window size during - sokol_main(), but the framebuffer sizes returned by sapp_width() - and sapp_height() will be scaled up according to the DPI scaling - ratio. - - Note that on some platforms the DPI scaling factor may change at any - time (for instance when a window is moved from a high-dpi display - to a low-dpi display). - - To query the current DPI scaling factor, call the function: - - float sapp_dpi_scale(void); - - For instance on a Retina Mac, returning the following sapp_desc - struct from sokol_main(): - - sapp_desc sokol_main(void) { - return (sapp_desc) { - .width = 640, - .height = 480, - .high_dpi = true, - ... - }; - } - - ...the functions the functions sapp_width(), sapp_height() - and sapp_dpi_scale() will return the following values: - - sapp_width: 1280 - sapp_height: 960 - sapp_dpi_scale: 2.0 - - If the high_dpi flag is false, or you're not running on a Retina display, - the values would be: - - sapp_width: 640 - sapp_height: 480 - sapp_dpi_scale: 1.0 - - If the window is moved from the Retina display to a low-dpi external display, - the values would change as follows: - - sapp_width: 1280 => 640 - sapp_height: 960 => 480 - sapp_dpi_scale: 2.0 => 1.0 - - Currently there is no event associated with a DPI change, but an - SAPP_EVENTTYPE_RESIZED will be sent as a side effect of the - framebuffer size changing. - - Per-monitor DPI is currently supported on macOS and Windows. - - APPLICATION QUIT - ================ - Without special quit handling, a sokol_app.h application will quit - 'gracefully' when the user clicks the window close-button unless a - platform's application model prevents this (e.g. on web or mobile). - 'Graceful exit' means that the application-provided cleanup callback will - be called before the application quits. - - On native desktop platforms sokol_app.h provides more control over the - application-quit-process. It's possible to initiate a 'programmatic quit' - from the application code, and a quit initiated by the application user can - be intercepted (for instance to show a custom dialog box). - - This 'programmatic quit protocol' is implemented through 3 functions - and 1 event: - - - sapp_quit(): This function simply quits the application without - giving the user a chance to intervene. Usually this might - be called when the user clicks the 'Ok' button in a 'Really Quit?' - dialog box - - sapp_request_quit(): Calling sapp_request_quit() will send the - event SAPP_EVENTTYPE_QUIT_REQUESTED to the applications event handler - callback, giving the user code a chance to intervene and cancel the - pending quit process (for instance to show a 'Really Quit?' dialog - box). If the event handler callback does nothing, the application - will be quit as usual. To prevent this, call the function - sapp_cancel_quit() from inside the event handler. - - sapp_cancel_quit(): Cancels a pending quit request, either initiated - by the user clicking the window close button, or programmatically - by calling sapp_request_quit(). The only place where calling this - function makes sense is from inside the event handler callback when - the SAPP_EVENTTYPE_QUIT_REQUESTED event has been received. - - SAPP_EVENTTYPE_QUIT_REQUESTED: this event is sent when the user - clicks the window's close button or application code calls the - sapp_request_quit() function. The event handler callback code can handle - this event by calling sapp_cancel_quit() to cancel the quit. - If the event is ignored, the application will quit as usual. - - On the web platform, the quit behaviour differs from native platforms, - because of web-specific restrictions: - - A `programmatic quit` initiated by calling sapp_quit() or - sapp_request_quit() will work as described above: the cleanup callback is - called, platform-specific cleanup is performed (on the web - this means that JS event handlers are unregistered), and then - the request-animation-loop will be exited. However that's all. The - web page itself will continue to exist (e.g. it's not possible to - programmatically close the browser tab). - - On the web it's also not possible to run custom code when the user - closes a browser tab, so it's not possible to prevent this with a - fancy custom dialog box. - - Instead the standard "Leave Site?" dialog box can be activated (or - deactivated) with the following function: - - sapp_html5_ask_leave_site(bool ask); - - The initial state of the associated internal flag can be provided - at startup via sapp_desc.html5.ask_leave_site. - - This feature should only be used sparingly in critical situations - for - instance when the user would loose data - since popping up modal dialog - boxes is considered quite rude in the web world. Note that there's no way - to customize the content of this dialog box or run any code as a result - of the user's decision. Also note that the user must have interacted with - the site before the dialog box will appear. These are all security measures - to prevent fishing. - - The Dear ImGui HighDPI sample contains example code of how to - implement a 'Really Quit?' dialog box with Dear ImGui (native desktop - platforms only), and for showing the hardwired "Leave Site?" dialog box - when running on the web platform: - - https://floooh.github.io/sokol-html5/wasm/imgui-highdpi-sapp.html - - FULLSCREEN - ========== - If the sapp_desc.fullscreen flag is true, sokol-app will try to create - a fullscreen window on platforms with a 'proper' window system - (mobile devices will always use fullscreen). The implementation details - depend on the target platform, in general sokol-app will use a - 'soft approach' which doesn't interfere too much with the platform's - window system (for instance borderless fullscreen window instead of - a 'real' fullscreen mode). Such details might change over time - as sokol-app is adapted for different needs. - - The most important effect of fullscreen mode to keep in mind is that - the requested canvas width and height will be ignored for the initial - window size, calling sapp_width() and sapp_height() will instead return - the resolution of the fullscreen canvas (however the provided size - might still be used for the non-fullscreen window, in case the user can - switch back from fullscreen- to windowed-mode). - - To toggle fullscreen mode programmatically, call sapp_toggle_fullscreen(). - - To check if the application window is currently in fullscreen mode, - call sapp_is_fullscreen(). - - On the web, sapp_desc.fullscreen will have no effect, and the application - will always start in non-fullscreen mode. Call sapp_toggle_fullscreen() - from within or 'near' an input event to switch to fullscreen programatically. - Note that on the web, the fullscreen state may change back to windowed at - any time (either because the browser had rejected switching into fullscreen, - or the user leaves fullscreen via Esc), this means that the result - of sapp_is_fullscreen() may change also without calling sapp_toggle_fullscreen()! - - - WINDOW ICON SUPPORT - =================== - Some sokol_app.h backends allow to change the window icon programmatically: - - - on Win32: the small icon in the window's title bar, and the - bigger icon in the task bar - - on Linux: highly dependent on the used window manager, but usually - the window's title bar icon and/or the task bar icon - - on HTML5: the favicon shown in the page's browser tab - - on macOS: the application icon shown in the dock, but only - for currently running applications - - NOTE that it is not possible to set the actual application icon which is - displayed by the operating system on the desktop or 'home screen'. Those - icons must be provided 'traditionally' through operating-system-specific - resources which are associated with the application (sokol_app.h might - later support setting the window icon from platform specific resource data - though). - - There are two ways to set the window icon: - - - at application start in the sokol_main() function by initializing - the sapp_desc.icon nested struct - - or later by calling the function sapp_set_icon() - - As a convenient shortcut, sokol_app.h comes with a builtin default-icon - (a rainbow-colored 'S', which at least looks a bit better than the Windows - default icon for applications), which can be activated like this: - - At startup in sokol_main(): - - sapp_desc sokol_main(...) { - return (sapp_desc){ - ... - icon.sokol_default = true - }; - } - - Or later by calling: - - sapp_set_icon(&(sapp_icon_desc){ .sokol_default = true }); - - NOTE that a completely zero-initialized sapp_icon_desc struct will not - update the window icon in any way. This is an 'escape hatch' so that you - can handle the window icon update yourself (or if you do this already, - sokol_app.h won't get in your way, in this case just leave the - sapp_desc.icon struct zero-initialized). - - Providing your own icon images works exactly like in GLFW (down to the - data format): - - You provide one or more 'candidate images' in different sizes, and the - sokol_app.h platform backends pick the best match for the specific backend - and icon type. - - For each candidate image, you need to provide: - - - the width in pixels - - the height in pixels - - and the actual pixel data in RGBA8 pixel format (e.g. 0xFFCC8844 - on a little-endian CPU means: alpha=0xFF, blue=0xCC, green=0x88, red=0x44) - - For instance, if you have 3 candidate images (small, medium, big) of - sizes 16x16, 32x32 and 64x64 the corresponding sapp_icon_desc struct is setup - like this: - - // the actual pixel data (RGBA8, origin top-left) - const uint32_t small[16][16] = { ... }; - const uint32_t medium[32][32] = { ... }; - const uint32_t big[64][64] = { ... }; - - const sapp_icon_desc icon_desc = { - .images = { - { .width = 16, .height = 16, .pixels = SAPP_RANGE(small) }, - { .width = 32, .height = 32, .pixels = SAPP_RANGE(medium) }, - // ...or without the SAPP_RANGE helper macro: - { .width = 64, .height = 64, .pixels = { .ptr=big, .size=sizeof(big) } } - } - }; - - An sapp_icon_desc struct initialized like this can then either be applied - at application start in sokol_main: - - sapp_desc sokol_main(...) { - return (sapp_desc){ - ... - icon = icon_desc - }; - } - - ...or later by calling sapp_set_icon(): - - sapp_set_icon(&icon_desc); - - Some window icon caveats: - - - once the window icon has been updated, there's no way to go back to - the platform's default icon, this is because some platforms (Linux - and HTML5) don't switch the icon visual back to the default even if - the custom icon is deleted or removed - - on HTML5, if the sokol_app.h icon doesn't show up in the browser - tab, check that there's no traditional favicon 'link' element - is defined in the page's index.html, sokol_app.h will only - append a new favicon link element, but not delete any manually - defined favicon in the page - - For an example and test of the window icon feature, check out the - 'icon-sapp' sample on the sokol-samples git repository. - - ONSCREEN KEYBOARD - ================= - On some platforms which don't provide a physical keyboard, sokol-app - can display the platform's integrated onscreen keyboard for text - input. To request that the onscreen keyboard is shown, call - - sapp_show_keyboard(true); - - Likewise, to hide the keyboard call: - - sapp_show_keyboard(false); - - Note that onscreen keyboard functionality is no longer supported - on the browser platform (the previous hacks and workarounds to make browser - keyboards work for on web applications that don't use HTML UIs - never really worked across browsers). - - INPUT EVENT BUBBLING ON THE WEB PLATFORM - ======================================== - By default, input event bubbling on the web platform is configured in - a way that makes the most sense for 'full-canvas' apps that cover the - entire browser client window area: - - - mouse, touch and wheel events do not bubble up, this prevents various - ugly side events, like: - - HTML text overlays being selected on double- or triple-click into - the canvas - - 'scroll bumping' even when the canvas covers the entire client area - - key_up/down events for 'character keys' *do* bubble up (otherwise - the browser will not generate UNICODE character events) - - all other key events *do not* bubble up by default (this prevents side effects - like F1 opening help, or F7 starting 'caret browsing') - - character events do not bubble up (although I haven't noticed any side effects - otherwise) - - Event bubbling can be enabled for input event categories during initialization - in the sapp_desc struct: - - sapp_desc sokol_main(int argc, char* argv[]) { - return (sapp_desc){ - //... - .html5 = { - .bubble_mouse_events = true, - .bubble_touch_events = true, - .bubble_wheel_events = true, - .bubble_key_events = true, - .bubble_char_events = true, - } - }; - } - - This basically opens the floodgates and lets *all* input events bubble up to the browser. - - To prevent individual events from bubbling, call sapp_consume_event() from within - the sokol_app.h event callback when that specific event is reported. - - - SETTING THE CANVAS OBJECT ON THE WEB PLATFORM - ============================================= - On the web, sokol_app.h and the Emscripten SDK functions need to find - the WebGL/WebGPU canvas intended for rendering and attaching event - handlers. This can happen in four ways: - - 1. do nothing and just set the id of the canvas object to 'canvas' (preferred) - 2. via a CSS Selector string (preferred) - 3. by setting the `Module.canvas` property to the canvas object - 4. by adding the canvas object to the global variable `specialHTMLTargets[]` - (this is a special variable used by the Emscripten runtime to lookup - event target objects for which document.querySelector() cannot be used) - - The easiest way is to just name your canvas object 'canvas': - - - - This works because the default css selector string used by sokol_app.h - is '#canvas'. - - If you name your canvas differently, you need to communicate that name to - sokol_app.h via `sapp_desc.html5.canvas_selector` as a regular css selector - string that's compatible with `document.querySelector()`. E.g. if your canvas - object looks like this: - - - - The `sapp_desc.html5.canvas_selector` string must be set to '#bla': - - .html5.canvas_selector = "#bla" - - If the canvas object cannot be looked up via `document.querySelector()` you - need to use one of the alternative methods, both involve the special - Emscripten runtime `Module` object which is usually setup in the index.html - like this before the WASM blob is loaded and instantiated: - - - - The first option is to set the `Module.canvas` property to your canvas object: - - - - When sokol_app.h initializes, it will check the global Module object whether - a `Module.canvas` property exists and is an object. This method will add - a new entry to the `specialHTMLTargets[]` object - - The other option is to add the canvas under a name chosen by you to the - special `specialHTMLTargets[]` map, which is used by the Emscripten runtime - to lookup 'event target objects' which are not visible to `document.querySelector()`. - Note that `specialHTMLTargets[]` must be updated after the Emscripten runtime - has started but before the WASM code is running. A good place for this is - the special `Module.preRun` array in index.html: - - - - In that case, pass the same string to sokol_app.h which is used as key - in the specialHTMLTargets[] map: - - .html5.canvas_selector = "my_canvas" - - If sokol_app.h can't find your canvas for some reason check for warning - messages on the browser console. - - - OPTIONAL: DON'T HIJACK main() (#define SOKOL_NO_ENTRY) - ====================================================== - NOTE: SOKOL_NO_ENTRY and sapp_run() is currently not supported on Android. - - In its default configuration, sokol_app.h "hijacks" the platform's - standard main() function. This was done because different platforms - have different entry point conventions which are not compatible with - C's main() (for instance WinMain on Windows has completely different - arguments). However, this "main hijacking" posed a problem for - usage scenarios like integrating sokol_app.h with other languages than - C or C++, so an alternative SOKOL_NO_ENTRY mode has been added - in which the user code provides the platform's main function: - - - define SOKOL_NO_ENTRY before including the sokol_app.h implementation - - do *not* provide a sokol_main() function - - instead provide the standard main() function of the platform - - from the main function, call the function ```sapp_run()``` which - takes a pointer to an ```sapp_desc``` structure. - - from here on```sapp_run()``` takes over control and calls the provided - init-, frame-, event- and cleanup-callbacks just like in the default model. - - sapp_run() behaves differently across platforms: - - - on some platforms, sapp_run() will return when the application quits - - on other platforms, sapp_run() will never return, even when the - application quits (the operating system is free to simply terminate - the application at any time) - - on Emscripten specifically, sapp_run() will return immediately while - the frame callback keeps being called - - This different behaviour of sapp_run() essentially means that there shouldn't - be any code *after* sapp_run(), because that may either never be called, or in - case of Emscripten will be called at an unexpected time (at application start). - - An application also should not depend on the cleanup-callback being called - when cross-platform compatibility is required. - - Since sapp_run() returns immediately on Emscripten you shouldn't activate - the 'EXIT_RUNTIME' linker option (this is disabled by default when compiling - for the browser target), since the C/C++ exit runtime would be called immediately at - application start, causing any global objects to be destroyed and global - variables to be zeroed. - - WINDOWS CONSOLE OUTPUT - ====================== - On Windows, regular windowed applications don't show any stdout/stderr text - output, which can be a bit of a hassle for printf() debugging or generally - logging text to the console. Also, console output by default uses a local - codepage setting and thus international UTF-8 encoded text is printed - as garbage. - - To help with these issues, sokol_app.h can be configured at startup - via the following Windows-specific sapp_desc flags: - - sapp_desc.win32.console_utf8 (default: false) - When set to true, the output console codepage will be switched - to UTF-8 (and restored to the original codepage on exit) - - sapp_desc.win32.console_attach (default: false) - When set to true, stdout and stderr will be attached to the - console of the parent process (if the parent process actually - has a console). This means that if the application was started - in a command line window, stdout and stderr output will be printed - to the terminal, just like a regular command line program. But if - the application is started via double-click, it will behave like - a regular UI application, and stdout/stderr will not be visible. - - sapp_desc.win32.console_create (default: false) - When set to true, a new console window will be created and - stdout/stderr will be redirected to that console window. It - doesn't matter if the application is started from the command - line or via double-click. - - NOTE: setting both win32.console_attach and win32.console_create - to true also makes sense and has the effect that output - will appear in the existing terminal when started from the cmdline, and - otherwise (when started via double-click) will open a console window. - - MEMORY ALLOCATION OVERRIDE - ========================== - You can override the memory allocation functions at initialization time - like this: - - void* my_alloc(size_t size, void* user_data) { - return malloc(size); - } - - void my_free(void* ptr, void* user_data) { - free(ptr); - } - - sapp_desc sokol_main(int argc, char* argv[]) { - return (sapp_desc){ - // ... - .allocator = { - .alloc_fn = my_alloc, - .free_fn = my_free, - .user_data = ..., - } - }; - } - - If no overrides are provided, malloc and free will be used. - - This only affects memory allocation calls done by sokol_app.h - itself though, not any allocations in OS libraries. - - - ERROR REPORTING AND LOGGING - =========================== - To get any logging information at all you need to provide a logging callback in the setup call - the easiest way is to use sokol_log.h: - - #include "sokol_log.h" - - sapp_desc sokol_main(int argc, char* argv[]) { - return (sapp_desc) { - ... - .logger.func = slog_func, - }; - } - - To override logging with your own callback, first write a logging function like this: - - void my_log(const char* tag, // e.g. 'sapp' - uint32_t log_level, // 0=panic, 1=error, 2=warn, 3=info - uint32_t log_item_id, // SAPP_LOGITEM_* - const char* message_or_null, // a message string, may be nullptr in release mode - uint32_t line_nr, // line number in sokol_app.h - const char* filename_or_null, // source filename, may be nullptr in release mode - void* user_data) - { - ... - } - - ...and then setup sokol-app like this: - - sapp_desc sokol_main(int argc, char* argv[]) { - return (sapp_desc) { - ... - .logger = { - .func = my_log, - .user_data = my_user_data, - } - }; - } - - The provided logging function must be reentrant (e.g. be callable from - different threads). - - If you don't want to provide your own custom logger it is highly recommended to use - the standard logger in sokol_log.h instead, otherwise you won't see any warnings or - errors. - - TEMP NOTE DUMP - ============== - - sapp_desc needs a bool whether to initialize depth-stencil surface - - the Android implementation calls cleanup_cb() and destroys the egl context in onDestroy - at the latest but should do it earlier, in onStop, as an app is "killable" after onStop - on Android Honeycomb and later (it can't be done at the moment as the app may be started - again after onStop and the sokol lifecycle does not yet handle context teardown/bringup) - - - LICENSE - ======= - zlib/libpng license - - Copyright (c) 2018 Andre Weissflog - - This software is provided 'as-is', without any express or implied warranty. - In no event will the authors be held liable for any damages arising from the - use of this software. - - Permission is granted to anyone to use this software for any purpose, - including commercial applications, and to alter it and redistribute it - freely, subject to the following restrictions: - - 1. The origin of this software must not be misrepresented; you must not - claim that you wrote the original software. If you use this software in a - product, an acknowledgment in the product documentation would be - appreciated but is not required. - - 2. Altered source versions must be plainly marked as such, and must not - be misrepresented as being the original software. - - 3. This notice may not be removed or altered from any source - distribution. -*/ -#define SOKOL_APP_INCLUDED (1) -#include // size_t -#include -#include - -#if defined(SOKOL_API_DECL) && !defined(SOKOL_APP_API_DECL) -#define SOKOL_APP_API_DECL SOKOL_API_DECL -#endif -#ifndef SOKOL_APP_API_DECL -#if defined(_WIN32) && defined(SOKOL_DLL) && defined(SOKOL_APP_IMPL) -#define SOKOL_APP_API_DECL __declspec(dllexport) -#elif defined(_WIN32) && defined(SOKOL_DLL) -#define SOKOL_APP_API_DECL __declspec(dllimport) -#else -#define SOKOL_APP_API_DECL extern -#endif -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -/* misc constants */ -enum { - SAPP_MAX_TOUCHPOINTS = 8, - SAPP_MAX_MOUSEBUTTONS = 3, - SAPP_MAX_KEYCODES = 512, - SAPP_MAX_ICONIMAGES = 8, -}; - -/* - sapp_event_type - - The type of event that's passed to the event handler callback - in the sapp_event.type field. These are not just "traditional" - input events, but also notify the application about state changes - or other user-invoked actions. -*/ -typedef enum sapp_event_type { - SAPP_EVENTTYPE_INVALID, - SAPP_EVENTTYPE_KEY_DOWN, - SAPP_EVENTTYPE_KEY_UP, - SAPP_EVENTTYPE_CHAR, - SAPP_EVENTTYPE_MOUSE_DOWN, - SAPP_EVENTTYPE_MOUSE_UP, - SAPP_EVENTTYPE_MOUSE_SCROLL, - SAPP_EVENTTYPE_MOUSE_MOVE, - SAPP_EVENTTYPE_MOUSE_ENTER, - SAPP_EVENTTYPE_MOUSE_LEAVE, - SAPP_EVENTTYPE_TOUCHES_BEGAN, - SAPP_EVENTTYPE_TOUCHES_MOVED, - SAPP_EVENTTYPE_TOUCHES_ENDED, - SAPP_EVENTTYPE_TOUCHES_CANCELLED, - SAPP_EVENTTYPE_RESIZED, - SAPP_EVENTTYPE_ICONIFIED, - SAPP_EVENTTYPE_RESTORED, - SAPP_EVENTTYPE_FOCUSED, - SAPP_EVENTTYPE_UNFOCUSED, - SAPP_EVENTTYPE_SUSPENDED, - SAPP_EVENTTYPE_RESUMED, - SAPP_EVENTTYPE_QUIT_REQUESTED, - SAPP_EVENTTYPE_CLIPBOARD_PASTED, - SAPP_EVENTTYPE_FILES_DROPPED, - _SAPP_EVENTTYPE_NUM, - _SAPP_EVENTTYPE_FORCE_U32 = 0x7FFFFFFF -} sapp_event_type; - -/* - sapp_keycode - - The 'virtual keycode' of a KEY_DOWN or KEY_UP event in the - struct field sapp_event.key_code. - - Note that the keycode values are identical with GLFW. -*/ -typedef enum sapp_keycode { - SAPP_KEYCODE_INVALID = 0, - SAPP_KEYCODE_SPACE = 32, - SAPP_KEYCODE_APOSTROPHE = 39, /* ' */ - SAPP_KEYCODE_COMMA = 44, /* , */ - SAPP_KEYCODE_MINUS = 45, /* - */ - SAPP_KEYCODE_PERIOD = 46, /* . */ - SAPP_KEYCODE_SLASH = 47, /* / */ - SAPP_KEYCODE_0 = 48, - SAPP_KEYCODE_1 = 49, - SAPP_KEYCODE_2 = 50, - SAPP_KEYCODE_3 = 51, - SAPP_KEYCODE_4 = 52, - SAPP_KEYCODE_5 = 53, - SAPP_KEYCODE_6 = 54, - SAPP_KEYCODE_7 = 55, - SAPP_KEYCODE_8 = 56, - SAPP_KEYCODE_9 = 57, - SAPP_KEYCODE_SEMICOLON = 59, /* ; */ - SAPP_KEYCODE_EQUAL = 61, /* = */ - SAPP_KEYCODE_A = 65, - SAPP_KEYCODE_B = 66, - SAPP_KEYCODE_C = 67, - SAPP_KEYCODE_D = 68, - SAPP_KEYCODE_E = 69, - SAPP_KEYCODE_F = 70, - SAPP_KEYCODE_G = 71, - SAPP_KEYCODE_H = 72, - SAPP_KEYCODE_I = 73, - SAPP_KEYCODE_J = 74, - SAPP_KEYCODE_K = 75, - SAPP_KEYCODE_L = 76, - SAPP_KEYCODE_M = 77, - SAPP_KEYCODE_N = 78, - SAPP_KEYCODE_O = 79, - SAPP_KEYCODE_P = 80, - SAPP_KEYCODE_Q = 81, - SAPP_KEYCODE_R = 82, - SAPP_KEYCODE_S = 83, - SAPP_KEYCODE_T = 84, - SAPP_KEYCODE_U = 85, - SAPP_KEYCODE_V = 86, - SAPP_KEYCODE_W = 87, - SAPP_KEYCODE_X = 88, - SAPP_KEYCODE_Y = 89, - SAPP_KEYCODE_Z = 90, - SAPP_KEYCODE_LEFT_BRACKET = 91, /* [ */ - SAPP_KEYCODE_BACKSLASH = 92, /* \ */ - SAPP_KEYCODE_RIGHT_BRACKET = 93, /* ] */ - SAPP_KEYCODE_GRAVE_ACCENT = 96, /* ` */ - SAPP_KEYCODE_WORLD_1 = 161, /* non-US #1 */ - SAPP_KEYCODE_WORLD_2 = 162, /* non-US #2 */ - SAPP_KEYCODE_ESCAPE = 256, - SAPP_KEYCODE_ENTER = 257, - SAPP_KEYCODE_TAB = 258, - SAPP_KEYCODE_BACKSPACE = 259, - SAPP_KEYCODE_INSERT = 260, - SAPP_KEYCODE_DELETE = 261, - SAPP_KEYCODE_RIGHT = 262, - SAPP_KEYCODE_LEFT = 263, - SAPP_KEYCODE_DOWN = 264, - SAPP_KEYCODE_UP = 265, - SAPP_KEYCODE_PAGE_UP = 266, - SAPP_KEYCODE_PAGE_DOWN = 267, - SAPP_KEYCODE_HOME = 268, - SAPP_KEYCODE_END = 269, - SAPP_KEYCODE_CAPS_LOCK = 280, - SAPP_KEYCODE_SCROLL_LOCK = 281, - SAPP_KEYCODE_NUM_LOCK = 282, - SAPP_KEYCODE_PRINT_SCREEN = 283, - SAPP_KEYCODE_PAUSE = 284, - SAPP_KEYCODE_F1 = 290, - SAPP_KEYCODE_F2 = 291, - SAPP_KEYCODE_F3 = 292, - SAPP_KEYCODE_F4 = 293, - SAPP_KEYCODE_F5 = 294, - SAPP_KEYCODE_F6 = 295, - SAPP_KEYCODE_F7 = 296, - SAPP_KEYCODE_F8 = 297, - SAPP_KEYCODE_F9 = 298, - SAPP_KEYCODE_F10 = 299, - SAPP_KEYCODE_F11 = 300, - SAPP_KEYCODE_F12 = 301, - SAPP_KEYCODE_F13 = 302, - SAPP_KEYCODE_F14 = 303, - SAPP_KEYCODE_F15 = 304, - SAPP_KEYCODE_F16 = 305, - SAPP_KEYCODE_F17 = 306, - SAPP_KEYCODE_F18 = 307, - SAPP_KEYCODE_F19 = 308, - SAPP_KEYCODE_F20 = 309, - SAPP_KEYCODE_F21 = 310, - SAPP_KEYCODE_F22 = 311, - SAPP_KEYCODE_F23 = 312, - SAPP_KEYCODE_F24 = 313, - SAPP_KEYCODE_F25 = 314, - SAPP_KEYCODE_KP_0 = 320, - SAPP_KEYCODE_KP_1 = 321, - SAPP_KEYCODE_KP_2 = 322, - SAPP_KEYCODE_KP_3 = 323, - SAPP_KEYCODE_KP_4 = 324, - SAPP_KEYCODE_KP_5 = 325, - SAPP_KEYCODE_KP_6 = 326, - SAPP_KEYCODE_KP_7 = 327, - SAPP_KEYCODE_KP_8 = 328, - SAPP_KEYCODE_KP_9 = 329, - SAPP_KEYCODE_KP_DECIMAL = 330, - SAPP_KEYCODE_KP_DIVIDE = 331, - SAPP_KEYCODE_KP_MULTIPLY = 332, - SAPP_KEYCODE_KP_SUBTRACT = 333, - SAPP_KEYCODE_KP_ADD = 334, - SAPP_KEYCODE_KP_ENTER = 335, - SAPP_KEYCODE_KP_EQUAL = 336, - SAPP_KEYCODE_LEFT_SHIFT = 340, - SAPP_KEYCODE_LEFT_CONTROL = 341, - SAPP_KEYCODE_LEFT_ALT = 342, - SAPP_KEYCODE_LEFT_SUPER = 343, - SAPP_KEYCODE_RIGHT_SHIFT = 344, - SAPP_KEYCODE_RIGHT_CONTROL = 345, - SAPP_KEYCODE_RIGHT_ALT = 346, - SAPP_KEYCODE_RIGHT_SUPER = 347, - SAPP_KEYCODE_MENU = 348, -} sapp_keycode; - -/* - Android specific 'tool type' enum for touch events. This lets the - application check what type of input device was used for - touch events. - - NOTE: the values must remain in sync with the corresponding - Android SDK type, so don't change those. - - See https://developer.android.com/reference/android/view/MotionEvent#TOOL_TYPE_UNKNOWN -*/ -typedef enum sapp_android_tooltype { - SAPP_ANDROIDTOOLTYPE_UNKNOWN = 0, // TOOL_TYPE_UNKNOWN - SAPP_ANDROIDTOOLTYPE_FINGER = 1, // TOOL_TYPE_FINGER - SAPP_ANDROIDTOOLTYPE_STYLUS = 2, // TOOL_TYPE_STYLUS - SAPP_ANDROIDTOOLTYPE_MOUSE = 3, // TOOL_TYPE_MOUSE -} sapp_android_tooltype; - -/* - sapp_touchpoint - - Describes a single touchpoint in a multitouch event (TOUCHES_BEGAN, - TOUCHES_MOVED, TOUCHES_ENDED). - - Touch points are stored in the nested array sapp_event.touches[], - and the number of touches is stored in sapp_event.num_touches. -*/ -typedef struct sapp_touchpoint { - uintptr_t identifier; - float pos_x; - float pos_y; - sapp_android_tooltype android_tooltype; // only valid on Android - bool changed; -} sapp_touchpoint; - -/* - sapp_mousebutton - - The currently pressed mouse button in the events MOUSE_DOWN - and MOUSE_UP, stored in the struct field sapp_event.mouse_button. -*/ -typedef enum sapp_mousebutton { - SAPP_MOUSEBUTTON_LEFT = 0x0, - SAPP_MOUSEBUTTON_RIGHT = 0x1, - SAPP_MOUSEBUTTON_MIDDLE = 0x2, - SAPP_MOUSEBUTTON_INVALID = 0x100, -} sapp_mousebutton; - -/* - These are currently pressed modifier keys (and mouse buttons) which are - passed in the event struct field sapp_event.modifiers. -*/ -enum { - SAPP_MODIFIER_SHIFT = 0x1, // left or right shift key - SAPP_MODIFIER_CTRL = 0x2, // left or right control key - SAPP_MODIFIER_ALT = 0x4, // left or right alt key - SAPP_MODIFIER_SUPER = 0x8, // left or right 'super' key - SAPP_MODIFIER_LMB = 0x100, // left mouse button - SAPP_MODIFIER_RMB = 0x200, // right mouse button - SAPP_MODIFIER_MMB = 0x400, // middle mouse button -}; - -/* - sapp_event - - This is an all-in-one event struct passed to the event handler - user callback function. Note that it depends on the event - type what struct fields actually contain useful values, so you - should first check the event type before reading other struct - fields. -*/ -typedef struct sapp_event { - uint64_t frame_count; // current frame counter, always valid, useful for checking if two events were issued in the same frame - sapp_event_type type; // the event type, always valid - sapp_keycode key_code; // the virtual key code, only valid in KEY_UP, KEY_DOWN - uint32_t char_code; // the UTF-32 character code, only valid in CHAR events - bool key_repeat; // true if this is a key-repeat event, valid in KEY_UP, KEY_DOWN and CHAR - uint32_t modifiers; // current modifier keys, valid in all key-, char- and mouse-events - sapp_mousebutton mouse_button; // mouse button that was pressed or released, valid in MOUSE_DOWN, MOUSE_UP - float mouse_x; // current horizontal mouse position in pixels, always valid except during mouse lock - float mouse_y; // current vertical mouse position in pixels, always valid except during mouse lock - float mouse_dx; // relative horizontal mouse movement since last frame, always valid - float mouse_dy; // relative vertical mouse movement since last frame, always valid - float scroll_x; // horizontal mouse wheel scroll distance, valid in MOUSE_SCROLL events - float scroll_y; // vertical mouse wheel scroll distance, valid in MOUSE_SCROLL events - int num_touches; // number of valid items in the touches[] array - sapp_touchpoint touches[SAPP_MAX_TOUCHPOINTS]; // current touch points, valid in TOUCHES_BEGIN, TOUCHES_MOVED, TOUCHES_ENDED - int window_width; // current window- and framebuffer sizes in pixels, always valid - int window_height; - int framebuffer_width; // = window_width * dpi_scale - int framebuffer_height; // = window_height * dpi_scale -} sapp_event; - -/* - sg_range - - A general pointer/size-pair struct and constructor macros for passing binary blobs - into sokol_app.h. -*/ -typedef struct sapp_range { - const void* ptr; - size_t size; -} sapp_range; -// disabling this for every includer isn't great, but the warnings are also quite pointless -#if defined(_MSC_VER) -#pragma warning(disable:4221) /* /W4 only: nonstandard extension used: 'x': cannot be initialized using address of automatic variable 'y' */ -#pragma warning(disable:4204) /* VS2015: nonstandard extension used: non-constant aggregate initializer */ -#endif -#if defined(__cplusplus) -#define SAPP_RANGE(x) sapp_range{ &x, sizeof(x) } -#else -#define SAPP_RANGE(x) (sapp_range){ &x, sizeof(x) } -#endif - -/* - sapp_image_desc - - This is used to describe image data to sokol_app.h (window icons and cursor images). - - The pixel format is RGBA8. - - cursor_hotspot_x and _y are used only for cursors, to define which pixel - of the image should be aligned with the mouse position. -*/ -typedef struct sapp_image_desc { - int width; - int height; - int cursor_hotspot_x; - int cursor_hotspot_y; - sapp_range pixels; -} sapp_image_desc; - -/* - sapp_icon_desc - - An icon description structure for use in sapp_desc.icon and - sapp_set_icon(). - - When setting a custom image, the application can provide a number of - candidates differing in size, and sokol_app.h will pick the image(s) - closest to the size expected by the platform's window system. - - To set sokol-app's default icon, set .sokol_default to true. - - Otherwise provide candidate images of different sizes in the - images[] array. - - If both the sokol_default flag is set to true, any image candidates - will be ignored and the sokol_app.h default icon will be set. -*/ -typedef struct sapp_icon_desc { - bool sokol_default; - sapp_image_desc images[SAPP_MAX_ICONIMAGES]; -} sapp_icon_desc; - -/* - sapp_allocator - - Used in sapp_desc to provide custom memory-alloc and -free functions - to sokol_app.h. If memory management should be overridden, both the - alloc_fn and free_fn function must be provided (e.g. it's not valid to - override one function but not the other). -*/ -typedef struct sapp_allocator { - void* (*alloc_fn)(size_t size, void* user_data); - void (*free_fn)(void* ptr, void* user_data); - void* user_data; -} sapp_allocator; - -/* - sapp_log_item - - Log items are defined via X-Macros and expanded to an enum - 'sapp_log_item', and in debug mode to corresponding - human readable error messages. -*/ -#define _SAPP_LOG_ITEMS \ - _SAPP_LOGITEM_XMACRO(OK, "Ok") \ - _SAPP_LOGITEM_XMACRO(MALLOC_FAILED, "memory allocation failed") \ - _SAPP_LOGITEM_XMACRO(MACOS_INVALID_NSOPENGL_PROFILE, "macos: invalid NSOpenGLProfile (valid choices are 1.0 and 4.1)") \ - _SAPP_LOGITEM_XMACRO(METAL_CREATE_SWAPCHAIN_DEPTH_TEXTURE_FAILED, "metal: failed to create swapchain depth-buffer texture") \ - _SAPP_LOGITEM_XMACRO(METAL_CREATE_SWAPCHAIN_MSAA_TEXTURE_FAILED, "metal: failed to create swapchain msaa texture") \ - _SAPP_LOGITEM_XMACRO(WIN32_LOAD_OPENGL32_DLL_FAILED, "failed loading opengl32.dll") \ - _SAPP_LOGITEM_XMACRO(WIN32_CREATE_HELPER_WINDOW_FAILED, "failed to create helper window") \ - _SAPP_LOGITEM_XMACRO(WIN32_HELPER_WINDOW_GETDC_FAILED, "failed to get helper window DC") \ - _SAPP_LOGITEM_XMACRO(WIN32_DUMMY_CONTEXT_SET_PIXELFORMAT_FAILED, "failed to set pixel format for dummy GL context") \ - _SAPP_LOGITEM_XMACRO(WIN32_CREATE_DUMMY_CONTEXT_FAILED, "failed to create dummy GL context") \ - _SAPP_LOGITEM_XMACRO(WIN32_DUMMY_CONTEXT_MAKE_CURRENT_FAILED, "failed to make dummy GL context current") \ - _SAPP_LOGITEM_XMACRO(WIN32_GET_PIXELFORMAT_ATTRIB_FAILED, "failed to get WGL pixel format attribute") \ - _SAPP_LOGITEM_XMACRO(WIN32_WGL_FIND_PIXELFORMAT_FAILED, "failed to find matching WGL pixel format") \ - _SAPP_LOGITEM_XMACRO(WIN32_WGL_DESCRIBE_PIXELFORMAT_FAILED, "failed to get pixel format descriptor") \ - _SAPP_LOGITEM_XMACRO(WIN32_WGL_SET_PIXELFORMAT_FAILED, "failed to set selected pixel format") \ - _SAPP_LOGITEM_XMACRO(WIN32_WGL_ARB_CREATE_CONTEXT_REQUIRED, "ARB_create_context required") \ - _SAPP_LOGITEM_XMACRO(WIN32_WGL_ARB_CREATE_CONTEXT_PROFILE_REQUIRED, "ARB_create_context_profile required") \ - _SAPP_LOGITEM_XMACRO(WIN32_WGL_OPENGL_VERSION_NOT_SUPPORTED, "requested OpenGL version not supported by GL driver (ERROR_INVALID_VERSION_ARB)") \ - _SAPP_LOGITEM_XMACRO(WIN32_WGL_OPENGL_PROFILE_NOT_SUPPORTED, "requested OpenGL profile not support by GL driver (ERROR_INVALID_PROFILE_ARB)") \ - _SAPP_LOGITEM_XMACRO(WIN32_WGL_INCOMPATIBLE_DEVICE_CONTEXT, "CreateContextAttribsARB failed with ERROR_INCOMPATIBLE_DEVICE_CONTEXTS_ARB") \ - _SAPP_LOGITEM_XMACRO(WIN32_WGL_CREATE_CONTEXT_ATTRIBS_FAILED_OTHER, "CreateContextAttribsARB failed for other reason") \ - _SAPP_LOGITEM_XMACRO(WIN32_D3D11_CREATE_DEVICE_AND_SWAPCHAIN_WITH_DEBUG_FAILED, "D3D11CreateDeviceAndSwapChain() with D3D11_CREATE_DEVICE_DEBUG failed, retrying without debug flag.") \ - _SAPP_LOGITEM_XMACRO(WIN32_D3D11_GET_IDXGIFACTORY_FAILED, "could not obtain IDXGIFactory object") \ - _SAPP_LOGITEM_XMACRO(WIN32_D3D11_GET_IDXGIADAPTER_FAILED, "could not obtain IDXGIAdapter object") \ - _SAPP_LOGITEM_XMACRO(WIN32_D3D11_QUERY_INTERFACE_IDXGIDEVICE1_FAILED, "could not obtain IDXGIDevice1 interface") \ - _SAPP_LOGITEM_XMACRO(WIN32_REGISTER_RAW_INPUT_DEVICES_FAILED_MOUSE_LOCK, "RegisterRawInputDevices() failed (on mouse lock)") \ - _SAPP_LOGITEM_XMACRO(WIN32_REGISTER_RAW_INPUT_DEVICES_FAILED_MOUSE_UNLOCK, "RegisterRawInputDevices() failed (on mouse unlock)") \ - _SAPP_LOGITEM_XMACRO(WIN32_GET_RAW_INPUT_DATA_FAILED, "GetRawInputData() failed") \ - _SAPP_LOGITEM_XMACRO(WIN32_DESTROYICON_FOR_CURSOR_FAILED, "DestroyIcon() for a cursor image failed") \ - _SAPP_LOGITEM_XMACRO(LINUX_GLX_LOAD_LIBGL_FAILED, "failed to load libGL") \ - _SAPP_LOGITEM_XMACRO(LINUX_GLX_LOAD_ENTRY_POINTS_FAILED, "failed to load GLX entry points") \ - _SAPP_LOGITEM_XMACRO(LINUX_GLX_EXTENSION_NOT_FOUND, "GLX extension not found") \ - _SAPP_LOGITEM_XMACRO(LINUX_GLX_QUERY_VERSION_FAILED, "failed to query GLX version") \ - _SAPP_LOGITEM_XMACRO(LINUX_GLX_VERSION_TOO_LOW, "GLX version too low (need at least 1.3)") \ - _SAPP_LOGITEM_XMACRO(LINUX_GLX_NO_GLXFBCONFIGS, "glXGetFBConfigs() returned no configs") \ - _SAPP_LOGITEM_XMACRO(LINUX_GLX_NO_SUITABLE_GLXFBCONFIG, "failed to find a suitable GLXFBConfig") \ - _SAPP_LOGITEM_XMACRO(LINUX_GLX_GET_VISUAL_FROM_FBCONFIG_FAILED, "glXGetVisualFromFBConfig failed") \ - _SAPP_LOGITEM_XMACRO(LINUX_GLX_REQUIRED_EXTENSIONS_MISSING, "GLX extensions ARB_create_context and ARB_create_context_profile missing") \ - _SAPP_LOGITEM_XMACRO(LINUX_GLX_CREATE_CONTEXT_FAILED, "Failed to create GL context via glXCreateContextAttribsARB") \ - _SAPP_LOGITEM_XMACRO(LINUX_GLX_CREATE_WINDOW_FAILED, "glXCreateWindow() failed") \ - _SAPP_LOGITEM_XMACRO(LINUX_X11_CREATE_WINDOW_FAILED, "XCreateWindow() failed") \ - _SAPP_LOGITEM_XMACRO(LINUX_EGL_BIND_OPENGL_API_FAILED, "eglBindAPI(EGL_OPENGL_API) failed") \ - _SAPP_LOGITEM_XMACRO(LINUX_EGL_BIND_OPENGL_ES_API_FAILED, "eglBindAPI(EGL_OPENGL_ES_API) failed") \ - _SAPP_LOGITEM_XMACRO(LINUX_EGL_GET_DISPLAY_FAILED, "eglGetDisplay() failed") \ - _SAPP_LOGITEM_XMACRO(LINUX_EGL_INITIALIZE_FAILED, "eglInitialize() failed") \ - _SAPP_LOGITEM_XMACRO(LINUX_EGL_NO_CONFIGS, "eglChooseConfig() returned no configs") \ - _SAPP_LOGITEM_XMACRO(LINUX_EGL_NO_NATIVE_VISUAL, "eglGetConfigAttrib() for EGL_NATIVE_VISUAL_ID failed") \ - _SAPP_LOGITEM_XMACRO(LINUX_EGL_GET_VISUAL_INFO_FAILED, "XGetVisualInfo() failed") \ - _SAPP_LOGITEM_XMACRO(LINUX_EGL_CREATE_WINDOW_SURFACE_FAILED, "eglCreateWindowSurface() failed") \ - _SAPP_LOGITEM_XMACRO(LINUX_EGL_CREATE_CONTEXT_FAILED, "eglCreateContext() failed") \ - _SAPP_LOGITEM_XMACRO(LINUX_EGL_MAKE_CURRENT_FAILED, "eglMakeCurrent() failed") \ - _SAPP_LOGITEM_XMACRO(LINUX_X11_OPEN_DISPLAY_FAILED, "XOpenDisplay() failed") \ - _SAPP_LOGITEM_XMACRO(LINUX_X11_QUERY_SYSTEM_DPI_FAILED, "failed to query system dpi value, assuming default 96.0") \ - _SAPP_LOGITEM_XMACRO(LINUX_X11_DROPPED_FILE_URI_WRONG_SCHEME, "dropped file URL doesn't start with 'file://'") \ - _SAPP_LOGITEM_XMACRO(LINUX_X11_FAILED_TO_BECOME_OWNER_OF_CLIPBOARD, "X11: Failed to become owner of clipboard selection") \ - _SAPP_LOGITEM_XMACRO(ANDROID_UNSUPPORTED_INPUT_EVENT_INPUT_CB, "unsupported input event encountered in _sapp_android_input_cb()") \ - _SAPP_LOGITEM_XMACRO(ANDROID_UNSUPPORTED_INPUT_EVENT_MAIN_CB, "unsupported input event encountered in _sapp_android_main_cb()") \ - _SAPP_LOGITEM_XMACRO(ANDROID_READ_MSG_FAILED, "failed to read message in _sapp_android_main_cb()") \ - _SAPP_LOGITEM_XMACRO(ANDROID_WRITE_MSG_FAILED, "failed to write message in _sapp_android_msg") \ - _SAPP_LOGITEM_XMACRO(ANDROID_MSG_CREATE, "MSG_CREATE") \ - _SAPP_LOGITEM_XMACRO(ANDROID_MSG_RESUME, "MSG_RESUME") \ - _SAPP_LOGITEM_XMACRO(ANDROID_MSG_PAUSE, "MSG_PAUSE") \ - _SAPP_LOGITEM_XMACRO(ANDROID_MSG_FOCUS, "MSG_FOCUS") \ - _SAPP_LOGITEM_XMACRO(ANDROID_MSG_NO_FOCUS, "MSG_NO_FOCUS") \ - _SAPP_LOGITEM_XMACRO(ANDROID_MSG_SET_NATIVE_WINDOW, "MSG_SET_NATIVE_WINDOW") \ - _SAPP_LOGITEM_XMACRO(ANDROID_MSG_SET_INPUT_QUEUE, "MSG_SET_INPUT_QUEUE") \ - _SAPP_LOGITEM_XMACRO(ANDROID_MSG_DESTROY, "MSG_DESTROY") \ - _SAPP_LOGITEM_XMACRO(ANDROID_UNKNOWN_MSG, "unknown msg type received") \ - _SAPP_LOGITEM_XMACRO(ANDROID_LOOP_THREAD_STARTED, "loop thread started") \ - _SAPP_LOGITEM_XMACRO(ANDROID_LOOP_THREAD_DONE, "loop thread done") \ - _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONSTART, "NativeActivity onStart()") \ - _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONRESUME, "NativeActivity onResume") \ - _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONSAVEINSTANCESTATE, "NativeActivity onSaveInstanceState") \ - _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONWINDOWFOCUSCHANGED, "NativeActivity onWindowFocusChanged") \ - _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONPAUSE, "NativeActivity onPause") \ - _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONSTOP, "NativeActivity onStop()") \ - _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONNATIVEWINDOWCREATED, "NativeActivity onNativeWindowCreated") \ - _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONNATIVEWINDOWDESTROYED, "NativeActivity onNativeWindowDestroyed") \ - _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONINPUTQUEUECREATED, "NativeActivity onInputQueueCreated") \ - _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONINPUTQUEUEDESTROYED, "NativeActivity onInputQueueDestroyed") \ - _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONCONFIGURATIONCHANGED, "NativeActivity onConfigurationChanged") \ - _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONLOWMEMORY, "NativeActivity onLowMemory") \ - _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONDESTROY, "NativeActivity onDestroy") \ - _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_DONE, "NativeActivity done") \ - _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONCREATE, "NativeActivity onCreate") \ - _SAPP_LOGITEM_XMACRO(ANDROID_CREATE_THREAD_PIPE_FAILED, "failed to create thread pipe") \ - _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_CREATE_SUCCESS, "NativeActivity successfully created") \ - _SAPP_LOGITEM_XMACRO(ANDROID_CHOREOGRAPHER_ENABLED, "Choreographer frame loop enabled") \ - _SAPP_LOGITEM_XMACRO(ANDROID_CHOREOGRAPHER_UNAVAILABLE, "Choreographer unavailable, using poll loop") \ - _SAPP_LOGITEM_XMACRO(WGPU_DEVICE_LOST, "wgpu: device lost") \ - _SAPP_LOGITEM_XMACRO(WGPU_DEVICE_LOG, "wgpu: device log") \ - _SAPP_LOGITEM_XMACRO(WGPU_DEVICE_UNCAPTURED_ERROR, "wgpu: uncaptured error") \ - _SAPP_LOGITEM_XMACRO(WGPU_SWAPCHAIN_CREATE_SURFACE_FAILED, "wgpu: failed to create surface for swapchain") \ - _SAPP_LOGITEM_XMACRO(WGPU_SWAPCHAIN_SURFACE_GET_CAPABILITIES_FAILED, "wgpu: wgpuSurfaceGetCapabilities failed") \ - _SAPP_LOGITEM_XMACRO(WGPU_SWAPCHAIN_CREATE_DEPTH_STENCIL_TEXTURE_FAILED, "wgpu: failed to create depth-stencil texture for swapchain") \ - _SAPP_LOGITEM_XMACRO(WGPU_SWAPCHAIN_CREATE_DEPTH_STENCIL_VIEW_FAILED, "wgpu: failed to create view object for swapchain depth-stencil texture") \ - _SAPP_LOGITEM_XMACRO(WGPU_SWAPCHAIN_CREATE_MSAA_TEXTURE_FAILED, "wgpu: failed to create msaa texture for swapchain") \ - _SAPP_LOGITEM_XMACRO(WGPU_SWAPCHAIN_CREATE_MSAA_VIEW_FAILED, "wgpu: failed to create view object for swapchain msaa texture") \ - _SAPP_LOGITEM_XMACRO(WGPU_SWAPCHAIN_GETCURRENTTEXTURE_FAILED, "wgpu: wgpuSurfaceGetCurrentTexture() failed") \ - _SAPP_LOGITEM_XMACRO(WGPU_REQUEST_DEVICE_STATUS_ERROR, "wgpu: requesting device failed with status 'error'") \ - _SAPP_LOGITEM_XMACRO(WGPU_REQUEST_DEVICE_STATUS_UNKNOWN, "wgpu: requesting device failed with status 'unknown'") \ - _SAPP_LOGITEM_XMACRO(WGPU_REQUEST_ADAPTER_STATUS_UNAVAILABLE, "wgpu: requesting adapter failed with 'unavailable'") \ - _SAPP_LOGITEM_XMACRO(WGPU_REQUEST_ADAPTER_STATUS_ERROR, "wgpu: requesting adapter failed with status 'error'") \ - _SAPP_LOGITEM_XMACRO(WGPU_REQUEST_ADAPTER_STATUS_UNKNOWN, "wgpu: requesting adapter failed with status 'unknown'") \ - _SAPP_LOGITEM_XMACRO(WGPU_CREATE_INSTANCE_FAILED, "wgpu: failed to create instance") \ - _SAPP_LOGITEM_XMACRO(VULKAN_REQUIRED_INSTANCE_EXTENSION_FUNCTION_MISSING, "vulkan: could not lookup a required instance extension function pointer") \ - _SAPP_LOGITEM_XMACRO(VULKAN_ALLOC_DEVICE_MEMORY_NO_SUITABLE_MEMORY_TYPE, "vulkan: could not find suitable memory type") \ - _SAPP_LOGITEM_XMACRO(VULKAN_ALLOCATE_MEMORY_FAILED, "vulkan: vkAllocateMemory() failed!") \ - _SAPP_LOGITEM_XMACRO(VULKAN_CREATE_INSTANCE_FAILED, "vulkan: vkCreateInstance failed") \ - _SAPP_LOGITEM_XMACRO(VULKAN_ENUMERATE_PHYSICAL_DEVICES_FAILED, "vulkan: vkEnumeratePhysicalDevices failed") \ - _SAPP_LOGITEM_XMACRO(VULKAN_NO_PHYSICAL_DEVICES_FOUND, "vulkan: vkEnumeratePhysicalDevices return no devices") \ - _SAPP_LOGITEM_XMACRO(VULKAN_NO_SUITABLE_PHYSICAL_DEVICE_FOUND, "vulkan: no suitable physical device found") \ - _SAPP_LOGITEM_XMACRO(VULKAN_CREATE_DEVICE_FAILED_EXTENSION_NOT_PRESENT, "vulkan: vkCreateDevice failed (extension not present)") \ - _SAPP_LOGITEM_XMACRO(VULKAN_CREATE_DEVICE_FAILED_FEATURE_NOT_PRESENT, "vulkan: vkCreateDevice failed (feature not present)") \ - _SAPP_LOGITEM_XMACRO(VULKAN_CREATE_DEVICE_FAILED_INITIALIZATION_FAILED, "vulkan: vkCreateDevice failed (initialization failed)") \ - _SAPP_LOGITEM_XMACRO(VULKAN_CREATE_DEVICE_FAILED_OTHER, "vulkan: vkCreateDevice failed (other)") \ - _SAPP_LOGITEM_XMACRO(VULKAN_CREATE_SURFACE_FAILED, "vulkan: vkCreate*SurfaceKHR failed") \ - _SAPP_LOGITEM_XMACRO(VULKAN_CREATE_SWAPCHAIN_FAILED, "vulkan: vkCreateSwapchainKHR failed") \ - _SAPP_LOGITEM_XMACRO(VULKAN_SWAPCHAIN_CREATE_IMAGE_VIEW_FAILED, "vulkan: vkCreateImageView for swapchain image failed") \ - _SAPP_LOGITEM_XMACRO(VULKAN_SWAPCHAIN_CREATE_IMAGE_FAILED, "vulkan: vkCreateImage for depth-stencil image failed") \ - _SAPP_LOGITEM_XMACRO(VULKAN_SWAPCHAIN_ALLOC_IMAGE_DEVICE_MEMORY_FAILED, "vulkan: failed to allocate device memory for depth-stencil image") \ - _SAPP_LOGITEM_XMACRO(VULKAN_SWAPCHAIN_BIND_IMAGE_MEMORY_FAILED, "vulkan: vkBindImageMemory() for depth-stencil image failed") \ - _SAPP_LOGITEM_XMACRO(VULKAN_ACQUIRE_NEXT_IMAGE_FAILED, "vulkan: vkAcquireNextImageKHR failed") \ - _SAPP_LOGITEM_XMACRO(VULKAN_QUEUE_PRESENT_FAILED, "vulkan: vkQueuePresentKHR failed") \ - _SAPP_LOGITEM_XMACRO(IMAGE_DATA_SIZE_MISMATCH, "image data size mismatch (must be width*height*4 bytes)") \ - _SAPP_LOGITEM_XMACRO(DROPPED_FILE_PATH_TOO_LONG, "dropped file path too long (sapp_desc.max_dropped_filed_path_length)") \ - _SAPP_LOGITEM_XMACRO(CLIPBOARD_STRING_TOO_BIG, "clipboard string didn't fit into clipboard buffer") \ - -#define _SAPP_LOGITEM_XMACRO(item,msg) SAPP_LOGITEM_##item, -typedef enum sapp_log_item { - _SAPP_LOG_ITEMS -} sapp_log_item; -#undef _SAPP_LOGITEM_XMACRO - -/* - sapp_pixel_format - - Defines the pixel format for swapchain surfaces. - - NOTE: when using sokol_gfx.h do not assume that the underlying - values are compatible with sg_pixel_format! - -*/ -typedef enum sapp_pixel_format { - _SAPP_PIXELFORMAT_DEFAULT, - SAPP_PIXELFORMAT_NONE, - SAPP_PIXELFORMAT_RGBA8, - SAPP_PIXELFORMAT_SRGB8A8, - SAPP_PIXELFORMAT_BGRA8, - SAPP_PIXELFORMAT_RGB10A2, // Modified by tettou771 for TrussC: 10-bit color output support - SAPP_PIXELFORMAT_SBGRA8, - SAPP_PIXELFORMAT_DEPTH, - SAPP_PIXELFORMAT_DEPTH_STENCIL, - _SAPP_PIXELFORMAT_FORCE_U32 = 0x7FFFFFFF -} sapp_pixel_format; - -/* - sapp_environment - - Used to provide runtime environment information to the - outside world (like default pixel formats and the backend - 3D API device pointer) via a call to sapp_get_environment(). - - NOTE: when using sokol_gfx.h, don't assume that sapp_environment - is binary compatible with sg_environment! Always use a translation - function like sglue_environment() to populate sg_environment - from sapp_environment! -*/ -typedef struct sapp_environment_defaults { - sapp_pixel_format color_format; - sapp_pixel_format depth_format; - int sample_count; -} sapp_environment_defaults; - -typedef struct sapp_metal_environment { - const void* device; -} sapp_metal_environment; - -typedef struct sapp_d3d11_environment { - const void* device; - const void* device_context; -} sapp_d3d11_environment; - -typedef struct sapp_wgpu_environment { - const void* device; -} sapp_wgpu_environment; - -typedef struct sapp_vulkan_environment { - const void* instance; - const void* physical_device; - const void* device; - const void* queue; - uint32_t queue_family_index; -} sapp_vulkan_environment; - -typedef struct sapp_environment { - sapp_environment_defaults defaults; - sapp_metal_environment metal; - sapp_d3d11_environment d3d11; - sapp_wgpu_environment wgpu; - sapp_vulkan_environment vulkan; -} sapp_environment; - -/* - sapp_swapchain - - Provides swapchain information for the current frame to the outside - world via a call to sapp_get_swapchain(). - - NOTE: sapp_get_swapchain() must be called exactly once per frame since - on some backends it will also acquire the next swapchain image. - - NOTE: when using sokol_gfx.h, don't assume that the sapp_swapchain struct - has the same memory layout as sg_swapchain! Use the sokol_log.h helper - function sglue_swapchain() to translate sapp_swapchain into a - sg_swapchain instead. -*/ -typedef struct sapp_metal_swapchain { - const void* current_drawable; // CAMetalDrawable (NOT MTLDrawable!!!) - const void* depth_stencil_texture; // MTLTexture - const void* msaa_color_texture; // MTLTexture -} sapp_metal_swapchain; - -typedef struct sapp_d3d11_swapchain { - const void* render_view; // ID3D11RenderTargetView - const void* resolve_view; // ID3D11RenderTargetView - const void* depth_stencil_view; // ID3D11DepthStencilView -} sapp_d3d11_swapchain; - -typedef struct sapp_wgpu_swapchain { - const void* render_view; // WGPUTextureView - const void* resolve_view; // WGPUTextureView - const void* depth_stencil_view; // WGPUTextureView -} sapp_wgpu_swapchain; - -typedef struct sapp_vulkan_swapchain { - const void* render_image; // vkImage - const void* render_view; // vkImageView - const void* resolve_image; // vkImage; - const void* resolve_view; // vkImageView - const void* depth_stencil_image; // vkImage - const void* depth_stencil_view; // vkImageView - const void* render_finished_semaphore; // vkSemaphore - const void* present_complete_semaphore; // vkSemaphore -} sapp_vulkan_swapchain; - -typedef struct sapp_gl_swapchain { - uint32_t framebuffer; // GL framebuffer object -} sapp_gl_swapchain; - -typedef struct sapp_swapchain { - int width; - int height; - int sample_count; - sapp_pixel_format color_format; - sapp_pixel_format depth_format; - sapp_metal_swapchain metal; - sapp_d3d11_swapchain d3d11; - sapp_wgpu_swapchain wgpu; - sapp_vulkan_swapchain vulkan; - sapp_gl_swapchain gl; -} sapp_swapchain; - -/* - sapp_logger - - Used in sapp_desc to provide a logging function. Please be aware that - without logging function, sokol-app will be completely silent, e.g. it will - not report errors or warnings. For maximum error verbosity, compile in - debug mode (e.g. NDEBUG *not* defined) and install a logger (for instance - the standard logging function from sokol_log.h). -*/ -typedef struct sapp_logger { - void (*func)( - const char* tag, // always "sapp" - uint32_t log_level, // 0=panic, 1=error, 2=warning, 3=info - uint32_t log_item_id, // SAPP_LOGITEM_* - const char* message_or_null, // a message string, may be nullptr in release mode - uint32_t line_nr, // line number in sokol_app.h - const char* filename_or_null, // source filename, may be nullptr in release mode - void* user_data); - void* user_data; -} sapp_logger; - -/* - sokol-app initialization options, used as return value of sokol_main() - or sapp_run() argument. -*/ -typedef struct sapp_gl_desc { - int major_version; // override GL/GLES major and minor version (defaults: GL4.1 (macOS) or GL4.3, GLES3.1 (Android) or GLES3.0 - int minor_version; -} sapp_gl_desc; - -typedef struct sapp_win32_desc { - bool console_utf8; // if true, set the output console codepage to UTF-8 - bool console_create; // if true, attach stdout/stderr to a new console window - bool console_attach; // if true, attach stdout/stderr to parent process -} sapp_win32_desc; - -typedef struct sapp_html5_desc { - const char* canvas_selector; // css selector of the HTML5 canvas element, default is "#canvas" - bool canvas_resize; // if true, the HTML5 canvas size is set to sapp_desc.width/height, otherwise canvas size is tracked - bool preserve_drawing_buffer; // HTML5 only: whether to preserve default framebuffer content between frames - bool premultiplied_alpha; // HTML5 only: whether the rendered pixels use premultiplied alpha convention - bool ask_leave_site; // initial state of the internal html5_ask_leave_site flag (see sapp_html5_ask_leave_site()) - bool update_document_title; // if true, update the HTML document.title with sapp_desc.window_title - bool bubble_mouse_events; // if true, mouse events will bubble up to the web page - bool bubble_touch_events; // same for touch events - bool bubble_wheel_events; // same for wheel events - bool bubble_key_events; // if true, bubble up *all* key events to browser, not just key events that represent characters - bool bubble_char_events; // if true, bubble up character events to browser - bool use_emsc_set_main_loop; // if true, use emscripten_set_main_loop() instead of emscripten_request_animation_frame_loop() - bool emsc_set_main_loop_simulate_infinite_loop; // this will be passed as the simulate_infinite_loop arg to emscripten_set_main_loop() -} sapp_html5_desc; - -typedef struct sapp_ios_desc { - bool keyboard_resizes_canvas; // if true, showing the iOS keyboard shrinks the canvas -} sapp_ios_desc; - -typedef struct sapp_desc { - void (*init_cb)(void); // these are the user-provided callbacks without user data - void (*frame_cb)(void); - void (*cleanup_cb)(void); - void (*event_cb)(const sapp_event*); - - void* user_data; // these are the user-provided callbacks with user data - void (*init_userdata_cb)(void*); - void (*frame_userdata_cb)(void*); - void (*cleanup_userdata_cb)(void*); - void (*event_userdata_cb)(const sapp_event*, void*); - - int width; // the preferred width of the window / canvas - int height; // the preferred height of the window / canvas - int sample_count; // MSAA sample count - int swap_interval; // the preferred swap interval (ignored on some platforms) - bool high_dpi; // whether the rendering canvas is full-resolution on HighDPI displays - bool fullscreen; // whether the window should be created in fullscreen mode - bool alpha; // whether the framebuffer should have an alpha channel (ignored on some platforms) - const char* window_title; // the window title as UTF-8 encoded string - bool enable_clipboard; // enable clipboard access, default is false - int clipboard_size; // max size of clipboard content in bytes - bool enable_dragndrop; // enable file dropping (drag'n'drop), default is false - int max_dropped_files; // max number of dropped files to process (default: 1) - int max_dropped_file_path_length; // max length in bytes of a dropped UTF-8 file path (default: 2048) - sapp_icon_desc icon; // the initial window icon to set - sapp_allocator allocator; // optional memory allocation overrides (default: malloc/free) - sapp_logger logger; // logging callback override (default: NO LOGGING!) - - // backend-specific options - sapp_gl_desc gl; - sapp_win32_desc win32; - sapp_html5_desc html5; - sapp_ios_desc ios; -} sapp_desc; - -/* HTML5 specific: request and response structs for - asynchronously loading dropped-file content. -*/ -typedef enum sapp_html5_fetch_error { - SAPP_HTML5_FETCH_ERROR_NO_ERROR, - SAPP_HTML5_FETCH_ERROR_BUFFER_TOO_SMALL, - SAPP_HTML5_FETCH_ERROR_OTHER, -} sapp_html5_fetch_error; - -typedef struct sapp_html5_fetch_response { - bool succeeded; // true if the loading operation has succeeded - sapp_html5_fetch_error error_code; - int file_index; // index of the dropped file (0..sapp_get_num_dropped_filed()-1) - sapp_range data; // pointer and size of the fetched data (data.ptr == buffer.ptr, data.size <= buffer.size) - sapp_range buffer; // the user-provided buffer ptr/size pair (buffer.ptr == data.ptr, buffer.size >= data.size) - void* user_data; // user-provided user data pointer -} sapp_html5_fetch_response; - -typedef struct sapp_html5_fetch_request { - int dropped_file_index; // 0..sapp_get_num_dropped_files()-1 - void (*callback)(const sapp_html5_fetch_response*); // response callback function pointer (required) - sapp_range buffer; // ptr/size of a memory buffer to load the data into - void* user_data; // optional userdata pointer -} sapp_html5_fetch_request; - -/* - sapp_mouse_cursor - - Predefined cursor image definitions, set with sapp_set_mouse_cursor(sapp_mouse_cursor cursor) -*/ -typedef enum sapp_mouse_cursor { - SAPP_MOUSECURSOR_DEFAULT = 0, // equivalent with system default cursor - SAPP_MOUSECURSOR_ARROW, - SAPP_MOUSECURSOR_IBEAM, - SAPP_MOUSECURSOR_CROSSHAIR, - SAPP_MOUSECURSOR_POINTING_HAND, - SAPP_MOUSECURSOR_RESIZE_EW, - SAPP_MOUSECURSOR_RESIZE_NS, - SAPP_MOUSECURSOR_RESIZE_NWSE, - SAPP_MOUSECURSOR_RESIZE_NESW, - SAPP_MOUSECURSOR_RESIZE_ALL, - SAPP_MOUSECURSOR_NOT_ALLOWED, - SAPP_MOUSECURSOR_CUSTOM_0, - SAPP_MOUSECURSOR_CUSTOM_1, - SAPP_MOUSECURSOR_CUSTOM_2, - SAPP_MOUSECURSOR_CUSTOM_3, - SAPP_MOUSECURSOR_CUSTOM_4, - SAPP_MOUSECURSOR_CUSTOM_5, - SAPP_MOUSECURSOR_CUSTOM_6, - SAPP_MOUSECURSOR_CUSTOM_7, - SAPP_MOUSECURSOR_CUSTOM_8, - SAPP_MOUSECURSOR_CUSTOM_9, - SAPP_MOUSECURSOR_CUSTOM_10, - SAPP_MOUSECURSOR_CUSTOM_11, - SAPP_MOUSECURSOR_CUSTOM_12, - SAPP_MOUSECURSOR_CUSTOM_13, - SAPP_MOUSECURSOR_CUSTOM_14, - SAPP_MOUSECURSOR_CUSTOM_15, - _SAPP_MOUSECURSOR_NUM, -} sapp_mouse_cursor; - -/* user-provided functions */ -extern sapp_desc sokol_main(int argc, char* argv[]); - -/* returns true after sokol-app has been initialized */ -SOKOL_APP_API_DECL bool sapp_isvalid(void); -/* returns the current framebuffer width in pixels */ -SOKOL_APP_API_DECL int sapp_width(void); -/* same as sapp_width(), but returns float */ -SOKOL_APP_API_DECL float sapp_widthf(void); -/* returns the current framebuffer height in pixels */ -SOKOL_APP_API_DECL int sapp_height(void); -/* same as sapp_height(), but returns float */ -SOKOL_APP_API_DECL float sapp_heightf(void); -/* get default framebuffer color pixel format */ -SOKOL_APP_API_DECL sapp_pixel_format sapp_color_format(void); -/* get default framebuffer depth pixel format */ -SOKOL_APP_API_DECL sapp_pixel_format sapp_depth_format(void); -/* get default framebuffer sample count */ -SOKOL_APP_API_DECL int sapp_sample_count(void); -/* returns true when high_dpi was requested and actually running in a high-dpi scenario */ -SOKOL_APP_API_DECL bool sapp_high_dpi(void); -/* returns the dpi scaling factor (window pixels to framebuffer pixels) */ -SOKOL_APP_API_DECL float sapp_dpi_scale(void); -/* show or hide the mobile device onscreen keyboard */ -SOKOL_APP_API_DECL void sapp_show_keyboard(bool show); -/* return true if the mobile device onscreen keyboard is currently shown */ -SOKOL_APP_API_DECL bool sapp_keyboard_shown(void); -/* query fullscreen mode */ -SOKOL_APP_API_DECL bool sapp_is_fullscreen(void); -/* toggle fullscreen mode */ -SOKOL_APP_API_DECL void sapp_toggle_fullscreen(void); -/* show or hide the mouse cursor */ -SOKOL_APP_API_DECL void sapp_show_mouse(bool show); -/* show or hide the mouse cursor */ -SOKOL_APP_API_DECL bool sapp_mouse_shown(void); -/* enable/disable mouse-pointer-lock mode */ -SOKOL_APP_API_DECL void sapp_lock_mouse(bool lock); -/* return true if in mouse-pointer-lock mode (this may toggle a few frames later) */ -SOKOL_APP_API_DECL bool sapp_mouse_locked(void); -/* set mouse cursor type */ -SOKOL_APP_API_DECL void sapp_set_mouse_cursor(sapp_mouse_cursor cursor); -/* get current mouse cursor type */ -SOKOL_APP_API_DECL sapp_mouse_cursor sapp_get_mouse_cursor(void); -/* associate a custom mouse cursor image to a sapp_mouse_cursor enum entry */ -SOKOL_APP_API_DECL sapp_mouse_cursor sapp_bind_mouse_cursor_image(sapp_mouse_cursor cursor, const sapp_image_desc* desc); -/* restore the sapp_mouse_cursor enum entry to it's default system appearance */ -SOKOL_APP_API_DECL void sapp_unbind_mouse_cursor_image(sapp_mouse_cursor cursor); -/* return the userdata pointer optionally provided in sapp_desc */ -SOKOL_APP_API_DECL void* sapp_userdata(void); -/* return a copy of the sapp_desc structure */ -SOKOL_APP_API_DECL sapp_desc sapp_query_desc(void); -/* initiate a "soft quit" (sends SAPP_EVENTTYPE_QUIT_REQUESTED) */ -SOKOL_APP_API_DECL void sapp_request_quit(void); -/* cancel a pending quit (when SAPP_EVENTTYPE_QUIT_REQUESTED has been received) */ -SOKOL_APP_API_DECL void sapp_cancel_quit(void); -/* initiate a "hard quit" (quit application without sending SAPP_EVENTTYPE_QUIT_REQUESTED) */ -SOKOL_APP_API_DECL void sapp_quit(void); -/* call from inside event callback to consume the current event (don't forward to platform) */ -SOKOL_APP_API_DECL void sapp_consume_event(void); -/* get the current frame counter (for comparison with sapp_event.frame_count) */ -SOKOL_APP_API_DECL uint64_t sapp_frame_count(void); -/* get an averaged/smoothed frame duration in seconds */ -SOKOL_APP_API_DECL double sapp_frame_duration(void); -/* get 'raw' unfiltered frame duration in seconds */ -SOKOL_APP_API_DECL double sapp_frame_duration_unfiltered(void); -/* write string into clipboard */ -SOKOL_APP_API_DECL void sapp_set_clipboard_string(const char* str); -/* read string from clipboard (usually during SAPP_EVENTTYPE_CLIPBOARD_PASTED) */ -SOKOL_APP_API_DECL const char* sapp_get_clipboard_string(void); -/* set the window title (only on desktop platforms) */ -SOKOL_APP_API_DECL void sapp_set_window_title(const char* str); -/* set the window icon (only on Windows and Linux) */ -SOKOL_APP_API_DECL void sapp_set_icon(const sapp_icon_desc* icon_desc); -/* gets the total number of dropped files (after an SAPP_EVENTTYPE_FILES_DROPPED event) */ -SOKOL_APP_API_DECL int sapp_get_num_dropped_files(void); -/* gets the dropped file paths */ -SOKOL_APP_API_DECL const char* sapp_get_dropped_file_path(int index); - -/* special run-function for SOKOL_NO_ENTRY (in standard mode this is an empty stub) */ -SOKOL_APP_API_DECL void sapp_run(const sapp_desc* desc); - -/* get runtime environment information */ -SOKOL_APP_API_DECL sapp_environment sapp_get_environment(void); -/* get current frame's swapchain information (call once per frame!) */ -SOKOL_APP_API_DECL sapp_swapchain sapp_get_swapchain(void); - -/* EGL: get EGLDisplay object */ -SOKOL_APP_API_DECL const void* sapp_egl_get_display(void); -/* EGL: get EGLContext object */ -SOKOL_APP_API_DECL const void* sapp_egl_get_context(void); - -/* HTML5: enable or disable the hardwired "Leave Site?" dialog box */ -SOKOL_APP_API_DECL void sapp_html5_ask_leave_site(bool ask); -/* HTML5: get byte size of a dropped file */ -SOKOL_APP_API_DECL uint32_t sapp_html5_get_dropped_file_size(int index); -/* HTML5: asynchronously load the content of a dropped file */ -SOKOL_APP_API_DECL void sapp_html5_fetch_dropped_file(const sapp_html5_fetch_request* request); - -/* macOS: get bridged pointer to macOS NSWindow */ -SOKOL_APP_API_DECL const void* sapp_macos_get_window(void); -/* iOS: get bridged pointer to iOS UIWindow */ -SOKOL_APP_API_DECL const void* sapp_ios_get_window(void); -/* iOS: set supported interface orientations (UIInterfaceOrientationMask values) */ -SOKOL_APP_API_DECL void sapp_ios_set_supported_orientations(uint32_t mask); - -/* D3D11: get pointer to IDXGISwapChain object */ -SOKOL_APP_API_DECL const void* sapp_d3d11_get_swap_chain(void); - -/* Win32: get the HWND window handle */ -SOKOL_APP_API_DECL const void* sapp_win32_get_hwnd(void); - -/* GL: get major version */ -SOKOL_APP_API_DECL int sapp_gl_get_major_version(void); -/* GL: get minor version */ -SOKOL_APP_API_DECL int sapp_gl_get_minor_version(void); -/* GL: return true if the context is GLES */ -SOKOL_APP_API_DECL bool sapp_gl_is_gles(void); - -/* X11: get Window */ -SOKOL_APP_API_DECL const void* sapp_x11_get_window(void); -/* X11: get Display */ -SOKOL_APP_API_DECL const void* sapp_x11_get_display(void); - -/* Android: get native activity handle */ -SOKOL_APP_API_DECL const void* sapp_android_get_native_activity(void); - -// Modified by tettou771 for TrussC: skip the next present call (for event-driven rendering) -SOKOL_APP_API_DECL void sapp_skip_present(void); - -#ifdef __cplusplus -} /* extern "C" */ - -/* reference-based equivalents for C++ */ -inline void sapp_run(const sapp_desc& desc) { return sapp_run(&desc); } - -#endif - -#endif // SOKOL_APP_INCLUDED - -// ██ ███ ███ ██████ ██ ███████ ███ ███ ███████ ███ ██ ████████ █████ ████████ ██ ██████ ███ ██ -// ██ ████ ████ ██ ██ ██ ██ ████ ████ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ -// ██ ██ ████ ██ ██████ ██ █████ ██ ████ ██ █████ ██ ██ ██ ██ ███████ ██ ██ ██ ██ ██ ██ ██ -// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ██ ██ ██ ██ ███████ ███████ ██ ██ ███████ ██ ████ ██ ██ ██ ██ ██ ██████ ██ ████ -// -// >>implementation -#ifdef SOKOL_APP_IMPL -#define SOKOL_APP_IMPL_INCLUDED (1) - -#if defined(SOKOL_MALLOC) || defined(SOKOL_CALLOC) || defined(SOKOL_FREE) -#error "SOKOL_MALLOC/CALLOC/FREE macros are no longer supported, please use sapp_desc.allocator to override memory allocation functions" -#endif - -#include // malloc, free -#include // memset, strncmp -#include // size_t -#include // roundf - -// helper macros -#define _sapp_def(val, def) (((val) == 0) ? (def) : (val)) -#define _sapp_absf(a) (((a)<0.0f)?-(a):(a)) - -#ifdef __cplusplus -#define _SAPP_STRUCT(TYPE, NAME) TYPE NAME = {} -#else -#define _SAPP_STRUCT(TYPE, NAME) TYPE NAME = {0} -#endif - -#define _SAPP_MAX_TITLE_LENGTH (128) -#define _SAPP_FALLBACK_DEFAULT_WINDOW_WIDTH (640) -#define _SAPP_FALLBACK_DEFAULT_WINDOW_HEIGHT (480) - -// check if the config defines are alright -#if defined(__APPLE__) - // see https://clang.llvm.org/docs/LanguageExtensions.html#automatic-reference-counting - #if !defined(__cplusplus) - #if __has_feature(objc_arc) && !__has_feature(objc_arc_fields) - #error "sokol_app.h requires __has_feature(objc_arc_field) if ARC is enabled (use a more recent compiler version)" - #endif - #endif - #define _SAPP_APPLE (1) - #include - #if defined(TARGET_OS_IPHONE) && !TARGET_OS_IPHONE - // MacOS - #define _SAPP_MACOS (1) - #if !defined(SOKOL_METAL) && !defined(SOKOL_GLCORE) && !defined(SOKOL_WGPU) - #error("sokol_app.h: unknown 3D API selected for MacOS, must be SOKOL_METAL, SOKOL_GLCORE or SOKOL_WGPU") - #endif - #else - // iOS or iOS Simulator - #define _SAPP_IOS (1) - #if !defined(SOKOL_METAL) && !defined(SOKOL_GLES3) - #error("sokol_app.h: unknown 3D API selected for iOS, must be SOKOL_METAL or SOKOL_GLES3") - #endif - #if TARGET_OS_TV - #define _SAPP_TVOS (1) - #endif - #endif -#elif defined(__EMSCRIPTEN__) - // Emscripten - #define _SAPP_EMSCRIPTEN (1) - #if !defined(SOKOL_GLES3) && !defined(SOKOL_WGPU) - #error("sokol_app.h: unknown 3D API selected for emscripten, must be SOKOL_GLES3 or SOKOL_WGPU") - #endif -#elif defined(_WIN32) - // Windows (D3D11 or GL) - #define _SAPP_WIN32 (1) - #if !defined(SOKOL_D3D11) && !defined(SOKOL_GLCORE) && !defined(SOKOL_WGPU) && !defined(SOKOL_VULKAN) && !defined(SOKOL_NOAPI) - #error("sokol_app.h: unknown 3D API selected for Win32, must be SOKOL_D3D11, SOKOL_GLCORE, SOKOL_WGPU, SOKOL_VULKAN or SOKOL_NOAPI") - #endif - #if defined(SOKOL_VULKAN) - #define VK_USE_PLATFORM_WIN32_KHR - #include - #endif -#elif defined(__ANDROID__) - // Android - #define _SAPP_ANDROID (1) - #if !defined(SOKOL_GLES3) - #error("sokol_app.h: unknown 3D API selected for Android, must be SOKOL_GLES3") - #endif - #if defined(SOKOL_NO_ENTRY) - #error("sokol_app.h: SOKOL_NO_ENTRY is not supported on Android") - #endif -#elif defined(__linux__) || defined(__unix__) - // Linux - #define _SAPP_LINUX (1) - #if !defined(SOKOL_GLCORE) && !defined(SOKOL_GLES3) && !defined(SOKOL_WGPU) && !defined(SOKOL_VULKAN) - #error("sokol_app.h: unknown 3D API selected for Linux, must be SOKOL_GLCORE, SOKOL_GLES3, SOKOL_WGPU or SOKOL_VULKAN") - #endif - #if defined(SOKOL_GLCORE) - #if defined(SOKOL_FORCE_EGL) - #define _SAPP_EGL (1) - #else - #define _SAPP_GLX (1) - #endif - #define GL_GLEXT_PROTOTYPES - #include - #elif defined(SOKOL_GLES3) - #define _SAPP_EGL (1) - #include - #include - #elif defined(SOKOL_VULKAN) - #define VK_USE_PLATFORM_XLIB_KHR - #include - #endif -#else -#error "sokol_app.h: Unknown platform" -#endif - -#if defined(SOKOL_GLCORE) || defined(SOKOL_GLES3) - #define _SAPP_ANY_GL (1) -#endif - -#ifndef SOKOL_API_IMPL - #define SOKOL_API_IMPL -#endif -#ifndef SOKOL_DEBUG - #ifndef NDEBUG - #define SOKOL_DEBUG - #endif -#endif -#ifndef SOKOL_ASSERT - #include - #define SOKOL_ASSERT(c) assert(c) -#endif -#ifndef SOKOL_UNREACHABLE - #define SOKOL_UNREACHABLE SOKOL_ASSERT(false) -#endif - -#ifndef _SOKOL_PRIVATE - #if defined(__GNUC__) || defined(__clang__) - #define _SOKOL_PRIVATE __attribute__((unused)) static - #else - #define _SOKOL_PRIVATE static - #endif -#endif -#ifndef _SOKOL_UNUSED - #define _SOKOL_UNUSED(x) (void)(x) -#endif - -#if defined(SOKOL_WGPU) - #include - #if !defined(__EMSCRIPTEN__) - #define _SAPP_WGPU_HAS_WAIT (1) - #endif -#endif - -#if defined(_SAPP_APPLE) - #ifndef GL_SILENCE_DEPRECATION - #define GL_SILENCE_DEPRECATION - #endif - #if defined(_SAPP_MACOS) - #import - #if defined(SOKOL_METAL) - #import - #import - #import - #elif defined(SOKOL_WGPU) - #import - #import - #elif defined(_SAPP_ANY_GL) - #include - #endif - #elif defined(_SAPP_IOS) - #import - #if defined(SOKOL_METAL) - #import - #import - #import - #elif defined(_SAPP_ANY_GL) - #import - #include - #endif - #endif - #include - #include -#elif defined(_SAPP_EMSCRIPTEN) - #if defined(SOKOL_GLES3) - #include - #endif - #include - #include -#elif defined(_SAPP_WIN32) - #ifdef _MSC_VER - #pragma warning(push) - #pragma warning(disable:4201) /* nonstandard extension used: nameless struct/union */ - #pragma warning(disable:4204) /* nonstandard extension used: non-constant aggregate initializer */ - #pragma warning(disable:4054) /* 'type cast': from function pointer */ - #pragma warning(disable:4055) /* 'type cast': from data pointer */ - #pragma warning(disable:4505) /* unreferenced local function has been removed */ - #pragma warning(disable:4115) /* /W4: 'ID3D11ModuleInstance': named type definition in parentheses (in d3d11.h) */ - #endif - #ifndef WIN32_LEAN_AND_MEAN - #define WIN32_LEAN_AND_MEAN - #endif - #ifndef NOMINMAX - #define NOMINMAX - #endif - #include - #include - #include - - #if defined(__GNUC__) - #pragma GCC diagnostic push - #pragma GCC diagnostic ignored "-Wunknown-pragmas" - #endif - - #if !defined(SOKOL_NO_ENTRY) // if SOKOL_NO_ENTRY is defined, it's the application's responsibility to use the right subsystem - - #if defined(SOKOL_WIN32_FORCE_MAIN) && defined(SOKOL_WIN32_FORCE_WINMAIN) - // If both are defined, it's the application's responsibility to use the right subsystem - #elif defined(SOKOL_WIN32_FORCE_MAIN) - #pragma comment (linker, "/subsystem:console") - #else - #pragma comment (linker, "/subsystem:windows") - #endif - #endif - #include /* freopen_s() */ - #include /* wcslen() */ - - #pragma comment (lib, "kernel32") - #pragma comment (lib, "user32") - #pragma comment (lib, "shell32") /* CommandLineToArgvW, DragQueryFileW, DragFinished */ - #pragma comment (lib, "gdi32") - #if defined(SOKOL_D3D11) - #pragma comment (lib, "dxgi") - #pragma comment (lib, "d3d11") - #endif - - #if defined(__GNUC__) - #pragma GCC diagnostic pop - #endif - - #if defined(SOKOL_D3D11) - #ifndef D3D11_NO_HELPERS - #define D3D11_NO_HELPERS - #endif - #include - #include - // DXGI_SWAP_EFFECT_FLIP_DISCARD is only defined in newer Windows SDKs, so don't depend on it - #define _SAPP_DXGI_SWAP_EFFECT_FLIP_DISCARD (4) - #endif - #ifndef WM_MOUSEHWHEEL /* see https://github.com/floooh/sokol/issues/138 */ - #define WM_MOUSEHWHEEL (0x020E) - #endif - #ifndef WM_DPICHANGED - #define WM_DPICHANGED (0x02E0) - #endif -#elif defined(_SAPP_ANDROID) - #include - #include - #include - #include - #include // [TrussC] AConfiguration for display density - #include - #if __ANDROID_API__ >= 29 - #include - #endif - #include - #include -#elif defined(_SAPP_LINUX) - #define GL_GLEXT_PROTOTYPES - #include - #include - #include - #include - #include - #include - #include - #include - #include /* XC_* font cursors */ - #include /* CARD32 */ - #if defined(_SAPP_EGL) - #include - #endif - #include /* dlopen, dlsym, dlclose */ - #include /* LONG_MAX */ - #include /* only used a linker-guard, search for _sapp_linux_run() and see first comment */ - #include - #include -#endif - -#if defined(_SAPP_APPLE) - // this is ARC compatible - #if defined(__cplusplus) - #define _SAPP_CLEAR_ARC_STRUCT(type, item) { item = type(); } - #else - #define _SAPP_CLEAR_ARC_STRUCT(type, item) { item = (type) { 0 }; } - #endif -#else - #define _SAPP_CLEAR_ARC_STRUCT(type, item) { _sapp_clear(&item, sizeof(item)); } -#endif - - -// ███████ ██████ █████ ███ ███ ███████ ████████ ██ ███ ███ ██ ███ ██ ██████ -// ██ ██ ██ ██ ██ ████ ████ ██ ██ ██ ████ ████ ██ ████ ██ ██ -// █████ ██████ ███████ ██ ████ ██ █████ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ███ -// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ██ ██ ██ ██ ██ ██ ██ ███████ ██ ██ ██ ██ ██ ██ ████ ██████ -// -// >>frame timing -typedef struct { - #if defined(_SAPP_APPLE) - struct { - mach_timebase_info_data_t timebase; - uint64_t start; - } mach; - #elif defined(_SAPP_EMSCRIPTEN) - int _dummy; - #elif defined(_SAPP_WIN32) - struct { - LARGE_INTEGER freq; - LARGE_INTEGER start; - } win; - #else // Linux, Android, ... - #ifdef CLOCK_MONOTONIC - #define _SAPP_CLOCK_MONOTONIC CLOCK_MONOTONIC - #else - // on some embedded platforms, CLOCK_MONOTONIC isn't defined - #define _SAPP_CLOCK_MONOTONIC (1) - #endif - struct { - uint64_t start; - } posix; - #endif -} _sapp_timestamp_t; - -_SOKOL_PRIVATE int64_t _sapp_int64_muldiv(int64_t value, int64_t numer, int64_t denom) { - int64_t q = value / denom; - int64_t r = value % denom; - return q * numer + r * numer / denom; -} - -_SOKOL_PRIVATE void _sapp_timestamp_init(_sapp_timestamp_t* ts) { - #if defined(_SAPP_APPLE) - mach_timebase_info(&ts->mach.timebase); - ts->mach.start = mach_absolute_time(); - #elif defined(_SAPP_EMSCRIPTEN) - (void)ts; - #elif defined(_SAPP_WIN32) - QueryPerformanceFrequency(&ts->win.freq); - QueryPerformanceCounter(&ts->win.start); - #else - struct timespec tspec; - clock_gettime(_SAPP_CLOCK_MONOTONIC, &tspec); - ts->posix.start = (uint64_t)tspec.tv_sec*1000000000 + (uint64_t)tspec.tv_nsec; - #endif -} - -_SOKOL_PRIVATE double _sapp_timestamp_now(_sapp_timestamp_t* ts) { - #if defined(_SAPP_APPLE) - const uint64_t traw = mach_absolute_time() - ts->mach.start; - const uint64_t now = (uint64_t) _sapp_int64_muldiv((int64_t)traw, (int64_t)ts->mach.timebase.numer, (int64_t)ts->mach.timebase.denom); - return (double)now / 1000000000.0; - #elif defined(_SAPP_EMSCRIPTEN) - (void)ts; - SOKOL_ASSERT(false); - return 0.0; - #elif defined(_SAPP_WIN32) - LARGE_INTEGER qpc; - QueryPerformanceCounter(&qpc); - const uint64_t now = (uint64_t)_sapp_int64_muldiv(qpc.QuadPart - ts->win.start.QuadPart, 1000000000, ts->win.freq.QuadPart); - return (double)now / 1000000000.0; - #else - struct timespec tspec; - clock_gettime(_SAPP_CLOCK_MONOTONIC, &tspec); - const uint64_t now = ((uint64_t)tspec.tv_sec*1000000000 + (uint64_t)tspec.tv_nsec) - ts->posix.start; - return (double)now / 1000000000.0; - #endif -} - -typedef struct { - _sapp_timestamp_t timestamp; - double dt_min; // config: min clamp value for unfiltered time delta (seconds) - double dt_max; // config: max clamp value for unfiltered time delta (seconds) - double dt_threshold; // config: threshold time delta for 'resetting' filtering (default: 0.004s, 4ms) - double alpha; // config: smoothing constant, lower values smoother, higher values faster response - double last; // last absolute time in seconds - double dt; // unfiltered frame delta in seconds, clamped to dt_min/dt_max - double ema; // intermediate ema-filter result - double smooth_dt; // smoothed frame delta in seconds -} _sapp_timing_t; - -_SOKOL_PRIVATE void _sapp_timing_init(_sapp_timing_t* t) { - _sapp_timestamp_init(&t->timestamp); - t->dt_min = 0.000001; // 1 us - t->dt_max = 0.1; // 100 ms - t->dt_threshold = 0.004; // 4ms - t->alpha = 0.025; - t->dt = 1.0 / 60.0; // a 'likely' non-null value - t->ema = t->dt; - t->smooth_dt = t->dt; -} - -_SOKOL_PRIVATE double _sapp_timing_clamp(_sapp_timing_t* t, double dt) { - SOKOL_ASSERT((t->dt_min > 0.0) && (t->dt_max > 0.0) && (t->dt_max >= t->dt_min)); - if (dt < t->dt_min) { - return t->dt_min; - } else if (dt > t->dt_max) { - return t->dt_max; - } else { - return dt; - } -} - -_SOKOL_PRIVATE void _sapp_timing_delta(_sapp_timing_t* t, double dt) { - // first clamp raw dt against min/max (min avoid division by zero, max - // may avoids glitches and 'death-spirals' during debugging - dt = _sapp_timing_clamp(t, dt); - t->dt = dt; - const double error = fabs(dt - t->smooth_dt); - if (error > t->dt_threshold) { - // 'reset' filter when new delta is outside threshold - t->ema = dt; - t->smooth_dt = dt; - } else { - // simple ema-filter with fixed alpha - t->ema = t->ema + t->alpha * (dt - t->ema); - t->smooth_dt = _sapp_timing_clamp(t, t->ema); - } -} - -_SOKOL_PRIVATE void _sapp_timing_update(_sapp_timing_t* t, double external_now) { - double now; - if (external_now == 0.0) { - now = _sapp_timestamp_now(&t->timestamp); - } else { - now = external_now; - } - if (t->last > 0.0) { - double dt = now - t->last; - _sapp_timing_delta(t, dt); - } - t->last = now; - -} - -_SOKOL_PRIVATE double _sapp_timing_get(_sapp_timing_t* t) { - return t->smooth_dt; -} - -// ███████ ████████ ██████ ██ ██ ██████ ████████ ███████ -// ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ███████ ██ ██████ ██ ██ ██ ██ ███████ -// ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ███████ ██ ██ ██ ██████ ██████ ██ ███████ -// -// >> structs -#if defined(SOKOL_WGPU) -typedef struct { - WGPUInstance instance; - WGPUAdapter adapter; - WGPUDevice device; - WGPUSurface surface; - WGPUTextureFormat render_format; - WGPUTexture msaa_tex; - WGPUTextureView msaa_view; - WGPUTexture depth_stencil_tex; - WGPUTextureView depth_stencil_view; - WGPUTextureView swapchain_view; - bool init_done; -} _sapp_wgpu_t; -#endif - -#if defined(SOKOL_VULKAN) -#define _SAPP_VK_MAX_SWAPCHAIN_IMAGES (8) - -typedef struct { - VkImage img; - VkDeviceMemory mem; - VkImageView view; -} _sapp_vk_swapchain_surface_t; - -typedef struct { - VkInstance instance; - VkSurfaceKHR surface; - VkSurfaceFormatKHR surface_format; - VkPhysicalDevice physical_device; - uint32_t queue_family_index; - VkDevice device; - VkQueue queue; - VkSwapchainKHR swapchain; - bool swapchain_acquired; - uint32_t num_swapchain_images; - uint32_t cur_swapchain_image_index; - VkImage swapchain_images[_SAPP_VK_MAX_SWAPCHAIN_IMAGES]; - VkImageView swapchain_views[_SAPP_VK_MAX_SWAPCHAIN_IMAGES]; - _sapp_vk_swapchain_surface_t msaa; - _sapp_vk_swapchain_surface_t depth; - uint32_t sync_slot; - struct { - VkSemaphore render_finished_sem; - VkSemaphore present_complete_sem; - } sync[_SAPP_VK_MAX_SWAPCHAIN_IMAGES]; - struct { - PFN_vkSetDebugUtilsObjectNameEXT set_debug_utils_object_name_ext; - } ext; -} _sapp_vk_t; -#endif - -#if defined(_SAPP_MACOS) -@interface _sapp_macos_app_delegate : NSObject -@end -@interface _sapp_macos_window : NSWindow -@end -@interface _sapp_macos_window_delegate : NSObject -@end -#if defined(SOKOL_METAL) || defined(SOKOL_WGPU) - @interface _sapp_macos_view : NSView - - (void)displayLinkFired:(id)sender; - - (void)fallbackTimerFired:(NSTimer*)timer; - @end -#elif defined(SOKOL_GLCORE) - @interface _sapp_macos_view : NSOpenGLView - - (void)timerFired:(id)sender; - @end -#endif // SOKOL_GLCORE - -typedef struct { - uint32_t flags_changed_store; - uint8_t mouse_buttons; - NSWindow* window; - NSTrackingArea* tracking_area; - id keyup_monitor; - _sapp_macos_app_delegate* app_dlg; - _sapp_macos_window_delegate* win_dlg; - _sapp_macos_view* view; - NSCursor* standard_cursors[_SAPP_MOUSECURSOR_NUM]; - NSCursor* custom_cursors[_SAPP_MOUSECURSOR_NUM]; - #if defined(SOKOL_METAL) - struct { - id device; - CAMetalLayer* layer; - CADisplayLink* display_link; - NSTimer* fallback_timer; - id depth_tex; - id msaa_tex; - // NOTE: CADisplayLink.timestamp seems to be very stable, so we'll use - // this instead of the generic measured+filtered frame timing code - struct { - CFTimeInterval timestamp; - CFTimeInterval frame_duration_sec; - } timing; - } mtl; - #endif - #if defined(SOKOL_WGPU) - struct { - CAMetalLayer* mtl_layer; - CADisplayLink* display_link; - } wgpu; - #endif -} _sapp_macos_t; - -#endif // _SAPP_MACOS - -#if defined(_SAPP_IOS) - -@interface _sapp_scene_delegate : NSObject; -@end -@interface _sapp_textfield_dlg : NSObject -- (void)keyboardWasShown:(NSNotification*)notif; -- (void)keyboardWillBeHidden:(NSNotification*)notif; -- (void)keyboardDidChangeFrame:(NSNotification*)notif; -@end - -// Modified by tettou771 for TrussC: custom view controller for runtime orientation control -@interface _sapp_ios_view_ctrl : UIViewController -@end - -#if defined(SOKOL_METAL) - @interface _sapp_ios_view : UIView - - (void)displayLinkFired:(id)sender; - @end -#else - @interface _sapp_ios_view : GLKView - @end -#endif - -typedef struct { - UIWindow* window; - _sapp_ios_view* view; - UITextField* textfield; - _sapp_textfield_dlg* textfield_dlg; - NSUInteger supported_orientations; // UIInterfaceOrientationMask (default: all) - #if defined(SOKOL_METAL) - UIViewController* view_ctrl; - #else - GLKViewController* view_ctrl; - #endif - #if defined(SOKOL_METAL) - struct { - id device; - CAMetalLayer* layer; - CADisplayLink* display_link; - id depth_tex; - id msaa_tex; - struct { - CFTimeInterval timestamp; - CFTimeInterval frame_duration_sec; - } timing; - } mtl; - #else - EAGLContext* eagl_ctx; - #endif - bool suspended; -} _sapp_ios_t; - -#endif // _SAPP_IOS - -#if defined(_SAPP_EMSCRIPTEN) - -typedef struct { - bool mouse_lock_requested; - uint16_t mouse_buttons; -} _sapp_emsc_t; -#endif // _SAPP_EMSCRIPTEN - -#if defined(SOKOL_D3D11) && defined(_SAPP_WIN32) -typedef struct { - ID3D11Device* device; - ID3D11DeviceContext* device_context; - ID3D11Texture2D* rt; - ID3D11RenderTargetView* rtv; - ID3D11Texture2D* msaa_rt; - ID3D11RenderTargetView* msaa_rtv; - ID3D11Texture2D* ds; - ID3D11DepthStencilView* dsv; - DXGI_SWAP_CHAIN_DESC swap_chain_desc; - IDXGISwapChain* swap_chain; - IDXGIDevice1* dxgi_device; -} _sapp_d3d11_t; -#endif - -#if defined(_SAPP_WIN32) - -#ifndef DPI_ENUMS_DECLARED -typedef enum PROCESS_DPI_AWARENESS -{ - PROCESS_DPI_UNAWARE = 0, - PROCESS_SYSTEM_DPI_AWARE = 1, - PROCESS_PER_MONITOR_DPI_AWARE = 2 -} PROCESS_DPI_AWARENESS; -typedef enum MONITOR_DPI_TYPE { - MDT_EFFECTIVE_DPI = 0, - MDT_ANGULAR_DPI = 1, - MDT_RAW_DPI = 2, - MDT_DEFAULT = MDT_EFFECTIVE_DPI -} MONITOR_DPI_TYPE; -#endif // DPI_ENUMS_DECLARED - -typedef struct { - bool aware; - float content_scale; - float window_scale; - float mouse_scale; -} _sapp_win32_dpi_t; - -typedef struct { - HWND hwnd; - HMONITOR hmonitor; - HDC dc; - HICON big_icon; - HICON small_icon; - HCURSOR standard_cursors[_SAPP_MOUSECURSOR_NUM]; - HCURSOR custom_cursors[_SAPP_MOUSECURSOR_NUM]; - UINT orig_codepage; - WCHAR surrogate; - RECT stored_window_rect; // used to restore window pos/size when toggling fullscreen => windowed - bool is_win10_or_greater; - bool in_create_window; - bool iconified; - _sapp_win32_dpi_t dpi; - struct { - struct { - LONG pos_x, pos_y; - bool pos_valid; - } lock; - struct { - LONG pos_x, pos_y; - bool pos_valid; - } raw_input; - bool requested_lock; - bool tracked; - uint8_t capture_mask; - } mouse; - struct { - size_t size; - void* ptr; - } raw_input_data; -} _sapp_win32_t; - -#if defined(SOKOL_GLCORE) -#define WGL_NUMBER_PIXEL_FORMATS_ARB 0x2000 -#define WGL_SUPPORT_OPENGL_ARB 0x2010 -#define WGL_DRAW_TO_WINDOW_ARB 0x2001 -#define WGL_PIXEL_TYPE_ARB 0x2013 -#define WGL_TYPE_RGBA_ARB 0x202b -#define WGL_ACCELERATION_ARB 0x2003 -#define WGL_NO_ACCELERATION_ARB 0x2025 -#define WGL_RED_BITS_ARB 0x2015 -#define WGL_GREEN_BITS_ARB 0x2017 -#define WGL_BLUE_BITS_ARB 0x2019 -#define WGL_ALPHA_BITS_ARB 0x201b -#define WGL_DEPTH_BITS_ARB 0x2022 -#define WGL_STENCIL_BITS_ARB 0x2023 -#define WGL_DOUBLE_BUFFER_ARB 0x2011 -#define WGL_SAMPLES_ARB 0x2042 -#define WGL_CONTEXT_DEBUG_BIT_ARB 0x00000001 -#define WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB 0x00000002 -#define WGL_CONTEXT_PROFILE_MASK_ARB 0x9126 -#define WGL_CONTEXT_CORE_PROFILE_BIT_ARB 0x00000001 -#define WGL_CONTEXT_MAJOR_VERSION_ARB 0x2091 -#define WGL_CONTEXT_MINOR_VERSION_ARB 0x2092 -#define WGL_CONTEXT_FLAGS_ARB 0x2094 -#define ERROR_INVALID_VERSION_ARB 0x2095 -#define ERROR_INVALID_PROFILE_ARB 0x2096 -#define ERROR_INCOMPATIBLE_DEVICE_CONTEXTS_ARB 0x2054 -typedef BOOL (WINAPI * PFNWGLSWAPINTERVALEXTPROC)(int); -typedef BOOL (WINAPI * PFNWGLGETPIXELFORMATATTRIBIVARBPROC)(HDC,int,int,UINT,const int*,int*); -typedef const char* (WINAPI * PFNWGLGETEXTENSIONSSTRINGEXTPROC)(void); -typedef const char* (WINAPI * PFNWGLGETEXTENSIONSSTRINGARBPROC)(HDC); -typedef HGLRC (WINAPI * PFNWGLCREATECONTEXTATTRIBSARBPROC)(HDC,HGLRC,const int*); -typedef HGLRC (WINAPI * PFN_wglCreateContext)(HDC); -typedef BOOL (WINAPI * PFN_wglDeleteContext)(HGLRC); -typedef PROC (WINAPI * PFN_wglGetProcAddress)(LPCSTR); -typedef HDC (WINAPI * PFN_wglGetCurrentDC)(void); -typedef BOOL (WINAPI * PFN_wglMakeCurrent)(HDC,HGLRC); - -typedef struct { - HINSTANCE opengl32; - HGLRC gl_ctx; - PFN_wglCreateContext CreateContext; - PFN_wglDeleteContext DeleteContext; - PFN_wglGetProcAddress GetProcAddress; - PFN_wglGetCurrentDC GetCurrentDC; - PFN_wglMakeCurrent MakeCurrent; - PFNWGLSWAPINTERVALEXTPROC SwapIntervalEXT; - PFNWGLGETPIXELFORMATATTRIBIVARBPROC GetPixelFormatAttribivARB; - PFNWGLGETEXTENSIONSSTRINGEXTPROC GetExtensionsStringEXT; - PFNWGLGETEXTENSIONSSTRINGARBPROC GetExtensionsStringARB; - PFNWGLCREATECONTEXTATTRIBSARBPROC CreateContextAttribsARB; - // special case glGetIntegerv - void (WINAPI *GetIntegerv)(uint32_t pname, int32_t* data); - bool ext_swap_control; - bool arb_multisample; - bool arb_pixel_format; - bool arb_create_context; - bool arb_create_context_profile; - HWND msg_hwnd; - HDC msg_dc; -} _sapp_wgl_t; -#endif // SOKOL_GLCORE - -#endif // _SAPP_WIN32 - -#if defined(_SAPP_ANDROID) -typedef enum { - _SOKOL_ANDROID_MSG_CREATE, - _SOKOL_ANDROID_MSG_RESUME, - _SOKOL_ANDROID_MSG_PAUSE, - _SOKOL_ANDROID_MSG_FOCUS, - _SOKOL_ANDROID_MSG_NO_FOCUS, - _SOKOL_ANDROID_MSG_SET_NATIVE_WINDOW, - _SOKOL_ANDROID_MSG_SET_INPUT_QUEUE, - _SOKOL_ANDROID_MSG_DESTROY, -} _sapp_android_msg_t; - -typedef struct { - pthread_t thread; - pthread_mutex_t mutex; - pthread_cond_t cond; - int read_from_main_fd; - int write_from_main_fd; -} _sapp_android_pt_t; - -typedef struct { - ANativeWindow* window; - AInputQueue* input; -} _sapp_android_resources_t; - -typedef struct { - ANativeActivity* activity; - _sapp_android_pt_t pt; - _sapp_android_resources_t pending; - _sapp_android_resources_t current; - ALooper* looper; - bool is_thread_started; - bool is_thread_stopping; - bool is_thread_stopped; - bool has_created; - bool has_resumed; - bool has_focus; - EGLConfig config; - EGLDisplay display; - EGLContext context; - EGLSurface surface; - #if __ANDROID_API__ >= 29 - AChoreographer* choreographer; - bool frame_callback_in_flight; - #endif -} _sapp_android_t; - -#endif // _SAPP_ANDROID - -#if defined(_SAPP_LINUX) - -#define _SAPP_X11_XDND_VERSION (5) -#define _SAPP_X11_MAX_X11_KEYCODES (256) - -#define GLX_VENDOR 1 -#define GLX_RGBA_BIT 0x00000001 -#define GLX_WINDOW_BIT 0x00000001 -#define GLX_DRAWABLE_TYPE 0x8010 -#define GLX_RENDER_TYPE 0x8011 -#define GLX_DOUBLEBUFFER 5 -#define GLX_RED_SIZE 8 -#define GLX_GREEN_SIZE 9 -#define GLX_BLUE_SIZE 10 -#define GLX_ALPHA_SIZE 11 -#define GLX_DEPTH_SIZE 12 -#define GLX_STENCIL_SIZE 13 -#define GLX_SAMPLES 0x186a1 -#define GLX_CONTEXT_CORE_PROFILE_BIT_ARB 0x00000001 -#define GLX_CONTEXT_PROFILE_MASK_ARB 0x9126 -#define GLX_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB 0x00000002 -#define GLX_CONTEXT_MAJOR_VERSION_ARB 0x2091 -#define GLX_CONTEXT_MINOR_VERSION_ARB 0x2092 -#define GLX_CONTEXT_FLAGS_ARB 0x2094 - -typedef XID GLXWindow; -typedef XID GLXDrawable; -typedef struct __GLXFBConfig* GLXFBConfig; -typedef struct __GLXcontext* GLXContext; -typedef void (*__GLXextproc)(void); - -typedef int (*PFNGLXGETFBCONFIGATTRIBPROC)(Display*,GLXFBConfig,int,int*); -typedef const char* (*PFNGLXGETCLIENTSTRINGPROC)(Display*,int); -typedef Bool (*PFNGLXQUERYEXTENSIONPROC)(Display*,int*,int*); -typedef Bool (*PFNGLXQUERYVERSIONPROC)(Display*,int*,int*); -typedef void (*PFNGLXDESTROYCONTEXTPROC)(Display*,GLXContext); -typedef Bool (*PFNGLXMAKECURRENTPROC)(Display*,GLXDrawable,GLXContext); -typedef void (*PFNGLXSWAPBUFFERSPROC)(Display*,GLXDrawable); -typedef const char* (*PFNGLXQUERYEXTENSIONSSTRINGPROC)(Display*,int); -typedef GLXFBConfig* (*PFNGLXGETFBCONFIGSPROC)(Display*,int,int*); -typedef __GLXextproc (* PFNGLXGETPROCADDRESSPROC)(const char *procName); -typedef void (*PFNGLXSWAPINTERVALEXTPROC)(Display*,GLXDrawable,int); -typedef XVisualInfo* (*PFNGLXGETVISUALFROMFBCONFIGPROC)(Display*,GLXFBConfig); -typedef GLXWindow (*PFNGLXCREATEWINDOWPROC)(Display*,GLXFBConfig,Window,const int*); -typedef void (*PFNGLXDESTROYWINDOWPROC)(Display*,GLXWindow); - -typedef int (*PFNGLXSWAPINTERVALMESAPROC)(int); -typedef GLXContext (*PFNGLXCREATECONTEXTATTRIBSARBPROC)(Display*,GLXFBConfig,GLXContext,Bool,const int*); - -typedef struct { - bool available; - int major_opcode; - int event_base; - int error_base; - int major; - int minor; -} _sapp_xi_t; - -typedef struct { - int version; - Window source; - Atom format; - Atom XdndAware; - Atom XdndEnter; - Atom XdndPosition; - Atom XdndStatus; - Atom XdndActionCopy; - Atom XdndDrop; - Atom XdndFinished; - Atom XdndSelection; - Atom XdndTypeList; - Atom text_uri_list; -} _sapp_xdnd_t; - -typedef struct { - uint8_t mouse_buttons; - Display* display; - int screen; - Window root; - Colormap colormap; - Window window; - Cursor hidden_cursor; - Cursor standard_cursors[_SAPP_MOUSECURSOR_NUM]; - Cursor custom_cursors[_SAPP_MOUSECURSOR_NUM]; - int window_state; - float dpi; - unsigned char error_code; - Atom UTF8_STRING; - Atom CLIPBOARD; - Atom TARGETS; - Atom WM_PROTOCOLS; - Atom WM_DELETE_WINDOW; - Atom WM_STATE; - Atom NET_WM_NAME; - Atom NET_WM_ICON_NAME; - Atom NET_WM_ICON; - Atom NET_WM_STATE; - Atom NET_WM_STATE_FULLSCREEN; - _sapp_xi_t xi; - _sapp_xdnd_t xdnd; - // XLib manual says keycodes are in the range [8, 255] inclusive. - // https://tronche.com/gui/x/xlib/input/keyboard-encoding.html - bool key_repeat[_SAPP_X11_MAX_X11_KEYCODES]; -} _sapp_x11_t; - -#if defined(_SAPP_GLX) - -typedef struct { - void* libgl; - int major; - int minor; - int event_base; - int error_base; - GLXContext ctx; - GLXWindow window; - - // GLX 1.3 functions - PFNGLXGETFBCONFIGSPROC GetFBConfigs; - PFNGLXGETFBCONFIGATTRIBPROC GetFBConfigAttrib; - PFNGLXGETCLIENTSTRINGPROC GetClientString; - PFNGLXQUERYEXTENSIONPROC QueryExtension; - PFNGLXQUERYVERSIONPROC QueryVersion; - PFNGLXDESTROYCONTEXTPROC DestroyContext; - PFNGLXMAKECURRENTPROC MakeCurrent; - PFNGLXSWAPBUFFERSPROC SwapBuffers; - PFNGLXQUERYEXTENSIONSSTRINGPROC QueryExtensionsString; - PFNGLXGETVISUALFROMFBCONFIGPROC GetVisualFromFBConfig; - PFNGLXCREATEWINDOWPROC CreateWindow; - PFNGLXDESTROYWINDOWPROC DestroyWindow; - - // GLX 1.4 and extension functions - PFNGLXGETPROCADDRESSPROC GetProcAddress; - PFNGLXGETPROCADDRESSPROC GetProcAddressARB; - PFNGLXSWAPINTERVALEXTPROC SwapIntervalEXT; - PFNGLXSWAPINTERVALMESAPROC SwapIntervalMESA; - PFNGLXCREATECONTEXTATTRIBSARBPROC CreateContextAttribsARB; - - // special case glGetIntegerv - void (*GetIntegerv)(uint32_t pname, int32_t* data); - - // extension availability - bool EXT_swap_control; - bool MESA_swap_control; - bool ARB_multisample; - bool ARB_create_context; - bool ARB_create_context_profile; -} _sapp_glx_t; -#endif // _SAPP_GLX - -#if defined(_SAPP_EGL) -typedef struct { - EGLDisplay display; - EGLContext context; - EGLSurface surface; -} _sapp_egl_t; -#endif // _SAPP_EGL -#endif // _SAPP_LINUX - -#if defined(_SAPP_ANY_GL) -typedef struct { - uint32_t framebuffer; -} _sapp_gl_t; -#endif - -typedef struct { - bool enabled; - int buf_size; - char* buffer; -} _sapp_clipboard_t; - -typedef struct { - bool enabled; - int max_files; - int max_path_length; - int num_files; - int buf_size; - char* buffer; -} _sapp_drop_t; - -typedef struct { - float x, y; - float dx, dy; - bool shown; - bool locked; - bool pos_valid; - sapp_mouse_cursor current_cursor; -} _sapp_mouse_t; - -typedef struct { - sapp_desc desc; - bool valid; - bool fullscreen; - bool first_frame; - bool init_called; - bool cleanup_called; - bool quit_requested; - bool quit_ordered; - bool event_consumed; - bool html5_ask_leave_site; - bool onscreen_keyboard_shown; - bool skip_present; // Modified by tettou771 for TrussC: skip next present call (for event-driven rendering) - int window_width; - int window_height; - int framebuffer_width; - int framebuffer_height; - int sample_count; - int swap_interval; - float dpi_scale; - uint64_t frame_count; - sapp_event event; - _sapp_mouse_t mouse; - _sapp_clipboard_t clipboard; - _sapp_drop_t drop; - sapp_icon_desc default_icon_desc; - uint32_t* default_icon_pixels; - _sapp_timing_t timing; - #if defined(SOKOL_WGPU) - _sapp_wgpu_t wgpu; - #endif - #if defined(SOKOL_VULKAN) - _sapp_vk_t vk; - #endif - #if defined(_SAPP_MACOS) - _sapp_macos_t macos; - #elif defined(_SAPP_IOS) - _sapp_ios_t ios; - #elif defined(_SAPP_EMSCRIPTEN) - _sapp_emsc_t emsc; - #elif defined(_SAPP_WIN32) - _sapp_win32_t win32; - #if defined(SOKOL_D3D11) - _sapp_d3d11_t d3d11; - #elif defined(SOKOL_GLCORE) - _sapp_wgl_t wgl; - #endif - #elif defined(_SAPP_ANDROID) - _sapp_android_t android; - #elif defined(_SAPP_LINUX) - _sapp_x11_t x11; - #if defined(_SAPP_GLX) - _sapp_glx_t glx; - #elif defined(_SAPP_EGL) - _sapp_egl_t egl; - #endif - #endif - #if defined(_SAPP_ANY_GL) - _sapp_gl_t gl; - #endif - char html5_canvas_selector[_SAPP_MAX_TITLE_LENGTH]; - char window_title[_SAPP_MAX_TITLE_LENGTH]; // UTF-8 - wchar_t window_title_wide[_SAPP_MAX_TITLE_LENGTH]; // UTF-32 or UCS-2 */ - sapp_keycode keycodes[SAPP_MAX_KEYCODES]; - bool custom_cursor_bound[_SAPP_MOUSECURSOR_NUM]; // true if a custom mouse cursor is bound on that slot -} _sapp_t; -static _sapp_t _sapp; - -// ██ ██████ ██████ ██████ ██ ███ ██ ██████ -// ██ ██ ██ ██ ██ ██ ████ ██ ██ -// ██ ██ ██ ██ ███ ██ ███ ██ ██ ██ ██ ██ ███ -// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ███████ ██████ ██████ ██████ ██ ██ ████ ██████ -// -// >>logging -#if defined(SOKOL_DEBUG) -#define _SAPP_LOGITEM_XMACRO(item,msg) #item ": " msg, -static const char* _sapp_log_messages[] = { - _SAPP_LOG_ITEMS -}; -#undef _SAPP_LOGITEM_XMACRO -#endif // SOKOL_DEBUG - -#define _SAPP_PANIC(code) _sapp_log(SAPP_LOGITEM_ ##code, 0, 0, __LINE__) -#define _SAPP_ERROR(code) _sapp_log(SAPP_LOGITEM_ ##code, 1, 0, __LINE__) -#define _SAPP_WARN(code) _sapp_log(SAPP_LOGITEM_ ##code, 2, 0, __LINE__) -#define _SAPP_INFO(code) _sapp_log(SAPP_LOGITEM_ ##code, 3, 0, __LINE__) -#define _SAPP_PANIC_MSG(code, msg) _sapp_log(SAPP_LOGITEM_ ##code, 0, msg, __LINE__) -#define _SAPP_ERROR_MSG(code, msg) _sapp_log(SAPP_LOGITEM_ ##code, 1, msg, __LINE__) -#define _SAPP_WARN_MSG(code, msg) _sapp_log(SAPP_LOGITEM_ ##code, 2, msg, __LINE__) -#define _SAPP_INFO_MSG(code, msg) _sapp_log(SAPP_LOGITEM_ ##code, 3, msg, __LINE__) - -static void _sapp_log(sapp_log_item log_item, uint32_t log_level, const char* msg, uint32_t line_nr) { - if (_sapp.desc.logger.func) { - const char* filename = 0; - #if defined(SOKOL_DEBUG) - filename = __FILE__; - if (0 == msg) { - msg = _sapp_log_messages[log_item]; - } - #endif - _sapp.desc.logger.func("sapp", log_level, (uint32_t)log_item, msg, line_nr, filename, _sapp.desc.logger.user_data); - } else { - // for log level PANIC it would be 'undefined behaviour' to continue - if (log_level == 0) { - abort(); - } - } -} - -// ███ ███ ███████ ███ ███ ██████ ██████ ██ ██ -// ████ ████ ██ ████ ████ ██ ██ ██ ██ ██ ██ -// ██ ████ ██ █████ ██ ████ ██ ██ ██ ██████ ████ -// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ██ ██ ███████ ██ ██ ██████ ██ ██ ██ -// -// >>memory -_SOKOL_PRIVATE void _sapp_clear(void* ptr, size_t size) { - SOKOL_ASSERT(ptr && (size > 0)); - memset(ptr, 0, size); -} - -_SOKOL_PRIVATE void* _sapp_malloc(size_t size) { - SOKOL_ASSERT(size > 0); - void* ptr; - if (_sapp.desc.allocator.alloc_fn) { - ptr = _sapp.desc.allocator.alloc_fn(size, _sapp.desc.allocator.user_data); - } else { - ptr = malloc(size); - } - if (0 == ptr) { - _SAPP_PANIC(MALLOC_FAILED); - } - return ptr; -} - -_SOKOL_PRIVATE void* _sapp_malloc_clear(size_t size) { - void* ptr = _sapp_malloc(size); - _sapp_clear(ptr, size); - return ptr; -} - -_SOKOL_PRIVATE void _sapp_free(void* ptr) { - if (_sapp.desc.allocator.free_fn) { - _sapp.desc.allocator.free_fn(ptr, _sapp.desc.allocator.user_data); - } else { - free(ptr); - } -} - -// ██ ██ ███████ ██ ██████ ███████ ██████ ███████ -// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ███████ █████ ██ ██████ █████ ██████ ███████ -// ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ██ ██ ███████ ███████ ██ ███████ ██ ██ ███████ -// -// >>helpers - -// round float to int and at least 1 -_SOKOL_PRIVATE int _sapp_roundf_gzero(float f) { - int val = (int)roundf(f); - if (val <= 0) { - val = 1; - } - return val; -} - -_SOKOL_PRIVATE void _sapp_call_init(void) { - if (_sapp.desc.init_cb) { - _sapp.desc.init_cb(); - } else if (_sapp.desc.init_userdata_cb) { - _sapp.desc.init_userdata_cb(_sapp.desc.user_data); - } - _sapp.init_called = true; -} - -_SOKOL_PRIVATE void _sapp_call_frame(void) { - if (_sapp.init_called && !_sapp.cleanup_called) { - if (_sapp.desc.frame_cb) { - _sapp.desc.frame_cb(); - } else if (_sapp.desc.frame_userdata_cb) { - _sapp.desc.frame_userdata_cb(_sapp.desc.user_data); - } - } -} - -_SOKOL_PRIVATE void _sapp_call_cleanup(void) { - if (!_sapp.cleanup_called) { - if (_sapp.desc.cleanup_cb) { - _sapp.desc.cleanup_cb(); - } else if (_sapp.desc.cleanup_userdata_cb) { - _sapp.desc.cleanup_userdata_cb(_sapp.desc.user_data); - } - _sapp.cleanup_called = true; - } -} - -_SOKOL_PRIVATE bool _sapp_call_event(const sapp_event* e) { - if (!_sapp.cleanup_called) { - if (_sapp.desc.event_cb) { - _sapp.desc.event_cb(e); - } else if (_sapp.desc.event_userdata_cb) { - _sapp.desc.event_userdata_cb(e, _sapp.desc.user_data); - } - } - if (_sapp.event_consumed) { - _sapp.event_consumed = false; - return true; - } else { - return false; - } -} - -_SOKOL_PRIVATE char* _sapp_dropped_file_path_ptr(int index) { - SOKOL_ASSERT(_sapp.drop.buffer); - SOKOL_ASSERT((index >= 0) && (index <= _sapp.drop.max_files)); - int offset = index * _sapp.drop.max_path_length; - SOKOL_ASSERT(offset < _sapp.drop.buf_size); - return &_sapp.drop.buffer[offset]; -} - -/* Copy a string (either zero-terminated or with explicit length) - into a fixed size buffer with guaranteed zero-termination. - - Return false if the string didn't fit into the buffer and had to be clamped. - - FIXME: Currently UTF-8 strings might become invalid if the string - is clamped, because the last zero-byte might be written into - the middle of a multi-byte sequence. -*/ -_SOKOL_PRIVATE bool _sapp_strcpy_range(const char* src, size_t src_len, char* dst, size_t dst_buf_len) { - SOKOL_ASSERT(src && dst && (dst_buf_len > 0)); - if (0 == src_len) { - src_len = dst_buf_len; - } - char* const end = &(dst[dst_buf_len-1]); - char c = 0; - for (size_t i = 0; i < dst_buf_len; i++) { - c = *src; - if (i >= src_len) { - c = 0; - } - if (c != 0) { - src++; - } - *dst++ = c; - } - // truncated? - if (c != 0) { - *end = 0; - return false; - } else { - return true; - } -} - -_SOKOL_PRIVATE bool _sapp_strcpy(const char* src, char* dst, size_t dst_buf_len) { - return _sapp_strcpy_range(src, 0, dst, dst_buf_len); -} - -_SOKOL_PRIVATE sapp_desc _sapp_desc_defaults(const sapp_desc* desc) { - SOKOL_ASSERT((desc->allocator.alloc_fn && desc->allocator.free_fn) || (!desc->allocator.alloc_fn && !desc->allocator.free_fn)); - sapp_desc res = *desc; - res.sample_count = _sapp_def(res.sample_count, 1); - res.swap_interval = _sapp_def(res.swap_interval, 1); - if (0 == res.gl.major_version) { - #if defined(SOKOL_GLCORE) - res.gl.major_version = 4; - #if defined(_SAPP_APPLE) - res.gl.minor_version = 1; - #else - res.gl.minor_version = 3; - #endif - #elif defined(SOKOL_GLES3) - res.gl.major_version = 3; - #if defined(_SAPP_ANDROID) || defined(_SAPP_LINUX) - res.gl.minor_version = 1; - #else - res.gl.minor_version = 0; - #endif - #endif - } - res.html5.canvas_selector = _sapp_def(res.html5.canvas_selector, "#canvas"); - res.clipboard_size = _sapp_def(res.clipboard_size, 8192); - res.max_dropped_files = _sapp_def(res.max_dropped_files, 1); - res.max_dropped_file_path_length = _sapp_def(res.max_dropped_file_path_length, 2048); - res.window_title = _sapp_def(res.window_title, "sokol"); - return res; -} - -_SOKOL_PRIVATE void _sapp_init_state(const sapp_desc* desc) { - SOKOL_ASSERT(desc); - SOKOL_ASSERT(desc->width >= 0); - SOKOL_ASSERT(desc->height >= 0); - SOKOL_ASSERT(desc->sample_count >= 0); - SOKOL_ASSERT(desc->swap_interval >= 0); - SOKOL_ASSERT(desc->clipboard_size >= 0); - SOKOL_ASSERT(desc->max_dropped_files >= 0); - SOKOL_ASSERT(desc->max_dropped_file_path_length >= 0); - _SAPP_CLEAR_ARC_STRUCT(_sapp_t, _sapp); - _sapp.desc = _sapp_desc_defaults(desc); - _sapp.first_frame = true; - // NOTE: _sapp.desc.width/height may be 0! Platform backends need to deal with this - _sapp.window_width = _sapp.desc.width; - _sapp.window_height = _sapp.desc.height; - _sapp.framebuffer_width = _sapp.window_width; - _sapp.framebuffer_height = _sapp.window_height; - _sapp.sample_count = _sapp.desc.sample_count; - _sapp.swap_interval = _sapp.desc.swap_interval; - _sapp_strcpy(_sapp.desc.html5.canvas_selector, _sapp.html5_canvas_selector, sizeof(_sapp.html5_canvas_selector)); - _sapp.desc.html5.canvas_selector = _sapp.html5_canvas_selector; - _sapp.html5_ask_leave_site = _sapp.desc.html5.ask_leave_site; - _sapp.clipboard.enabled = _sapp.desc.enable_clipboard; - if (_sapp.clipboard.enabled) { - _sapp.clipboard.buf_size = _sapp.desc.clipboard_size; - _sapp.clipboard.buffer = (char*) _sapp_malloc_clear((size_t)_sapp.clipboard.buf_size); - } - _sapp.drop.enabled = _sapp.desc.enable_dragndrop; - if (_sapp.drop.enabled) { - _sapp.drop.max_files = _sapp.desc.max_dropped_files; - _sapp.drop.max_path_length = _sapp.desc.max_dropped_file_path_length; - _sapp.drop.buf_size = _sapp.drop.max_files * _sapp.drop.max_path_length; - _sapp.drop.buffer = (char*) _sapp_malloc_clear((size_t)_sapp.drop.buf_size); - } - _sapp_strcpy(_sapp.desc.window_title, _sapp.window_title, sizeof(_sapp.window_title)); - _sapp.desc.window_title = _sapp.window_title; - _sapp.dpi_scale = 1.0f; - _sapp.fullscreen = _sapp.desc.fullscreen; - _sapp.mouse.shown = true; - _sapp_timing_init(&_sapp.timing); -} - -_SOKOL_PRIVATE void _sapp_discard_state(void) { - if (_sapp.clipboard.enabled) { - SOKOL_ASSERT(_sapp.clipboard.buffer); - _sapp_free((void*)_sapp.clipboard.buffer); - } - if (_sapp.drop.enabled) { - SOKOL_ASSERT(_sapp.drop.buffer); - _sapp_free((void*)_sapp.drop.buffer); - } - if (_sapp.default_icon_pixels) { - _sapp_free((void*)_sapp.default_icon_pixels); - } - for (int i = 0; i < _SAPP_MOUSECURSOR_NUM; i++) { - sapp_unbind_mouse_cursor_image((sapp_mouse_cursor) i); - } - _SAPP_CLEAR_ARC_STRUCT(_sapp_t, _sapp); -} - -_SOKOL_PRIVATE void _sapp_init_event(sapp_event_type type) { - _sapp_clear(&_sapp.event, sizeof(_sapp.event)); - _sapp.event.type = type; - _sapp.event.frame_count = _sapp.frame_count; - _sapp.event.mouse_button = SAPP_MOUSEBUTTON_INVALID; - _sapp.event.window_width = _sapp.window_width; - _sapp.event.window_height = _sapp.window_height; - _sapp.event.framebuffer_width = _sapp.framebuffer_width; - _sapp.event.framebuffer_height = _sapp.framebuffer_height; - _sapp.event.mouse_x = _sapp.mouse.x; - _sapp.event.mouse_y = _sapp.mouse.y; - _sapp.event.mouse_dx = _sapp.mouse.dx; - _sapp.event.mouse_dy = _sapp.mouse.dy; -} - -_SOKOL_PRIVATE bool _sapp_events_enabled(void) { - /* only send events when an event callback is set, and the init function was called */ - return (_sapp.desc.event_cb || _sapp.desc.event_userdata_cb) && _sapp.init_called; -} - -_SOKOL_PRIVATE sapp_keycode _sapp_translate_key(int scan_code) { - if ((scan_code >= 0) && (scan_code < SAPP_MAX_KEYCODES)) { - return _sapp.keycodes[scan_code]; - } else { - return SAPP_KEYCODE_INVALID; - } -} - -_SOKOL_PRIVATE void _sapp_clear_drop_buffer(void) { - if (_sapp.drop.enabled) { - SOKOL_ASSERT(_sapp.drop.buffer); - _sapp_clear(_sapp.drop.buffer, (size_t)_sapp.drop.buf_size); - } -} - -_SOKOL_PRIVATE void _sapp_frame(void) { - if (_sapp.first_frame) { - _sapp.first_frame = false; - _sapp_call_init(); - } - _sapp_call_frame(); - _sapp.frame_count++; -} - -_SOKOL_PRIVATE bool _sapp_image_validate(const sapp_image_desc* desc) { - SOKOL_ASSERT(desc->width > 0); - SOKOL_ASSERT(desc->height > 0); - SOKOL_ASSERT(desc->pixels.ptr != 0); - SOKOL_ASSERT(desc->pixels.size > 0); - const size_t wh_size = (size_t)(desc->width * desc->height) * sizeof(uint32_t); - if (wh_size != desc->pixels.size) { - _SAPP_ERROR(IMAGE_DATA_SIZE_MISMATCH); - return false; - } - return true; -} - -_SOKOL_PRIVATE int _sapp_image_bestmatch(const sapp_image_desc image_descs[], int num_images, int width, int height) { - int least_diff = 0x7FFFFFFF; - int least_index = 0; - for (int i = 0; i < num_images; i++) { - int diff = (image_descs[i].width * image_descs[i].height) - (width * height); - if (diff < 0) { - diff = -diff; - } - if (diff < least_diff) { - least_diff = diff; - least_index = i; - } - } - return least_index; -} - -_SOKOL_PRIVATE int _sapp_icon_num_images(const sapp_icon_desc* desc) { - int index = 0; - for (; index < SAPP_MAX_ICONIMAGES; index++) { - if (0 == desc->images[index].pixels.ptr) { - break; - } - } - return index; -} - -_SOKOL_PRIVATE bool _sapp_validate_icon_desc(const sapp_icon_desc* desc, int num_images) { - SOKOL_ASSERT(num_images <= SAPP_MAX_ICONIMAGES); - for (int i = 0; i < num_images; i++) { - const sapp_image_desc* img_desc = &desc->images[i]; - if (!_sapp_image_validate(img_desc)) { - return false; - } - } - return true; -} - -_SOKOL_PRIVATE void _sapp_setup_default_icon(void) { - SOKOL_ASSERT(0 == _sapp.default_icon_pixels); - - const int num_icons = 3; - const int icon_sizes[3] = { 16, 32, 64 }; // must be multiple of 8! - - // allocate a pixel buffer for all icon pixels - int all_num_pixels = 0; - for (int i = 0; i < num_icons; i++) { - all_num_pixels += icon_sizes[i] * icon_sizes[i]; - } - _sapp.default_icon_pixels = (uint32_t*) _sapp_malloc_clear((size_t)all_num_pixels * sizeof(uint32_t)); - - // initialize default_icon_desc struct - uint32_t* dst = _sapp.default_icon_pixels; - const uint32_t* dst_end = dst + all_num_pixels; - (void)dst_end; // silence unused warning in release mode - for (int i = 0; i < num_icons; i++) { - const int dim = (int) icon_sizes[i]; - const int num_pixels = dim * dim; - sapp_image_desc* img_desc = &_sapp.default_icon_desc.images[i]; - img_desc->width = dim; - img_desc->height = dim; - img_desc->pixels.ptr = dst; - img_desc->pixels.size = (size_t)num_pixels * sizeof(uint32_t); - dst += num_pixels; - } - SOKOL_ASSERT(dst == dst_end); - - // Amstrad CPC font 'S' - const uint8_t tile[8] = { - 0x3C, - 0x66, - 0x60, - 0x3C, - 0x06, - 0x66, - 0x3C, - 0x00, - }; - // rainbow colors - const uint32_t colors[8] = { - 0xFF4370FF, - 0xFF26A7FF, - 0xFF58EEFF, - 0xFF57E1D4, - 0xFF65CC9C, - 0xFF6ABB66, - 0xFFF5A542, - 0xFFC2577E, - }; - dst = _sapp.default_icon_pixels; - const uint32_t blank = 0x00FFFFFF; - const uint32_t shadow = 0xFF000000; - for (int i = 0; i < num_icons; i++) { - const int dim = icon_sizes[i]; - SOKOL_ASSERT((dim % 8) == 0); - const int scale = dim / 8; - for (int ty = 0, y = 0; ty < 8; ty++) { - const uint32_t color = colors[ty]; - for (int sy = 0; sy < scale; sy++, y++) { - uint8_t bits = tile[ty]; - for (int tx = 0, x = 0; tx < 8; tx++, bits<<=1) { - uint32_t pixel = (0 == (bits & 0x80)) ? blank : color; - for (int sx = 0; sx < scale; sx++, x++) { - SOKOL_ASSERT(dst < dst_end); - *dst++ = pixel; - } - } - } - } - } - SOKOL_ASSERT(dst == dst_end); - - // right shadow - dst = _sapp.default_icon_pixels; - for (int i = 0; i < num_icons; i++) { - const int dim = icon_sizes[i]; - for (int y = 0; y < dim; y++) { - uint32_t prev_color = blank; - for (int x = 0; x < dim; x++) { - const int dst_index = y * dim + x; - const uint32_t cur_color = dst[dst_index]; - if ((cur_color == blank) && (prev_color != blank)) { - dst[dst_index] = shadow; - } - prev_color = cur_color; - } - } - dst += dim * dim; - } - SOKOL_ASSERT(dst == dst_end); - - // bottom shadow - dst = _sapp.default_icon_pixels; - for (int i = 0; i < num_icons; i++) { - const int dim = icon_sizes[i]; - for (int x = 0; x < dim; x++) { - uint32_t prev_color = blank; - for (int y = 0; y < dim; y++) { - const int dst_index = y * dim + x; - const uint32_t cur_color = dst[dst_index]; - if ((cur_color == blank) && (prev_color != blank)) { - dst[dst_index] = shadow; - } - prev_color = cur_color; - } - } - dst += dim * dim; - } - SOKOL_ASSERT(dst == dst_end); -} - -// ██ ██ ██████ ██████ ██ ██ -// ██ ██ ██ ██ ██ ██ ██ -// ██ █ ██ ██ ███ ██████ ██ ██ -// ██ ███ ██ ██ ██ ██ ██ ██ -// ███ ███ ██████ ██ ██████ -// -// >>wgpu -#if defined(SOKOL_WGPU) - -_SOKOL_PRIVATE WGPUStringView _sapp_wgpu_stringview(const char* str) { - WGPUStringView res; - if (str) { - res.data = str; - res.length = strlen(str); - } else { - res.data = 0; - res.length = 0; - } - return res; -} - -_SOKOL_PRIVATE WGPUCallbackMode _sapp_wgpu_callbackmode(void) { - #if defined(_SAPP_WGPU_HAS_WAIT) - return WGPUCallbackMode_WaitAnyOnly; - #else - return WGPUCallbackMode_AllowProcessEvents; - #endif -} - -_SOKOL_PRIVATE void _sapp_wgpu_await(WGPUFuture future) { - #if defined(_SAPP_WGPU_HAS_WAIT) - SOKOL_ASSERT(_sapp.wgpu.instance); - _SAPP_STRUCT(WGPUFutureWaitInfo, wait_info); - wait_info.future = future; - WGPUWaitStatus res = wgpuInstanceWaitAny(_sapp.wgpu.instance, 1, &wait_info, UINT64_MAX); - SOKOL_ASSERT(res == WGPUWaitStatus_Success); _SOKOL_UNUSED(res); - #else - // this code path should never be called - _SOKOL_UNUSED(future); - SOKOL_ASSERT(false); - #endif -} - -_SOKOL_PRIVATE WGPUTextureFormat _sapp_wgpu_pick_render_format(size_t count, const WGPUTextureFormat* formats) { - // NOTE: only accept non-SRGB formats until sokol_app.h gets proper SRGB support - SOKOL_ASSERT((count > 0) && formats); - for (size_t i = 0; i < count; i++) { - const WGPUTextureFormat fmt = formats[i]; - switch (fmt) { - case WGPUTextureFormat_RGBA8Unorm: - case WGPUTextureFormat_BGRA8Unorm: - return fmt; - default: break; - } - } - // FIXME: fallback might still return an SRGB format - return formats[0]; -} - -_SOKOL_PRIVATE void _sapp_wgpu_create_swapchain(bool called_from_resize) { - SOKOL_ASSERT(_sapp.wgpu.instance); - SOKOL_ASSERT(_sapp.wgpu.device); - SOKOL_ASSERT(0 == _sapp.wgpu.msaa_tex); - SOKOL_ASSERT(0 == _sapp.wgpu.msaa_view); - SOKOL_ASSERT(0 == _sapp.wgpu.depth_stencil_tex); - SOKOL_ASSERT(0 == _sapp.wgpu.depth_stencil_view); - - if (!called_from_resize) { - SOKOL_ASSERT(0 == _sapp.wgpu.surface); - _SAPP_STRUCT(WGPUSurfaceDescriptor, surf_desc); - #if defined (_SAPP_EMSCRIPTEN) - _SAPP_STRUCT(WGPUEmscriptenSurfaceSourceCanvasHTMLSelector, html_canvas_desc); - html_canvas_desc.chain.sType = WGPUSType_EmscriptenSurfaceSourceCanvasHTMLSelector; - html_canvas_desc.selector = _sapp_wgpu_stringview(_sapp.html5_canvas_selector); - surf_desc.nextInChain = &html_canvas_desc.chain; - #elif defined(_SAPP_MACOS) - _SAPP_STRUCT(WGPUSurfaceSourceMetalLayer, from_metal_layer); - from_metal_layer.chain.sType = WGPUSType_SurfaceSourceMetalLayer; - from_metal_layer.layer = _sapp.macos.view.layer; - surf_desc.nextInChain = &from_metal_layer.chain; - #elif defined(_SAPP_WIN32) - _SAPP_STRUCT(WGPUSurfaceSourceWindowsHWND, from_hwnd); - from_hwnd.chain.sType = WGPUSType_SurfaceSourceWindowsHWND; - from_hwnd.hinstance = GetModuleHandleW(NULL); - from_hwnd.hwnd = _sapp.win32.hwnd; - surf_desc.nextInChain = &from_hwnd.chain; - #elif defined(_SAPP_LINUX) - _SAPP_STRUCT(WGPUSurfaceSourceXlibWindow, from_xlib); - from_xlib.chain.sType = WGPUSType_SurfaceSourceXlibWindow; - from_xlib.display = _sapp.x11.display; - from_xlib.window = _sapp.x11.window; - surf_desc.nextInChain = &from_xlib.chain; - #else - #error "sokol_app.h: unsupported WebGPU platform" - #endif - _sapp.wgpu.surface = wgpuInstanceCreateSurface(_sapp.wgpu.instance, &surf_desc); - if (0 == _sapp.wgpu.surface) { - _SAPP_PANIC(WGPU_SWAPCHAIN_CREATE_SURFACE_FAILED); - } - _SAPP_STRUCT(WGPUSurfaceCapabilities, surf_caps); - WGPUStatus caps_status = wgpuSurfaceGetCapabilities(_sapp.wgpu.surface, _sapp.wgpu.adapter, &surf_caps); - if (caps_status != WGPUStatus_Success) { - _SAPP_PANIC(WGPU_SWAPCHAIN_SURFACE_GET_CAPABILITIES_FAILED); - } - _sapp.wgpu.render_format = _sapp_wgpu_pick_render_format(surf_caps.formatCount, surf_caps.formats); - } - - SOKOL_ASSERT(_sapp.wgpu.surface); - _SAPP_STRUCT(WGPUSurfaceConfiguration, surf_conf); - surf_conf.device = _sapp.wgpu.device; - surf_conf.format = _sapp.wgpu.render_format; - surf_conf.usage = WGPUTextureUsage_RenderAttachment; - surf_conf.width = (uint32_t)_sapp.framebuffer_width; - surf_conf.height = (uint32_t)_sapp.framebuffer_height; - surf_conf.alphaMode = WGPUCompositeAlphaMode_Opaque; - #if defined(_SAPP_EMSCRIPTEN) - // FIXME: make this further configurable? - if (_sapp.desc.html5.premultiplied_alpha) { - surf_conf.alphaMode = WGPUCompositeAlphaMode_Premultiplied; - } - #endif - surf_conf.presentMode = WGPUPresentMode_Fifo; - wgpuSurfaceConfigure(_sapp.wgpu.surface, &surf_conf); - - _SAPP_STRUCT(WGPUTextureDescriptor, ds_desc); - ds_desc.usage = WGPUTextureUsage_RenderAttachment; - ds_desc.dimension = WGPUTextureDimension_2D; - ds_desc.size.width = (uint32_t)_sapp.framebuffer_width; - ds_desc.size.height = (uint32_t)_sapp.framebuffer_height; - ds_desc.size.depthOrArrayLayers = 1; - ds_desc.format = WGPUTextureFormat_Depth32FloatStencil8; - ds_desc.mipLevelCount = 1; - ds_desc.sampleCount = (uint32_t)_sapp.sample_count; - _sapp.wgpu.depth_stencil_tex = wgpuDeviceCreateTexture(_sapp.wgpu.device, &ds_desc); - if (0 == _sapp.wgpu.depth_stencil_tex) { - _SAPP_PANIC(WGPU_SWAPCHAIN_CREATE_DEPTH_STENCIL_TEXTURE_FAILED); - } - _sapp.wgpu.depth_stencil_view = wgpuTextureCreateView(_sapp.wgpu.depth_stencil_tex, 0); - if (0 == _sapp.wgpu.depth_stencil_view) { - _SAPP_PANIC(WGPU_SWAPCHAIN_CREATE_DEPTH_STENCIL_VIEW_FAILED); - } - - if (_sapp.sample_count > 1) { - _SAPP_STRUCT(WGPUTextureDescriptor, msaa_desc); - msaa_desc.usage = WGPUTextureUsage_RenderAttachment; - msaa_desc.dimension = WGPUTextureDimension_2D; - msaa_desc.size.width = (uint32_t)_sapp.framebuffer_width; - msaa_desc.size.height = (uint32_t)_sapp.framebuffer_height; - msaa_desc.size.depthOrArrayLayers = 1; - msaa_desc.format = _sapp.wgpu.render_format; - msaa_desc.mipLevelCount = 1; - msaa_desc.sampleCount = (uint32_t)_sapp.sample_count; - _sapp.wgpu.msaa_tex = wgpuDeviceCreateTexture(_sapp.wgpu.device, &msaa_desc); - if (0 == _sapp.wgpu.msaa_tex) { - _SAPP_PANIC(WGPU_SWAPCHAIN_CREATE_MSAA_TEXTURE_FAILED); - } - _sapp.wgpu.msaa_view = wgpuTextureCreateView(_sapp.wgpu.msaa_tex, 0); - if (0 == _sapp.wgpu.msaa_view) { - _SAPP_PANIC(WGPU_SWAPCHAIN_CREATE_MSAA_VIEW_FAILED); - } - } -} - -_SOKOL_PRIVATE void _sapp_wgpu_discard_swapchain(bool called_from_resize) { - if (_sapp.wgpu.msaa_view) { - wgpuTextureViewRelease(_sapp.wgpu.msaa_view); - _sapp.wgpu.msaa_view = 0; - } - if (_sapp.wgpu.msaa_tex) { - wgpuTextureRelease(_sapp.wgpu.msaa_tex); - _sapp.wgpu.msaa_tex = 0; - } - if (_sapp.wgpu.depth_stencil_view) { - wgpuTextureViewRelease(_sapp.wgpu.depth_stencil_view); - _sapp.wgpu.depth_stencil_view = 0; - } - if (_sapp.wgpu.depth_stencil_tex) { - wgpuTextureRelease(_sapp.wgpu.depth_stencil_tex); - _sapp.wgpu.depth_stencil_tex = 0; - } - if (!called_from_resize) { - if (_sapp.wgpu.surface) { - wgpuSurfaceRelease(_sapp.wgpu.surface); - _sapp.wgpu.surface = 0; - } - } -} - -_SOKOL_PRIVATE void _sapp_wgpu_swapchain_next(void) { - SOKOL_ASSERT(0 == _sapp.wgpu.swapchain_view); - _SAPP_STRUCT(WGPUSurfaceTexture, surf_tex); - wgpuSurfaceGetCurrentTexture(_sapp.wgpu.surface, &surf_tex); - switch (surf_tex.status) { - case WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal: - case WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal: - // all ok - break; - case WGPUSurfaceGetCurrentTextureStatus_Timeout: - case WGPUSurfaceGetCurrentTextureStatus_Outdated: - case WGPUSurfaceGetCurrentTextureStatus_Lost: - if (surf_tex.texture) { - wgpuTextureRelease(surf_tex.texture); - } - _sapp_wgpu_discard_swapchain(false); - _sapp_wgpu_create_swapchain(false); - // FIXME: currently this will assert in the caller - return; - case WGPUSurfaceGetCurrentTextureStatus_Error: - default: - _SAPP_PANIC(WGPU_SWAPCHAIN_GETCURRENTTEXTURE_FAILED); - break; - } - _sapp.wgpu.swapchain_view = wgpuTextureCreateView(surf_tex.texture, 0); - SOKOL_ASSERT(_sapp.wgpu.swapchain_view); -} - -_SOKOL_PRIVATE void _sapp_wgpu_swapchain_size_changed(void) { - if (_sapp.wgpu.surface) { - _sapp_wgpu_discard_swapchain(true); - _sapp_wgpu_create_swapchain(true); - } -} - -_SOKOL_PRIVATE void _sapp_wgpu_device_lost_cb(const WGPUDevice* dev, WGPUDeviceLostReason reason, WGPUStringView msg, void* ud1, void* ud2) { - _SOKOL_UNUSED(dev); _SOKOL_UNUSED(reason); _SOKOL_UNUSED(ud1); _SOKOL_UNUSED(ud2); - // NOTE: on wgpuInstanceRelease(), the device lost callback is always called with - // WGPUDeviceLostReason_CallbackCancelled (even though no device should exist at that point) - if (reason != WGPUDeviceLostReason_CallbackCancelled) { - SOKOL_ASSERT(msg.data && (msg.length > 0)); - char buf[1024]; - _sapp_strcpy_range(msg.data, msg.length, buf, sizeof(buf)); - _SAPP_ERROR_MSG(WGPU_DEVICE_LOST, buf); - } -} - -// NOTE: emdawnwebgpu doesn't seem to have a device logging callback -#if !defined(_SAPP_EMSCRIPTEN) -_SOKOL_PRIVATE void _sapp_wgpu_device_logging_cb(WGPULoggingType log_type, WGPUStringView msg, void* ud1, void* ud2) { - _SOKOL_UNUSED(log_type); _SOKOL_UNUSED(ud1); _SOKOL_UNUSED(ud2); - SOKOL_ASSERT(msg.data && (msg.length > 0)); - char buf[1024]; - _sapp_strcpy_range(msg.data, msg.length, buf, sizeof(buf)); - switch (log_type) { - case WGPULoggingType_Warning: - _SAPP_WARN_MSG(WGPU_DEVICE_LOG, buf); - break; - case WGPULoggingType_Error: - _SAPP_ERROR_MSG(WGPU_DEVICE_LOG, buf); - break; - default: - _SAPP_INFO_MSG(WGPU_DEVICE_LOG, buf); - break; - } -} -#endif - -_SOKOL_PRIVATE void _sapp_wgpu_uncaptured_error_cb(const WGPUDevice* dev, WGPUErrorType err_type, WGPUStringView msg, void* ud1, void* ud2) { - _SOKOL_UNUSED(dev); _SOKOL_UNUSED(ud1); _SOKOL_UNUSED(ud2); - if (err_type != WGPUErrorType_NoError) { - SOKOL_ASSERT(msg.data && (msg.length > 0)); - char buf[1024]; - _sapp_strcpy_range(msg.data, msg.length, buf, sizeof(buf)); - _SAPP_ERROR_MSG(WGPU_DEVICE_UNCAPTURED_ERROR, buf); - } -} - -_SOKOL_PRIVATE void _sapp_wgpu_request_device_cb(WGPURequestDeviceStatus status, WGPUDevice device, WGPUStringView msg, void* userdata1, void* userdata2) { - _SOKOL_UNUSED(msg); - _SOKOL_UNUSED(userdata1); - _SOKOL_UNUSED(userdata2); - SOKOL_ASSERT(!_sapp.wgpu.init_done); - if (status != WGPURequestDeviceStatus_Success) { - if (status == WGPURequestDeviceStatus_Error) { - _SAPP_PANIC(WGPU_REQUEST_DEVICE_STATUS_ERROR); - } else { - _SAPP_PANIC(WGPU_REQUEST_DEVICE_STATUS_UNKNOWN); - } - } - SOKOL_ASSERT(device); - _sapp.wgpu.device = device; - #if !defined(_SAPP_EMSCRIPTEN) - _SAPP_STRUCT(WGPULoggingCallbackInfo, cb_info); - cb_info.callback = _sapp_wgpu_device_logging_cb; - wgpuDeviceSetLoggingCallback(_sapp.wgpu.device, cb_info); - #endif - _sapp_wgpu_create_swapchain(false); - _sapp.wgpu.init_done = true; -} - -_SOKOL_PRIVATE void _sapp_wgpu_create_device_and_swapchain(void) { - SOKOL_ASSERT(_sapp.wgpu.adapter); - size_t cur_feature_index = 1; - #define _SAPP_WGPU_MAX_REQUESTED_FEATURES (16) - WGPUFeatureName requiredFeatures[_SAPP_WGPU_MAX_REQUESTED_FEATURES] = { - WGPUFeatureName_Depth32FloatStencil8, - }; - // check for optional features we're interested in - if (wgpuAdapterHasFeature(_sapp.wgpu.adapter, WGPUFeatureName_TextureCompressionBC)) { - SOKOL_ASSERT(cur_feature_index < _SAPP_WGPU_MAX_REQUESTED_FEATURES); - requiredFeatures[cur_feature_index++] = WGPUFeatureName_TextureCompressionBC; - } - if (wgpuAdapterHasFeature(_sapp.wgpu.adapter, WGPUFeatureName_TextureCompressionETC2)) { - SOKOL_ASSERT(cur_feature_index < _SAPP_WGPU_MAX_REQUESTED_FEATURES); - requiredFeatures[cur_feature_index++] = WGPUFeatureName_TextureCompressionETC2; - } - if (wgpuAdapterHasFeature(_sapp.wgpu.adapter, WGPUFeatureName_TextureCompressionASTC)) { - SOKOL_ASSERT(cur_feature_index < _SAPP_WGPU_MAX_REQUESTED_FEATURES); - requiredFeatures[cur_feature_index++] = WGPUFeatureName_TextureCompressionASTC; - } - if (wgpuAdapterHasFeature(_sapp.wgpu.adapter, WGPUFeatureName_DualSourceBlending)) { - SOKOL_ASSERT(cur_feature_index < _SAPP_WGPU_MAX_REQUESTED_FEATURES); - requiredFeatures[cur_feature_index++] = WGPUFeatureName_DualSourceBlending; - } - if (wgpuAdapterHasFeature(_sapp.wgpu.adapter, WGPUFeatureName_ShaderF16)) { - SOKOL_ASSERT(cur_feature_index < _SAPP_WGPU_MAX_REQUESTED_FEATURES); - requiredFeatures[cur_feature_index++] = WGPUFeatureName_ShaderF16; - } - if (wgpuAdapterHasFeature(_sapp.wgpu.adapter, WGPUFeatureName_Float32Filterable)) { - SOKOL_ASSERT(cur_feature_index < _SAPP_WGPU_MAX_REQUESTED_FEATURES); - requiredFeatures[cur_feature_index++] = WGPUFeatureName_Float32Filterable; - } - if (wgpuAdapterHasFeature(_sapp.wgpu.adapter, WGPUFeatureName_Float32Blendable)) { - SOKOL_ASSERT(cur_feature_index < _SAPP_WGPU_MAX_REQUESTED_FEATURES); - requiredFeatures[cur_feature_index++] = WGPUFeatureName_Float32Blendable; - } - if (wgpuAdapterHasFeature(_sapp.wgpu.adapter, WGPUFeatureName_TextureFormatsTier2)) { - SOKOL_ASSERT(cur_feature_index < _SAPP_WGPU_MAX_REQUESTED_FEATURES); - requiredFeatures[cur_feature_index++] = WGPUFeatureName_TextureFormatsTier2; - } - #undef _SAPP_WGPU_MAX_REQUESTED_FEATURES - - WGPULimits adapterLimits = WGPU_LIMITS_INIT; - wgpuAdapterGetLimits(_sapp.wgpu.adapter, &adapterLimits); - - WGPULimits requiredLimits = WGPU_LIMITS_INIT; - requiredLimits.maxColorAttachments = adapterLimits.maxColorAttachments; - requiredLimits.maxSampledTexturesPerShaderStage = adapterLimits.maxSampledTexturesPerShaderStage; - requiredLimits.maxStorageBuffersPerShaderStage = adapterLimits.maxStorageBuffersPerShaderStage; - requiredLimits.maxStorageTexturesPerShaderStage = adapterLimits.maxStorageTexturesPerShaderStage; - - _SAPP_STRUCT(WGPURequestDeviceCallbackInfo, cb_info); - cb_info.mode = _sapp_wgpu_callbackmode(); - cb_info.callback = _sapp_wgpu_request_device_cb; - - _SAPP_STRUCT(WGPUDeviceDescriptor, dev_desc); - dev_desc.requiredFeatureCount = cur_feature_index; - dev_desc.requiredFeatures = requiredFeatures; - dev_desc.requiredLimits = &requiredLimits; - dev_desc.deviceLostCallbackInfo.mode = WGPUCallbackMode_AllowProcessEvents; - dev_desc.deviceLostCallbackInfo.callback = _sapp_wgpu_device_lost_cb; - dev_desc.uncapturedErrorCallbackInfo.callback = _sapp_wgpu_uncaptured_error_cb; - WGPUFuture future = wgpuAdapterRequestDevice(_sapp.wgpu.adapter, &dev_desc, cb_info); - #if defined(_SAPP_WGPU_HAS_WAIT) - _sapp_wgpu_await(future); - #else - _SOKOL_UNUSED(future); - #endif -} - -_SOKOL_PRIVATE void _sapp_wgpu_request_adapter_cb(WGPURequestAdapterStatus status, WGPUAdapter adapter, WGPUStringView msg, void* userdata1, void* userdata2) { - _SOKOL_UNUSED(msg); - _SOKOL_UNUSED(userdata1); - _SOKOL_UNUSED(userdata2); - if (status != WGPURequestAdapterStatus_Success) { - switch (status) { - case WGPURequestAdapterStatus_Unavailable: _SAPP_PANIC(WGPU_REQUEST_ADAPTER_STATUS_UNAVAILABLE); break; - case WGPURequestAdapterStatus_Error: _SAPP_PANIC(WGPU_REQUEST_ADAPTER_STATUS_ERROR); break; - default: _SAPP_PANIC(WGPU_REQUEST_ADAPTER_STATUS_UNKNOWN); break; - } - } - SOKOL_ASSERT(adapter); - _sapp.wgpu.adapter = adapter; - #if !defined(_SAPP_WGPU_HAS_WAIT) - // chain device creation - _sapp_wgpu_create_device_and_swapchain(); - #endif -} - -_SOKOL_PRIVATE void _sapp_wgpu_create_adapter(void) { - SOKOL_ASSERT(_sapp.wgpu.instance); - // FIXME: power preference? - _SAPP_STRUCT(WGPURequestAdapterCallbackInfo, cb_info); - cb_info.mode = _sapp_wgpu_callbackmode(); - cb_info.callback = _sapp_wgpu_request_adapter_cb; - WGPUFuture future = wgpuInstanceRequestAdapter(_sapp.wgpu.instance, 0, cb_info); - #if defined(_SAPP_WGPU_HAS_WAIT) - _sapp_wgpu_await(future); - #else - _SOKOL_UNUSED(future); - #endif -} - -_SOKOL_PRIVATE void _sapp_wgpu_init(void) { - SOKOL_ASSERT(0 == _sapp.wgpu.instance); - SOKOL_ASSERT(!_sapp.wgpu.init_done); - - _SAPP_STRUCT(WGPUInstanceDescriptor, desc); - #if defined(_SAPP_WGPU_HAS_WAIT) - WGPUInstanceFeatureName inst_features[1] = { - WGPUInstanceFeatureName_TimedWaitAny, - }; - desc.requiredFeatureCount = 1; - desc.requiredFeatures = inst_features; - #endif - _sapp.wgpu.instance = wgpuCreateInstance(&desc); - if (0 == _sapp.wgpu.instance) { - _SAPP_PANIC(WGPU_CREATE_INSTANCE_FAILED); - } - // NOTE: on Emscripten, device and swapchain creation are chained in the callacks - _sapp_wgpu_create_adapter(); - #if defined(_SAPP_WGPU_HAS_WAIT) - _sapp_wgpu_create_device_and_swapchain(); - SOKOL_ASSERT(_sapp.wgpu.init_done); - #endif -} - -_SOKOL_PRIVATE void _sapp_wgpu_discard(void) { - _sapp_wgpu_discard_swapchain(false); - if (_sapp.wgpu.device) { - wgpuDeviceRelease(_sapp.wgpu.device); - _sapp.wgpu.device = 0; - } - if (_sapp.wgpu.adapter) { - wgpuAdapterRelease(_sapp.wgpu.adapter); - _sapp.wgpu.adapter = 0; - } - if (_sapp.wgpu.instance) { - wgpuInstanceRelease(_sapp.wgpu.instance); - _sapp.wgpu.instance = 0; - } -} - -_SOKOL_PRIVATE void _sapp_wgpu_frame(void) { - wgpuInstanceProcessEvents(_sapp.wgpu.instance); - if (_sapp.wgpu.init_done) { - _sapp_frame(); - if (_sapp.wgpu.swapchain_view) { - wgpuTextureViewRelease(_sapp.wgpu.swapchain_view); - _sapp.wgpu.swapchain_view = 0; - } - #if !defined(_SAPP_EMSCRIPTEN) - // Modified by tettou771 for TrussC: skip present support - if (!_sapp.skip_present) { - wgpuSurfacePresent(_sapp.wgpu.surface); - } else { - _sapp.skip_present = false; - } - #endif - } -} -#endif // SOKOL_WGPU - -// ██ ██ ██ ██ ██ ██ ██ █████ ███ ██ -// ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ -// ██ ██ ██ ██ ██ █████ ███████ ██ ██ ██ -// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ████ ██████ ███████ ██ ██ ██ ██ ██ ████ -// -// >>vulkan -// >>vk -#if defined(SOKOL_VULKAN) - -#if defined(__cplusplus) -#define _SAPP_VK_ZERO_COUNT_AND_ARRAY(num, type, count_name, array_name) uint32_t count_name = 0; type array_name[num] = {} -#define _SAPP_VK_MAX_COUNT_AND_ARRAY(num, type, count_name, array_name) uint32_t count_name = num; type array_name[num] = {} -#else -#define _SAPP_VK_ZERO_COUNT_AND_ARRAY(num, type, count_name, array_name) uint32_t count_name = 0; type array_name[num] = {0} -#define _SAPP_VK_MAX_COUNT_AND_ARRAY(num, type, count_name, array_name) uint32_t count_name = num; type array_name[num] = {0} -#endif - -_SOKOL_PRIVATE void _sapp_vk_load_instance_ext_funcs(void) { - SOKOL_ASSERT(_sapp.vk.instance); - #if defined(SOKOL_DEBUG) - _sapp.vk.ext.set_debug_utils_object_name_ext = (PFN_vkSetDebugUtilsObjectNameEXT)vkGetInstanceProcAddr(_sapp.vk.instance, "vkSetDebugUtilsObjectNameEXT"); - if (0 == _sapp.vk.ext.set_debug_utils_object_name_ext) { - _SAPP_PANIC(VULKAN_REQUIRED_INSTANCE_EXTENSION_FUNCTION_MISSING); - } - #endif -} - -_SOKOL_PRIVATE void _sapp_vk_set_object_label(VkObjectType obj_type, uint64_t obj_handle, const char* label) { - #if defined(SOKOL_DEBUG) - SOKOL_ASSERT(_sapp.vk.device); - SOKOL_ASSERT(_sapp.vk.ext.set_debug_utils_object_name_ext); - SOKOL_ASSERT(obj_handle); - if (label) { - _SAPP_STRUCT(VkDebugUtilsObjectNameInfoEXT, name_info); - name_info.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT; - name_info.objectType = obj_type; - name_info.objectHandle = obj_handle, - name_info.pObjectName = label; - VkResult res = _sapp.vk.ext.set_debug_utils_object_name_ext(_sapp.vk.device, &name_info); - SOKOL_ASSERT(res == VK_SUCCESS); - } - #else - _SOKOL_UNUSED(obj_type); - _SOKOL_UNUSED(obj_handle); - _SOKOL_UNUSED(label); - #endif -} - -_SOKOL_PRIVATE int _sapp_vk_mem_find_memory_type_index(uint32_t type_filter, VkMemoryPropertyFlags props) { - SOKOL_ASSERT(_sapp.vk.physical_device); - _SAPP_STRUCT(VkPhysicalDeviceMemoryProperties, mem_props); - vkGetPhysicalDeviceMemoryProperties(_sapp.vk.physical_device, &mem_props); - for (uint32_t i = 0; i < mem_props.memoryTypeCount; i++) { - if ((type_filter & (1 << i)) && ((mem_props.memoryTypes[i].propertyFlags & props) == props)) { - return (int)i; - } - } - return -1; -} - -_SOKOL_PRIVATE void _sapp_vk_create_instance(void) { - SOKOL_ASSERT(0 == _sapp.vk.instance); - - _SAPP_STRUCT(VkApplicationInfo, app_info); - app_info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; - app_info.pApplicationName = "sokol-app"; // FIXME: override via sapp_desc? - app_info.applicationVersion = VK_MAKE_VERSION(1, 0, 0); - app_info.pEngineName = "sokol"; - app_info.engineVersion = VK_MAKE_VERSION(1, 0, 0); - app_info.apiVersion = VK_API_VERSION_1_3; - - _SAPP_VK_ZERO_COUNT_AND_ARRAY(32, const char*, layer_count, layer_names); - #if defined(SOKOL_DEBUG) - layer_names[layer_count++] = "VK_LAYER_KHRONOS_validation"; - SOKOL_ASSERT(layer_count <= 32); - #endif - - _SAPP_VK_ZERO_COUNT_AND_ARRAY(32, const char*, ext_count, ext_names); - ext_names[ext_count++] = VK_KHR_SURFACE_EXTENSION_NAME; - #if defined(SOKOL_DEBUG) - ext_names[ext_count++] = VK_EXT_DEBUG_UTILS_EXTENSION_NAME; - #endif - #if defined(VK_USE_PLATFORM_XLIB_KHR) - ext_names[ext_count++] = VK_KHR_XLIB_SURFACE_EXTENSION_NAME; - #elif defined(VK_USE_PLATFORM_WIN32_KHR) - ext_names[ext_count++] = VK_KHR_WIN32_SURFACE_EXTENSION_NAME; - #endif - SOKOL_ASSERT(ext_count <= 32); - - _SAPP_STRUCT(VkInstanceCreateInfo, create_info); - create_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; - create_info.flags = 0; - create_info.pApplicationInfo = &app_info; - create_info.enabledLayerCount = layer_count; - create_info.ppEnabledLayerNames = layer_names; - create_info.enabledExtensionCount = ext_count; - create_info.ppEnabledExtensionNames = ext_names; - VkResult res = vkCreateInstance(&create_info, 0, &_sapp.vk.instance); - if (res != VK_SUCCESS) { - _SAPP_PANIC(VULKAN_CREATE_INSTANCE_FAILED); - } - SOKOL_ASSERT(_sapp.vk.instance); -} - -_SOKOL_PRIVATE void _sapp_vk_destroy_instance(void) { - SOKOL_ASSERT(_sapp.vk.instance); - vkDestroyInstance(_sapp.vk.instance, 0); - _sapp.vk.instance = 0; -} - -_SOKOL_PRIVATE uint32_t _sapp_vk_required_device_extensions(const char** out_names, uint32_t max_count) { - SOKOL_ASSERT(out_names && (max_count > 0)); - uint32_t count = 0; - out_names[count++] = VK_KHR_SWAPCHAIN_EXTENSION_NAME; - out_names[count++] = VK_EXT_DESCRIPTOR_BUFFER_EXTENSION_NAME; - SOKOL_ASSERT(count <= max_count); _SOKOL_UNUSED(max_count); - return count; -} - -_SOKOL_PRIVATE bool _sapp_vk_check_device_extensions(VkPhysicalDevice pdev, const char** required_exts, uint32_t num_required_exts) { - SOKOL_ASSERT(pdev && required_exts && num_required_exts > 0); - uint32_t ext_count = 0; - VkResult res = vkEnumerateDeviceExtensionProperties(pdev, 0, &ext_count, 0); - SOKOL_ASSERT(res == VK_SUCCESS); _SOKOL_UNUSED(res); - if (ext_count == 0) { - return false; - } - VkExtensionProperties* ext_props = (VkExtensionProperties*) _sapp_malloc(sizeof(VkExtensionProperties) * ext_count); - SOKOL_ASSERT(ext_props); - res = vkEnumerateDeviceExtensionProperties(pdev, 0, &ext_count, ext_props); - bool all_supported = true; - for (uint32_t req_ext_idx = 0; req_ext_idx < num_required_exts; req_ext_idx++) { - const char* req_ext_name = required_exts[req_ext_idx]; - bool required_ext_supported = false; - for (uint32_t ext_idx = 0; ext_idx < ext_count; ext_idx++) { - const VkExtensionProperties* ext_prop = &ext_props[ext_idx]; - if (0 == strcmp(req_ext_name, ext_prop->extensionName)) { - required_ext_supported = true; - break; - } - } - if (!required_ext_supported) { - all_supported = false; - } - } - _sapp_free(ext_props); - return all_supported; -} - -_SOKOL_PRIVATE void _sapp_vk_pick_physical_device(void) { - SOKOL_ASSERT(_sapp.vk.instance); - SOKOL_ASSERT(_sapp.vk.surface); - SOKOL_ASSERT(0 == _sapp.vk.physical_device); - VkResult res = VK_SUCCESS; - - _SAPP_VK_MAX_COUNT_AND_ARRAY(8, VkPhysicalDevice, physical_device_count, physical_devices); - res = vkEnumeratePhysicalDevices(_sapp.vk.instance, &physical_device_count, physical_devices); - if ((res != VK_SUCCESS) && (res != VK_INCOMPLETE)) { - _SAPP_PANIC(VULKAN_ENUMERATE_PHYSICAL_DEVICES_FAILED); - } - if (physical_device_count == 0) { - _SAPP_PANIC(VULKAN_NO_PHYSICAL_DEVICES_FOUND); - } - _SAPP_VK_ZERO_COUNT_AND_ARRAY(32, const char*, ext_count, ext_names); - ext_count = _sapp_vk_required_device_extensions(ext_names, 32); - - VkPhysicalDevice picked_pdev = 0; - for (uint32_t pdev_idx = 0; pdev_idx < physical_device_count; pdev_idx++) { - const VkPhysicalDevice pdev = physical_devices[pdev_idx]; - _SAPP_STRUCT(VkPhysicalDeviceProperties, dev_props); - vkGetPhysicalDeviceProperties(pdev, &dev_props); - if (dev_props.apiVersion < VK_API_VERSION_1_3) { - continue; - } - if (!_sapp_vk_check_device_extensions(pdev, ext_names, ext_count)) { - continue; - } - // FIXME: handle theoretical case where graphics and present aren't supported by the same queue family index - _SAPP_VK_MAX_COUNT_AND_ARRAY(8, VkQueueFamilyProperties, queue_family_props_count, queue_family_props); - vkGetPhysicalDeviceQueueFamilyProperties(pdev, &queue_family_props_count, queue_family_props); - bool has_required_queues = false; - const VkQueueFlags required_flags = VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT | VK_QUEUE_TRANSFER_BIT; - for (uint32_t qfp_idx = 0; qfp_idx < queue_family_props_count; qfp_idx++) { - const VkQueueFlags queue_flags = queue_family_props[qfp_idx].queueFlags; - if ((queue_flags & required_flags) == required_flags) { - _sapp.vk.queue_family_index = qfp_idx; - has_required_queues = true; - break; - } - } - if (!has_required_queues) { - continue; - } - - VkBool32 presentation_supported = false; - res = vkGetPhysicalDeviceSurfaceSupportKHR(pdev, _sapp.vk.queue_family_index, _sapp.vk.surface, &presentation_supported); - SOKOL_ASSERT(VK_SUCCESS == res); - if (!presentation_supported) { - continue; - } - - // if we arrive here, found a suitable device - picked_pdev = pdev; - break; - } - if (0 == picked_pdev) { - _SAPP_PANIC(VULKAN_NO_SUITABLE_PHYSICAL_DEVICE_FOUND); - } - _sapp.vk.physical_device = picked_pdev; - SOKOL_ASSERT(_sapp.vk.physical_device); -} - -_SOKOL_PRIVATE void _sapp_vk_create_device(void) { - SOKOL_ASSERT(_sapp.vk.physical_device); - SOKOL_ASSERT(0 == _sapp.vk.device); - - const float queue_priority = 0.0f; - _SAPP_STRUCT(VkDeviceQueueCreateInfo, queue_create_info); - queue_create_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; - queue_create_info.queueFamilyIndex = _sapp.vk.queue_family_index; - queue_create_info.queueCount = 1; - queue_create_info.pQueuePriorities = &queue_priority; - - _SAPP_VK_ZERO_COUNT_AND_ARRAY(32, const char*, ext_count, ext_names); - ext_count = _sapp_vk_required_device_extensions(ext_names, 32); - - _SAPP_STRUCT(VkPhysicalDeviceFeatures2, supports); - supports.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; - vkGetPhysicalDeviceFeatures2(_sapp.vk.physical_device, &supports); - - _SAPP_STRUCT(VkPhysicalDeviceDescriptorBufferFeaturesEXT, descriptor_buffer_features); - descriptor_buffer_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_BUFFER_FEATURES_EXT; - descriptor_buffer_features.descriptorBuffer = VK_TRUE; - - _SAPP_STRUCT(VkPhysicalDeviceExtendedDynamicStateFeaturesEXT, xds_features); - xds_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT; - xds_features.pNext = &descriptor_buffer_features; - xds_features.extendedDynamicState = VK_TRUE; - - _SAPP_STRUCT(VkPhysicalDeviceVulkan12Features, vk12_features); - vk12_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES; - vk12_features.pNext = &xds_features; - vk12_features.bufferDeviceAddress = VK_TRUE; - - _SAPP_STRUCT(VkPhysicalDeviceVulkan13Features, vk13_features); - vk13_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES; - vk13_features.pNext = &vk12_features; - vk13_features.dynamicRendering = VK_TRUE; - vk13_features.synchronization2 = VK_TRUE; - - _SAPP_STRUCT(VkPhysicalDeviceFeatures2, required); - required.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; - required.pNext = &vk13_features; - required.features.samplerAnisotropy = VK_TRUE; - required.features.dualSrcBlend = VK_TRUE; - if (supports.features.textureCompressionBC) { - required.features.textureCompressionBC = VK_TRUE; - } - if (supports.features.textureCompressionETC2) { - required.features.textureCompressionETC2 = VK_TRUE; - } - if (supports.features.textureCompressionASTC_LDR) { - required.features.textureCompressionASTC_LDR = VK_TRUE; - } - _SAPP_STRUCT(VkDeviceCreateInfo, dev_create_info); - dev_create_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; - dev_create_info.pNext = &required; - dev_create_info.queueCreateInfoCount = 1; - dev_create_info.pQueueCreateInfos = &queue_create_info; - dev_create_info.enabledExtensionCount = ext_count; - dev_create_info.ppEnabledExtensionNames = ext_names; - - VkResult res = vkCreateDevice(_sapp.vk.physical_device, &dev_create_info, 0, &_sapp.vk.device); - if (res != VK_SUCCESS) { - switch (res) { - case VK_ERROR_EXTENSION_NOT_PRESENT: - _SAPP_PANIC(VULKAN_CREATE_DEVICE_FAILED_EXTENSION_NOT_PRESENT); - break; - case VK_ERROR_FEATURE_NOT_PRESENT: - _SAPP_PANIC(VULKAN_CREATE_DEVICE_FAILED_FEATURE_NOT_PRESENT); - break; - case VK_ERROR_INITIALIZATION_FAILED: - _SAPP_PANIC(VULKAN_CREATE_DEVICE_FAILED_INITIALIZATION_FAILED); - break; - default: - _SAPP_PANIC(VULKAN_CREATE_DEVICE_FAILED_OTHER); - break; - } - } - SOKOL_ASSERT(_sapp.vk.device); - - SOKOL_ASSERT(0 == _sapp.vk.queue); - vkGetDeviceQueue(_sapp.vk.device, _sapp.vk.queue_family_index, 0, &_sapp.vk.queue); - SOKOL_ASSERT(_sapp.vk.queue); -} - -_SOKOL_PRIVATE void _sapp_vk_destroy_device(void) { - SOKOL_ASSERT(_sapp.vk.device); - vkDestroyDevice(_sapp.vk.device, 0); - _sapp.vk.device = 0; - _sapp.vk.queue = 0; -} - -_SOKOL_PRIVATE void _sapp_vk_create_surface(void) { - SOKOL_ASSERT(_sapp.vk.instance); - SOKOL_ASSERT(0 == _sapp.vk.surface); - VkResult res = VK_SUCCESS; - - #if defined(_SAPP_LINUX) - _SAPP_STRUCT(VkXlibSurfaceCreateInfoKHR, xlib_info); - xlib_info.sType = VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR; - xlib_info.dpy = _sapp.x11.display; - xlib_info.window = _sapp.x11.window; - res = vkCreateXlibSurfaceKHR(_sapp.vk.instance, &xlib_info, 0, &_sapp.vk.surface); - #elif defined(_SAPP_WIN32) - _SAPP_STRUCT(VkWin32SurfaceCreateInfoKHR, win32_info); - win32_info.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR; - win32_info.hinstance = GetModuleHandleW(NULL); - win32_info.hwnd = _sapp.win32.hwnd; - res = vkCreateWin32SurfaceKHR(_sapp.vk.instance, &win32_info, 0, &_sapp.vk.surface); - #else - #error "sokol_app.h: unsupported Vulkan platform" - #endif - if (res != VK_SUCCESS) { - _SAPP_PANIC(VULKAN_CREATE_SURFACE_FAILED); - } - SOKOL_ASSERT(_sapp.vk.surface); -} - -_SOKOL_PRIVATE void _sapp_vk_destroy_surface(void) { - SOKOL_ASSERT(_sapp.vk.instance); - SOKOL_ASSERT(_sapp.vk.surface); - vkDestroySurfaceKHR(_sapp.vk.instance, _sapp.vk.surface, 0); - _sapp.vk.surface = 0; -} - -_SOKOL_PRIVATE VkSurfaceFormatKHR _sapp_vk_pick_surface_format(void) { - SOKOL_ASSERT(_sapp.vk.instance); - SOKOL_ASSERT(_sapp.vk.surface); - _SAPP_VK_MAX_COUNT_AND_ARRAY(64, VkSurfaceFormatKHR, fmt_count, formats); - VkResult res = vkGetPhysicalDeviceSurfaceFormatsKHR(_sapp.vk.physical_device, _sapp.vk.surface, &fmt_count, formats); - SOKOL_ASSERT((res == VK_SUCCESS) || (res == VK_INCOMPLETE)); _SOKOL_UNUSED(res); - SOKOL_ASSERT(fmt_count > 0); - // FIXME: only accept non-SRGB formats until sokol_app.h gets proper SRGB support - for (uint32_t i = 0; i < fmt_count; i++) { - switch (formats[i].format) { - case VK_FORMAT_B8G8R8A8_UNORM: - case VK_FORMAT_R8G8B8A8_UNORM: - return formats[i]; - default: break; - } - } - // FIXME: fallback might still return an SRGB format - return formats[0]; -} - -_SOKOL_PRIVATE void _sapp_vk_create_sync_objects(void) { - SOKOL_ASSERT(_sapp.vk.device); - _SAPP_STRUCT(VkSemaphoreCreateInfo, create_info); - create_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; - VkResult res; - _SOKOL_UNUSED(res); - for (uint32_t i = 0; i < _sapp.vk.num_swapchain_images; i++) { - SOKOL_ASSERT(0 == _sapp.vk.sync[i].present_complete_sem); - SOKOL_ASSERT(0 == _sapp.vk.sync[i].render_finished_sem); - res = vkCreateSemaphore(_sapp.vk.device, &create_info, 0, &_sapp.vk.sync[i].present_complete_sem); - SOKOL_ASSERT((res == VK_SUCCESS) && (_sapp.vk.sync[i].present_complete_sem)); - _sapp_vk_set_object_label(VK_OBJECT_TYPE_SEMAPHORE, (uint64_t)_sapp.vk.sync[i].present_complete_sem, "present_complete_sem"); - res = vkCreateSemaphore(_sapp.vk.device, &create_info, 0, &_sapp.vk.sync[i].render_finished_sem); - SOKOL_ASSERT((res == VK_SUCCESS) && (_sapp.vk.sync[i].render_finished_sem)); - _sapp_vk_set_object_label(VK_OBJECT_TYPE_SEMAPHORE, (uint64_t)_sapp.vk.sync[i].render_finished_sem, "render_finished_sem"); - } -} - -_SOKOL_PRIVATE void _sapp_vk_destroy_sync_objects(void) { - SOKOL_ASSERT(_sapp.vk.device); - for (uint32_t i = 0; i < _sapp.vk.num_swapchain_images; i++) { - SOKOL_ASSERT(_sapp.vk.sync[i].present_complete_sem); - SOKOL_ASSERT(_sapp.vk.sync[i].render_finished_sem); - vkDestroySemaphore(_sapp.vk.device, _sapp.vk.sync[i].present_complete_sem, 0); - vkDestroySemaphore(_sapp.vk.device, _sapp.vk.sync[i].render_finished_sem, 0); - _sapp.vk.sync[i].present_complete_sem = 0; - _sapp.vk.sync[i].render_finished_sem = 0; - } -} - -_SOKOL_PRIVATE VkDeviceMemory _sapp_vk_mem_alloc_image_memory(const VkMemoryRequirements* mem_reqs) { - SOKOL_ASSERT(_sapp.vk.device); - SOKOL_ASSERT(mem_reqs); - int mem_type_index = _sapp_vk_mem_find_memory_type_index(mem_reqs->memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - if (-1 == mem_type_index) { - _SAPP_ERROR(VULKAN_ALLOC_DEVICE_MEMORY_NO_SUITABLE_MEMORY_TYPE); - return 0; - } - _SAPP_STRUCT(VkMemoryAllocateInfo, alloc_info); - alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; - alloc_info.allocationSize = mem_reqs->size; - alloc_info.memoryTypeIndex = (uint32_t) mem_type_index; - VkDeviceMemory vk_dev_mem = 0; - VkResult res = vkAllocateMemory(_sapp.vk.device, &alloc_info, 0, &vk_dev_mem); - if (res != VK_SUCCESS) { - _SAPP_ERROR(VULKAN_ALLOCATE_MEMORY_FAILED); - return 0; - } - SOKOL_ASSERT(vk_dev_mem); - return vk_dev_mem; -} - -_SOKOL_PRIVATE void _sapp_vk_mem_free_image_memory(VkDeviceMemory vk_dev_mem) { - SOKOL_ASSERT(_sapp.vk.device); - SOKOL_ASSERT(vk_dev_mem); - vkFreeMemory(_sapp.vk.device, vk_dev_mem, 0); -} - -_SOKOL_PRIVATE void _sapp_vk_swapchain_destroy_surface(_sapp_vk_swapchain_surface_t* surf) { - SOKOL_ASSERT(surf); - SOKOL_ASSERT(surf->img); - SOKOL_ASSERT(surf->mem); - SOKOL_ASSERT(surf->view); - vkDestroyImageView(_sapp.vk.device, surf->view, 0); - surf->view = 0; - _sapp_vk_mem_free_image_memory(surf->mem); - surf->mem = 0; - vkDestroyImage(_sapp.vk.device, surf->img, 0); - surf->img = 0; -} - -_SOKOL_PRIVATE void _sapp_vk_swapchain_create_surface( - _sapp_vk_swapchain_surface_t* surf, - bool recreate, - VkFormat format, - uint32_t width, - uint32_t height, - VkSampleCountFlagBits sample_count_flags, - VkImageUsageFlags usage, - VkImageAspectFlags aspect_mask, - const char* image_debug_label, - const char* view_debug_label) -{ - SOKOL_ASSERT(_sapp.vk.physical_device); - SOKOL_ASSERT(_sapp.vk.device); - SOKOL_ASSERT(surf); - if (recreate) { - _sapp_vk_swapchain_destroy_surface(surf); - } - SOKOL_ASSERT(0 == surf->img); - SOKOL_ASSERT(0 == surf->mem); - SOKOL_ASSERT(0 == surf->view); - VkResult res; - - _SAPP_STRUCT(VkImageCreateInfo, img_create_info); - img_create_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; - img_create_info.imageType = VK_IMAGE_TYPE_2D; - img_create_info.format = format; - img_create_info.extent.width = width; - img_create_info.extent.height = height; - img_create_info.extent.depth = 1; - img_create_info.mipLevels = 1; - img_create_info.arrayLayers = 1; - img_create_info.samples = sample_count_flags; - img_create_info.tiling = VK_IMAGE_TILING_OPTIMAL; - img_create_info.usage = usage; - img_create_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - img_create_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - res = vkCreateImage(_sapp.vk.device, &img_create_info, 0, &surf->img); - if (res != VK_SUCCESS) { - _SAPP_PANIC(VULKAN_SWAPCHAIN_CREATE_IMAGE_FAILED); - } - SOKOL_ASSERT(surf->img); - _sapp_vk_set_object_label(VK_OBJECT_TYPE_IMAGE, (uint64_t)surf->img, image_debug_label); - - _SAPP_STRUCT(VkMemoryRequirements, mem_reqs); - vkGetImageMemoryRequirements(_sapp.vk.device, surf->img, &mem_reqs); - surf->mem = _sapp_vk_mem_alloc_image_memory(&mem_reqs); - if (0 == surf->mem) { - _SAPP_PANIC(VULKAN_SWAPCHAIN_ALLOC_IMAGE_DEVICE_MEMORY_FAILED); - } - res = vkBindImageMemory(_sapp.vk.device, surf->img, surf->mem, 0); - if (res != VK_SUCCESS) { - _SAPP_PANIC(VULKAN_SWAPCHAIN_BIND_IMAGE_MEMORY_FAILED); - } - SOKOL_ASSERT(surf->mem); - - _SAPP_STRUCT(VkImageViewCreateInfo, view_create_info); - view_create_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; - view_create_info.image = surf->img; - view_create_info.viewType = VK_IMAGE_VIEW_TYPE_2D; - view_create_info.format = format; - view_create_info.subresourceRange.aspectMask = aspect_mask; - view_create_info.subresourceRange.levelCount = 1; - view_create_info.subresourceRange.layerCount = 1; - res = vkCreateImageView(_sapp.vk.device, &view_create_info, 0, &surf->view); - if (res != VK_SUCCESS) { - _SAPP_PANIC(VULKAN_SWAPCHAIN_CREATE_IMAGE_VIEW_FAILED); - } - SOKOL_ASSERT(surf->view); - _sapp_vk_set_object_label(VK_OBJECT_TYPE_IMAGE_VIEW, (uint64_t)surf->view, view_debug_label); -} - -_SOKOL_PRIVATE uint32_t _sapp_vk_swapchain_min_image_count(const VkSurfaceCapabilitiesKHR* surf_caps) { - // FIXME: figure out why at least 3 swapchain images are required to appease the validation layer - // (on the Linux Intel driver, present-mode-fifo has a surf_caps.minImageCount == 3, while - // on Windows surf_caps.minImageCount == 2, and using this directly causes validation layer - // errors about the present-complete semaphore (to reproduce simply change the '= 3' below to '= 2') - SOKOL_ASSERT(surf_caps); - const uint32_t required_image_count = 3; - uint32_t min_image_count = surf_caps->minImageCount; - if (min_image_count < required_image_count) { - min_image_count = required_image_count; - } - return min_image_count; -} - -_SOKOL_PRIVATE void _sapp_vk_create_swapchain_image_view(uint32_t image_index) { - SOKOL_ASSERT(_sapp.vk.device); - SOKOL_ASSERT(image_index < _sapp.vk.num_swapchain_images); - SOKOL_ASSERT(_sapp.vk.swapchain_images[image_index]); - SOKOL_ASSERT(0 == _sapp.vk.swapchain_views[image_index]); - - _SAPP_STRUCT(VkImageViewCreateInfo, view_create_info); - view_create_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; - view_create_info.viewType = VK_IMAGE_VIEW_TYPE_2D; - view_create_info.format = _sapp.vk.surface_format.format; - view_create_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - view_create_info.subresourceRange.levelCount = 1; - view_create_info.subresourceRange.layerCount = 1; - view_create_info.image = _sapp.vk.swapchain_images[image_index]; - VkResult res = vkCreateImageView(_sapp.vk.device, &view_create_info, 0, &_sapp.vk.swapchain_views[image_index]); - if (res != VK_SUCCESS) { - _SAPP_PANIC(VULKAN_SWAPCHAIN_CREATE_IMAGE_VIEW_FAILED); - } - SOKOL_ASSERT(_sapp.vk.swapchain_views[image_index]); - _sapp_vk_set_object_label(VK_OBJECT_TYPE_IMAGE_VIEW, (uint64_t)_sapp.vk.swapchain_views[image_index], "swapchain_view"); -} - -_SOKOL_PRIVATE void _sapp_vk_destroy_swapchain_image_view(uint32_t image_index) { - SOKOL_ASSERT(_sapp.vk.device); - SOKOL_ASSERT(image_index < _sapp.vk.num_swapchain_images); - SOKOL_ASSERT(_sapp.vk.swapchain_views[image_index]); - vkDestroyImageView(_sapp.vk.device, _sapp.vk.swapchain_views[image_index], 0); - _sapp.vk.swapchain_views[image_index] = 0; -} - -_SOKOL_PRIVATE void _sapp_vk_create_swapchain(bool recreate) { - SOKOL_ASSERT(_sapp.vk.physical_device); - SOKOL_ASSERT(_sapp.vk.surface); - SOKOL_ASSERT(_sapp.vk.device); - if (!recreate) { - SOKOL_ASSERT(0 == _sapp.vk.swapchain); - SOKOL_ASSERT(0 == _sapp.vk.num_swapchain_images); - SOKOL_ASSERT(0 == _sapp.vk.swapchain_images[0]); - SOKOL_ASSERT(0 == _sapp.vk.swapchain_views[0]); - } else { - SOKOL_ASSERT(_sapp.vk.swapchain); - SOKOL_ASSERT(_sapp.vk.num_swapchain_images > 0); - SOKOL_ASSERT(_sapp.vk.swapchain_images[0]); - SOKOL_ASSERT(_sapp.vk.swapchain_views[0]); - } - - VkSwapchainKHR old_swapchain = _sapp.vk.swapchain; - - _SAPP_STRUCT(VkSurfaceCapabilitiesKHR, surf_caps); - VkResult res = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(_sapp.vk.physical_device, _sapp.vk.surface, &surf_caps); - SOKOL_ASSERT(res == VK_SUCCESS); - const uint32_t fb_width = surf_caps.currentExtent.width; - const uint32_t fb_height = surf_caps.currentExtent.height; - - _sapp.vk.surface_format = _sapp_vk_pick_surface_format(); - const VkPresentModeKHR present_mode = VK_PRESENT_MODE_FIFO_KHR; - - _SAPP_STRUCT(VkSwapchainCreateInfoKHR, create_info); - create_info.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; - create_info.flags = 0; - create_info.surface = _sapp.vk.surface; - create_info.minImageCount = _sapp_vk_swapchain_min_image_count(&surf_caps); - create_info.imageFormat = _sapp.vk.surface_format.format; - create_info.imageColorSpace = _sapp.vk.surface_format.colorSpace; - create_info.imageExtent.width = fb_width; - create_info.imageExtent.height = fb_height; - create_info.imageArrayLayers = 1; - create_info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; - create_info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; - create_info.preTransform = surf_caps.currentTransform; - create_info.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; - create_info.presentMode = present_mode; - create_info.clipped = true; - create_info.oldSwapchain = old_swapchain; - res = vkCreateSwapchainKHR(_sapp.vk.device, &create_info, 0, &_sapp.vk.swapchain); - if (res != VK_SUCCESS) { - _SAPP_PANIC(VULKAN_CREATE_SWAPCHAIN_FAILED); - } - SOKOL_ASSERT(_sapp.vk.swapchain); - - if (old_swapchain) { - // NOTE: destroying the depth- and msaa-surfaces happens - // down in the respective _sapp_vk_swapchain_create_surface() calls! - for (uint32_t i = 0; i < _sapp.vk.num_swapchain_images; i++) { - _sapp_vk_destroy_swapchain_image_view(i); - } - vkDestroySwapchainKHR(_sapp.vk.device, old_swapchain, 0); - } - - _sapp.vk.num_swapchain_images = _SAPP_VK_MAX_SWAPCHAIN_IMAGES; - res = vkGetSwapchainImagesKHR(_sapp.vk.device, - _sapp.vk.swapchain, - &_sapp.vk.num_swapchain_images, - _sapp.vk.swapchain_images); - SOKOL_ASSERT(res == VK_SUCCESS); - SOKOL_ASSERT(_sapp.vk.num_swapchain_images >= surf_caps.minImageCount); - - for (uint32_t i = 0; i < _sapp.vk.num_swapchain_images; i++) { - _sapp_vk_create_swapchain_image_view(i); - } - - // create depth-stencil buffer - _sapp_vk_swapchain_create_surface(&_sapp.vk.depth, - recreate, - VK_FORMAT_D32_SFLOAT_S8_UINT, - fb_width, - fb_height, - (VkSampleCountFlagBits)_sapp.sample_count, - VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, - VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT, - "swapchain_depthstencil_image", - "swapchain_depthstencil_view"); - - // optionally create MSAA surface - if (_sapp.sample_count > 1) { - _sapp_vk_swapchain_create_surface(&_sapp.vk.msaa, - recreate, - _sapp.vk.surface_format.format, - fb_width, - fb_height, - (VkSampleCountFlagBits)_sapp.sample_count, - VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, - VK_IMAGE_ASPECT_COLOR_BIT, - "swapchain_msaa_image", - "swapchain_msaa_view"); - } - - // this is the only place in the Vulkan code path which updates - // _sapp.framebuffer_width/height - _sapp.framebuffer_width = (int)fb_width; - _sapp.framebuffer_height = (int)fb_height; -} - -_SOKOL_PRIVATE void _sapp_vk_destroy_swapchain(void) { - SOKOL_ASSERT(_sapp.vk.device); - SOKOL_ASSERT(_sapp.vk.swapchain); - SOKOL_ASSERT(_sapp.vk.num_swapchain_images > 0); - if (_sapp.vk.msaa.img) { - _sapp_vk_swapchain_destroy_surface(&_sapp.vk.msaa); - } - _sapp_vk_swapchain_destroy_surface(&_sapp.vk.depth); - for (uint32_t i = 0; i < _sapp.vk.num_swapchain_images; i++) { - _sapp_vk_destroy_swapchain_image_view(i); - _sapp.vk.swapchain_images[i] = 0; - } - vkDestroySwapchainKHR(_sapp.vk.device, _sapp.vk.swapchain, 0); - _sapp.vk.swapchain = 0; - _sapp.vk.num_swapchain_images = 0; -} - -#if defined(_SAPP_LINUX) -_SOKOL_PRIVATE void _sapp_x11_app_event(sapp_event_type type); -#endif -#if defined(_SAPP_WIN32) -_SOKOL_PRIVATE void _sapp_win32_app_event(sapp_event_type type); -#endif - -_SOKOL_PRIVATE void _sapp_vk_recreate_swapchain(void) { - SOKOL_ASSERT(_sapp.vk.device); - vkDeviceWaitIdle(_sapp.vk.device); - int fb_width = _sapp.framebuffer_width; - int fb_height = _sapp.framebuffer_height; - _sapp_vk_create_swapchain(true); - if ((fb_width != _sapp.framebuffer_width) || (fb_height != _sapp.framebuffer_height)) { - if (!_sapp.first_frame) { - #if defined(_SAPP_LINUX) - _sapp_x11_app_event(SAPP_EVENTTYPE_RESIZED); - #endif - #if defined(_SAPP_WIN32) - _sapp_win32_app_event(SAPP_EVENTTYPE_RESIZED); - #endif - } - } -} - -_SOKOL_PRIVATE void _sapp_vk_init(void) { - _sapp_vk_create_instance(); - _sapp_vk_load_instance_ext_funcs(); - _sapp_vk_create_surface(); - _sapp_vk_pick_physical_device(); - _sapp_vk_create_device(); - _sapp_vk_create_swapchain(false); - _sapp_vk_create_sync_objects(); -} - -_SOKOL_PRIVATE void _sapp_vk_discard(void) { - SOKOL_ASSERT(_sapp.vk.device); - vkDeviceWaitIdle(_sapp.vk.device); - _sapp_vk_destroy_sync_objects(); - _sapp_vk_destroy_swapchain(); - _sapp_vk_destroy_device(); - _sapp_vk_destroy_surface(); - _sapp_vk_destroy_instance(); -} - -_SOKOL_PRIVATE void _sapp_vk_swapchain_next(void) { - SOKOL_ASSERT(_sapp.vk.device); - SOKOL_ASSERT(_sapp.vk.swapchain); - _sapp.vk.swapchain_acquired = true; - VkResult res = vkAcquireNextImageKHR( - _sapp.vk.device, - _sapp.vk.swapchain, - UINT64_MAX, // timeout - _sapp.vk.sync[_sapp.vk.sync_slot].present_complete_sem, // semaphore to signal - 0, // fence to signal - &_sapp.vk.cur_swapchain_image_index); - if ((res != VK_NOT_READY) && (res != VK_SUBOPTIMAL_KHR) && (res != VK_SUCCESS) && (res != VK_TIMEOUT)) { - _SAPP_WARN(VULKAN_ACQUIRE_NEXT_IMAGE_FAILED); - } -} - -_SOKOL_PRIVATE void _sapp_vk_present(void) { - SOKOL_ASSERT(_sapp.vk.queue); - if (_sapp.vk.swapchain_acquired) { - _sapp.vk.swapchain_acquired = false; - _SAPP_STRUCT(VkPresentInfoKHR, present_info); - present_info.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; - present_info.waitSemaphoreCount = 1; - // NOTE: using the current swapchain image index here instead of `sync_slot` is *NOT* a bug! The render_finished_semaphore *must* - // be associated with the current swapchain image in case the swapchain implementation doesn't return swapchain images in order - present_info.pWaitSemaphores = &_sapp.vk.sync[_sapp.vk.cur_swapchain_image_index].render_finished_sem; - present_info.swapchainCount = 1; - present_info.pSwapchains = &_sapp.vk.swapchain; - present_info.pImageIndices = &_sapp.vk.cur_swapchain_image_index; - VkResult res = vkQueuePresentKHR(_sapp.vk.queue, &present_info); - if ((res == VK_ERROR_OUT_OF_DATE_KHR) || (res == VK_SUBOPTIMAL_KHR)) { - _sapp_vk_recreate_swapchain(); - } else if (res != VK_SUCCESS) { - _SAPP_WARN(VULKAN_QUEUE_PRESENT_FAILED); - } - } -} - -_SOKOL_PRIVATE void _sapp_vk_frame(void) { - _sapp_frame(); - // Modified by tettou771 for TrussC: skip present support - if (_sapp.skip_present) { _sapp.skip_present = false; } - else { _sapp_vk_present(); } - _sapp.vk.sync_slot = (_sapp.vk.sync_slot + 1) % _sapp.vk.num_swapchain_images; -} - -#endif // SOKOL_VULKAN - -// █████ ██████ ██████ ██ ███████ -// ██ ██ ██ ██ ██ ██ ██ ██ -// ███████ ██████ ██████ ██ █████ -// ██ ██ ██ ██ ██ ██ -// ██ ██ ██ ██ ███████ ███████ -// -// >>apple -#if defined(_SAPP_APPLE) - -#if __has_feature(objc_arc) -#define _SAPP_OBJC_RELEASE(obj) { obj = nil; } -#else -#define _SAPP_OBJC_RELEASE(obj) { [obj release]; obj = nil; } -#endif - -// ███ ███ █████ ██████ ██████ ███████ -// ████ ████ ██ ██ ██ ██ ██ ██ -// ██ ████ ██ ███████ ██ ██ ██ ███████ -// ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ██ ██ ██ ██ ██████ ██████ ███████ -// -// >>macos -#if defined(_SAPP_MACOS) - -#define _SAPP_MACOS_MTL_OBSCURED_FRAME_DURATION_IN_SECONDS (0.0166667) - -_SOKOL_PRIVATE NSInteger _sapp_macos_max_fps(void) { - return [NSScreen.mainScreen maximumFramesPerSecond]; -} - -#if defined(SOKOL_METAL) -_SOKOL_PRIVATE id _sapp_macos_mtl_create_texture(int width, int height, MTLPixelFormat fmt, int sample_count, const char* label) { - MTLTextureDescriptor* mtl_desc = [[MTLTextureDescriptor alloc] init]; - if (sample_count > 1) { - mtl_desc.textureType = MTLTextureType2DMultisample; - } else { - mtl_desc.textureType = MTLTextureType2D; - } - mtl_desc.pixelFormat = fmt; - mtl_desc.width = (NSUInteger)width; - mtl_desc.height = (NSUInteger)height; - mtl_desc.depth = 1; - mtl_desc.mipmapLevelCount = 1; - mtl_desc.arrayLength = 1; - mtl_desc.sampleCount = (NSUInteger)sample_count; - mtl_desc.usage = MTLTextureUsageRenderTarget; - mtl_desc.resourceOptions = MTLResourceStorageModePrivate; - id mtl_tex = [_sapp.macos.mtl.device newTextureWithDescriptor:mtl_desc]; - _SAPP_OBJC_RELEASE(mtl_desc); - #if defined(SOKOL_DEBUG) - if (mtl_tex) { - mtl_tex.label = [NSString stringWithUTF8String:label]; - } - #else - _SOKOL_UNUSED(label); - #endif - return mtl_tex; -} - -_SOKOL_PRIVATE void _sapp_macos_mtl_swapchain_create(int width, int height) { - _sapp.macos.mtl.depth_tex =_sapp_macos_mtl_create_texture(width, height, MTLPixelFormatDepth32Float_Stencil8, _sapp.sample_count, "swapchain_depth_tex"); - if (nil == _sapp.macos.mtl.depth_tex) { - _SAPP_PANIC(METAL_CREATE_SWAPCHAIN_DEPTH_TEXTURE_FAILED); - } - if (_sapp.sample_count > 1) { - _sapp.macos.mtl.msaa_tex = _sapp_macos_mtl_create_texture(width, height, MTLPixelFormatRGB10A2Unorm /* Modified by tettou771 for TrussC: 10-bit color */, _sapp.sample_count, "swapchain_msaa_tex"); - if (nil == _sapp.macos.mtl.msaa_tex) { - _SAPP_PANIC(METAL_CREATE_SWAPCHAIN_MSAA_TEXTURE_FAILED); - } - } -} - -_SOKOL_PRIVATE void _sapp_macos_mtl_swapchain_destroy(void) { - if (_sapp.macos.mtl.depth_tex) { - _SAPP_OBJC_RELEASE(_sapp.macos.mtl.depth_tex); - } - if (_sapp.macos.mtl.msaa_tex) { - _SAPP_OBJC_RELEASE(_sapp.macos.mtl.msaa_tex); - } -} - -_SOKOL_PRIVATE void _sapp_macos_mtl_swapchain_resize(int width, int height) { - _sapp_macos_mtl_swapchain_destroy(); - _sapp_macos_mtl_swapchain_create(width, height); -} - -_SOKOL_PRIVATE id _sapp_macos_mtl_swapchain_next(void) { - id drawable = [_sapp.macos.mtl.layer nextDrawable]; - SOKOL_ASSERT(drawable != nil); - return drawable; -} - -_SOKOL_PRIVATE bool _sapp_macos_mtl_display_link_active(void) { - return (nil != _sapp.macos.mtl.display_link) && (!_sapp.macos.mtl.display_link.paused); -} - -_SOKOL_PRIVATE void _sapp_macos_mtl_timing_init(void) { - _sapp.macos.mtl.timing.timestamp = 0.0; - _sapp.macos.mtl.timing.frame_duration_sec = 1.0 / _sapp_macos_max_fps(); -} - -_SOKOL_PRIVATE void _sapp_macos_mtl_timing_update(void) { - // NOTE: if display link is not active, frame duration will be provided - // by the regular platform-agnostic timing code - if (_sapp_macos_mtl_display_link_active()) { - CFTimeInterval cur_timestamp = _sapp.macos.mtl.display_link.timestamp; - // skip first frame (frame_duration had been initialized to display refresh rate) - if (_sapp.macos.mtl.timing.timestamp > 0.0) { - const double dt = cur_timestamp - _sapp.macos.mtl.timing.timestamp; - _sapp.macos.mtl.timing.frame_duration_sec = _sapp_timing_clamp(&_sapp.timing, dt); - } else { - SOKOL_ASSERT(_sapp.macos.mtl.timing.frame_duration_sec > 0.0); - } - _sapp.macos.mtl.timing.timestamp = cur_timestamp; - } -} - -_SOKOL_PRIVATE double _sapp_macos_mtl_timing_frame_duration(void) { - if (_sapp_macos_mtl_display_link_active()) { - SOKOL_ASSERT(_sapp.macos.mtl.timing.frame_duration_sec > 0.0); - return _sapp.macos.mtl.timing.frame_duration_sec; - } else { - return _sapp_timing_get(&_sapp.timing); - } -} - -_SOKOL_PRIVATE void _sapp_macos_mtl_start_display_link(void) { - if (nil != _sapp.macos.mtl.display_link) { - _sapp.macos.mtl.display_link.paused = false; - return; - } - // NOTE: CADisplayLink is only available since macOS 14.0 - SOKOL_ASSERT(nil == _sapp.macos.mtl.display_link); - SOKOL_ASSERT(nil == _sapp.macos.mtl.fallback_timer); - SOKOL_ASSERT(nil != _sapp.macos.view); - NSInteger max_fps = _sapp_macos_max_fps(); - _sapp.macos.mtl.display_link = [_sapp.macos.view displayLinkWithTarget:_sapp.macos.view selector:@selector(displayLinkFired:)]; - const float preferred_fps = max_fps / _sapp.swap_interval; - const CAFrameRateRange frame_rate_range = { preferred_fps, preferred_fps, preferred_fps }; - _sapp.macos.mtl.display_link.preferredFrameRateRange = frame_rate_range; - [_sapp.macos.mtl.display_link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; -} - -_SOKOL_PRIVATE void _sapp_macos_mtl_stop_display_link(void) { - if (nil != _sapp.macos.mtl.display_link) { - _sapp.macos.mtl.display_link.paused = true; - } -} - -_SOKOL_PRIVATE void _sapp_macos_mtl_start_fallback_timer(void) { - SOKOL_ASSERT(nil == _sapp.macos.mtl.fallback_timer); - _sapp.macos.mtl.fallback_timer = [NSTimer - timerWithTimeInterval: _SAPP_MACOS_MTL_OBSCURED_FRAME_DURATION_IN_SECONDS - target: _sapp.macos.view - selector: @selector(fallbackTimerFired:) - userInfo: nil - repeats: YES]; - [[NSRunLoop currentRunLoop] addTimer:_sapp.macos.mtl.fallback_timer forMode:NSRunLoopCommonModes]; -} - -_SOKOL_PRIVATE void _sapp_macos_mtl_stop_fallback_timer(void) { - if (nil != _sapp.macos.mtl.fallback_timer) { - [_sapp.macos.mtl.fallback_timer invalidate]; - _sapp.macos.mtl.fallback_timer = nil; - } -} - -_SOKOL_PRIVATE void _sapp_macos_mtl_transition_to_occluded(void) { - if (_sapp_macos_mtl_display_link_active()) { - _sapp_macos_mtl_stop_display_link(); - _sapp_macos_mtl_start_fallback_timer(); - } -} - -_SOKOL_PRIVATE void _sapp_macos_mtl_transition_to_visible(void) { - if (!_sapp_macos_mtl_display_link_active()) { - _sapp_macos_mtl_stop_fallback_timer(); - _sapp_macos_mtl_start_display_link(); - } -} - -_SOKOL_PRIVATE void _sapp_macos_mtl_init(void) { - _sapp.macos.mtl.device = MTLCreateSystemDefaultDevice(); - _sapp.macos.mtl.layer = [CAMetalLayer layer]; - _sapp.macos.mtl.layer.device = _sapp.macos.mtl.device; - _sapp.macos.mtl.layer.magnificationFilter = kCAFilterNearest; - _sapp.macos.mtl.layer.opaque = true; - _sapp.macos.mtl.layer.pixelFormat = MTLPixelFormatRGB10A2Unorm /* Modified by tettou771 for TrussC: 10-bit color */; - _sapp.macos.mtl.layer.framebufferOnly = false /* Modified for TrussC: enable captureWindow() reads (issue #56) */; - //NOTE: default is 3: _sapp.macos.mtl.layer.maximumDrawableCount = 2; - // FIXME: _sapp.macos.mtl.layer.colorspace = ...; - _sapp.macos.view = [[_sapp_macos_view alloc] init]; - [_sapp.macos.view updateTrackingAreas]; - _sapp.macos.view.wantsLayer = YES; - _sapp.macos.view.layer = _sapp.macos.mtl.layer; - _sapp_macos_mtl_start_display_link(); - _sapp_macos_mtl_timing_init(); -} - -_SOKOL_PRIVATE void _sapp_macos_mtl_discard_state(void) { - _sapp_macos_mtl_stop_display_link(); - _sapp_macos_mtl_stop_fallback_timer(); - _sapp_macos_mtl_swapchain_destroy(); - _SAPP_OBJC_RELEASE(_sapp.macos.mtl.layer); - _SAPP_OBJC_RELEASE(_sapp.macos.mtl.device); -} - -_SOKOL_PRIVATE bool _sapp_macos_mtl_update_framebuffer_dimensions(NSRect view_bounds) { - _sapp.framebuffer_width = _sapp_roundf_gzero(view_bounds.size.width * _sapp.dpi_scale); - _sapp.framebuffer_height = _sapp_roundf_gzero(view_bounds.size.height * _sapp.dpi_scale); - const CGSize cur_fb_size = _sapp.macos.mtl.layer.drawableSize; - int cur_fb_width = _sapp_roundf_gzero(cur_fb_size.width); - int cur_fb_height = _sapp_roundf_gzero(cur_fb_size.height); - bool dim_changed = (_sapp.framebuffer_width != cur_fb_width) || (_sapp.framebuffer_height != cur_fb_height); - if (dim_changed) { - const CGSize drawable_size = { (CGFloat) _sapp.framebuffer_width, (CGFloat) _sapp.framebuffer_height }; - _sapp.macos.mtl.layer.drawableSize = drawable_size; - _sapp_macos_mtl_swapchain_resize(_sapp.framebuffer_width, _sapp.framebuffer_height); - } - return dim_changed; -} -#endif - -#if defined(SOKOL_WGPU) -_SOKOL_PRIVATE void _sapp_macos_wgpu_init(void) { - NSInteger max_fps = _sapp_macos_max_fps(); - _sapp.macos.wgpu.mtl_layer = [CAMetalLayer layer]; - _sapp.macos.wgpu.mtl_layer.magnificationFilter = kCAFilterNearest; - _sapp.macos.wgpu.mtl_layer.opaque = true; - // NOTE: might experiment with this, valid values are 2 or 3 (default: 3), I don't see any difference tbh - // _sapp.macos.wgpu.mtl_layer.maximumDrawableCount = 2; - _sapp.macos.view = [[_sapp_macos_view alloc] init]; - [_sapp.macos.view updateTrackingAreas]; - _sapp.macos.view.wantsLayer = YES; - _sapp.macos.view.layer = _sapp.macos.wgpu.mtl_layer; - _sapp.macos.wgpu.display_link = [_sapp.macos.view displayLinkWithTarget:_sapp.macos.view selector:@selector(displayLinkFired:)]; - float preferred_fps = max_fps / _sapp.swap_interval; - CAFrameRateRange frame_rate_range = { preferred_fps, preferred_fps, preferred_fps }; - _sapp.macos.wgpu.display_link.preferredFrameRateRange = frame_rate_range; - [_sapp.macos.wgpu.display_link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; - _sapp_wgpu_init(); -} - -_SOKOL_PRIVATE void _sapp_macos_wgpu_discard_state(void) { - _SAPP_OBJC_RELEASE(_sapp.macos.wgpu.display_link); - _SAPP_OBJC_RELEASE(_sapp.macos.wgpu.mtl_layer); - _sapp_wgpu_discard(); -} - -_SOKOL_PRIVATE bool _sapp_macos_wgpu_update_framebuffer_dimensions(NSRect view_bounds) { - _sapp.framebuffer_width = _sapp_roundf_gzero(view_bounds.size.width * _sapp.dpi_scale); - _sapp.framebuffer_height = _sapp_roundf_gzero(view_bounds.size.height * _sapp.dpi_scale); - const CGSize cur_fb_size = _sapp.macos.wgpu.mtl_layer.drawableSize; - int cur_fb_width = _sapp_roundf_gzero(cur_fb_size.width); - int cur_fb_height = _sapp_roundf_gzero(cur_fb_size.height); - bool dim_changed = (_sapp.framebuffer_width != cur_fb_width) || (_sapp.framebuffer_height != cur_fb_height); - if (dim_changed) { - const CGSize drawable_size = { (CGFloat) _sapp.framebuffer_width, (CGFloat) _sapp.framebuffer_height }; - _sapp.macos.wgpu.mtl_layer.drawableSize = drawable_size; - _sapp_wgpu_swapchain_size_changed(); - } - return dim_changed; -} -#endif - -#if defined(SOKOL_GLCORE) -_SOKOL_PRIVATE void _sapp_macos_gl_init(NSRect window_rect) { - NSOpenGLPixelFormatAttribute attrs[32]; - int i = 0; - attrs[i++] = NSOpenGLPFAAccelerated; - attrs[i++] = NSOpenGLPFADoubleBuffer; - attrs[i++] = NSOpenGLPFAOpenGLProfile; - const int glVersion = _sapp.desc.gl.major_version * 10 + _sapp.desc.gl.minor_version; - switch(glVersion) { - case 10: attrs[i++] = NSOpenGLProfileVersionLegacy; break; - case 32: attrs[i++] = NSOpenGLProfileVersion3_2Core; break; - case 41: attrs[i++] = NSOpenGLProfileVersion4_1Core; break; - default: - _SAPP_PANIC(MACOS_INVALID_NSOPENGL_PROFILE); - } - attrs[i++] = NSOpenGLPFAColorSize; attrs[i++] = 24; - attrs[i++] = NSOpenGLPFAAlphaSize; attrs[i++] = 8; - attrs[i++] = NSOpenGLPFADepthSize; attrs[i++] = 24; - attrs[i++] = NSOpenGLPFAStencilSize; attrs[i++] = 8; - if (_sapp.sample_count > 1) { - attrs[i++] = NSOpenGLPFAMultisample; - attrs[i++] = NSOpenGLPFASampleBuffers; attrs[i++] = 1; - attrs[i++] = NSOpenGLPFASamples; attrs[i++] = (NSOpenGLPixelFormatAttribute)_sapp.sample_count; - } else { - attrs[i++] = NSOpenGLPFASampleBuffers; attrs[i++] = 0; - } - attrs[i++] = 0; - NSOpenGLPixelFormat* glpixelformat_obj = [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs]; - SOKOL_ASSERT(glpixelformat_obj != nil); - - _sapp.macos.view = [[_sapp_macos_view alloc] - initWithFrame:window_rect - pixelFormat:glpixelformat_obj]; - _SAPP_OBJC_RELEASE(glpixelformat_obj); - [_sapp.macos.view updateTrackingAreas]; - if (_sapp.desc.high_dpi) { - [_sapp.macos.view setWantsBestResolutionOpenGLSurface:YES]; - } else { - [_sapp.macos.view setWantsBestResolutionOpenGLSurface:NO]; - } - - NSTimer* timer_obj = [NSTimer timerWithTimeInterval:0.001 - target:_sapp.macos.view - selector:@selector(timerFired:) - userInfo:nil - repeats:YES]; - [[NSRunLoop currentRunLoop] addTimer:timer_obj forMode:NSDefaultRunLoopMode]; - timer_obj = nil; -} - -_SOKOL_PRIVATE void _sapp_macos_gl_discard_state(void) { - // nothing to do here -} - -_SOKOL_PRIVATE bool _sapp_macos_gl_update_framebuffer_dimensions(NSRect view_bounds) { - const int cur_fb_width = _sapp_roundf_gzero(view_bounds.size.width * _sapp.dpi_scale); - const int cur_fb_height = _sapp_roundf_gzero(view_bounds.size.height * _sapp.dpi_scale); - const bool dim_changed = (_sapp.framebuffer_width != cur_fb_width) || (_sapp.framebuffer_height != cur_fb_height); - _sapp.framebuffer_width = cur_fb_width; - _sapp.framebuffer_height = cur_fb_height; - return dim_changed; -} -#endif - -_SOKOL_PRIVATE void _sapp_macos_init_keytable(void) { - _sapp.keycodes[0x1D] = SAPP_KEYCODE_0; - _sapp.keycodes[0x12] = SAPP_KEYCODE_1; - _sapp.keycodes[0x13] = SAPP_KEYCODE_2; - _sapp.keycodes[0x14] = SAPP_KEYCODE_3; - _sapp.keycodes[0x15] = SAPP_KEYCODE_4; - _sapp.keycodes[0x17] = SAPP_KEYCODE_5; - _sapp.keycodes[0x16] = SAPP_KEYCODE_6; - _sapp.keycodes[0x1A] = SAPP_KEYCODE_7; - _sapp.keycodes[0x1C] = SAPP_KEYCODE_8; - _sapp.keycodes[0x19] = SAPP_KEYCODE_9; - _sapp.keycodes[0x00] = SAPP_KEYCODE_A; - _sapp.keycodes[0x0B] = SAPP_KEYCODE_B; - _sapp.keycodes[0x08] = SAPP_KEYCODE_C; - _sapp.keycodes[0x02] = SAPP_KEYCODE_D; - _sapp.keycodes[0x0E] = SAPP_KEYCODE_E; - _sapp.keycodes[0x03] = SAPP_KEYCODE_F; - _sapp.keycodes[0x05] = SAPP_KEYCODE_G; - _sapp.keycodes[0x04] = SAPP_KEYCODE_H; - _sapp.keycodes[0x22] = SAPP_KEYCODE_I; - _sapp.keycodes[0x26] = SAPP_KEYCODE_J; - _sapp.keycodes[0x28] = SAPP_KEYCODE_K; - _sapp.keycodes[0x25] = SAPP_KEYCODE_L; - _sapp.keycodes[0x2E] = SAPP_KEYCODE_M; - _sapp.keycodes[0x2D] = SAPP_KEYCODE_N; - _sapp.keycodes[0x1F] = SAPP_KEYCODE_O; - _sapp.keycodes[0x23] = SAPP_KEYCODE_P; - _sapp.keycodes[0x0C] = SAPP_KEYCODE_Q; - _sapp.keycodes[0x0F] = SAPP_KEYCODE_R; - _sapp.keycodes[0x01] = SAPP_KEYCODE_S; - _sapp.keycodes[0x11] = SAPP_KEYCODE_T; - _sapp.keycodes[0x20] = SAPP_KEYCODE_U; - _sapp.keycodes[0x09] = SAPP_KEYCODE_V; - _sapp.keycodes[0x0D] = SAPP_KEYCODE_W; - _sapp.keycodes[0x07] = SAPP_KEYCODE_X; - _sapp.keycodes[0x10] = SAPP_KEYCODE_Y; - _sapp.keycodes[0x06] = SAPP_KEYCODE_Z; - _sapp.keycodes[0x27] = SAPP_KEYCODE_APOSTROPHE; - _sapp.keycodes[0x2A] = SAPP_KEYCODE_BACKSLASH; - _sapp.keycodes[0x2B] = SAPP_KEYCODE_COMMA; - _sapp.keycodes[0x18] = SAPP_KEYCODE_EQUAL; - _sapp.keycodes[0x32] = SAPP_KEYCODE_GRAVE_ACCENT; - _sapp.keycodes[0x21] = SAPP_KEYCODE_LEFT_BRACKET; - _sapp.keycodes[0x1B] = SAPP_KEYCODE_MINUS; - _sapp.keycodes[0x2F] = SAPP_KEYCODE_PERIOD; - _sapp.keycodes[0x1E] = SAPP_KEYCODE_RIGHT_BRACKET; - _sapp.keycodes[0x29] = SAPP_KEYCODE_SEMICOLON; - _sapp.keycodes[0x2C] = SAPP_KEYCODE_SLASH; - _sapp.keycodes[0x0A] = SAPP_KEYCODE_WORLD_1; - _sapp.keycodes[0x33] = SAPP_KEYCODE_BACKSPACE; - _sapp.keycodes[0x39] = SAPP_KEYCODE_CAPS_LOCK; - _sapp.keycodes[0x75] = SAPP_KEYCODE_DELETE; - _sapp.keycodes[0x7D] = SAPP_KEYCODE_DOWN; - _sapp.keycodes[0x77] = SAPP_KEYCODE_END; - _sapp.keycodes[0x24] = SAPP_KEYCODE_ENTER; - _sapp.keycodes[0x35] = SAPP_KEYCODE_ESCAPE; - _sapp.keycodes[0x7A] = SAPP_KEYCODE_F1; - _sapp.keycodes[0x78] = SAPP_KEYCODE_F2; - _sapp.keycodes[0x63] = SAPP_KEYCODE_F3; - _sapp.keycodes[0x76] = SAPP_KEYCODE_F4; - _sapp.keycodes[0x60] = SAPP_KEYCODE_F5; - _sapp.keycodes[0x61] = SAPP_KEYCODE_F6; - _sapp.keycodes[0x62] = SAPP_KEYCODE_F7; - _sapp.keycodes[0x64] = SAPP_KEYCODE_F8; - _sapp.keycodes[0x65] = SAPP_KEYCODE_F9; - _sapp.keycodes[0x6D] = SAPP_KEYCODE_F10; - _sapp.keycodes[0x67] = SAPP_KEYCODE_F11; - _sapp.keycodes[0x6F] = SAPP_KEYCODE_F12; - _sapp.keycodes[0x69] = SAPP_KEYCODE_F13; - _sapp.keycodes[0x6B] = SAPP_KEYCODE_F14; - _sapp.keycodes[0x71] = SAPP_KEYCODE_F15; - _sapp.keycodes[0x6A] = SAPP_KEYCODE_F16; - _sapp.keycodes[0x40] = SAPP_KEYCODE_F17; - _sapp.keycodes[0x4F] = SAPP_KEYCODE_F18; - _sapp.keycodes[0x50] = SAPP_KEYCODE_F19; - _sapp.keycodes[0x5A] = SAPP_KEYCODE_F20; - _sapp.keycodes[0x73] = SAPP_KEYCODE_HOME; - _sapp.keycodes[0x72] = SAPP_KEYCODE_INSERT; - _sapp.keycodes[0x7B] = SAPP_KEYCODE_LEFT; - _sapp.keycodes[0x3A] = SAPP_KEYCODE_LEFT_ALT; - _sapp.keycodes[0x3B] = SAPP_KEYCODE_LEFT_CONTROL; - _sapp.keycodes[0x38] = SAPP_KEYCODE_LEFT_SHIFT; - _sapp.keycodes[0x37] = SAPP_KEYCODE_LEFT_SUPER; - _sapp.keycodes[0x6E] = SAPP_KEYCODE_MENU; - _sapp.keycodes[0x47] = SAPP_KEYCODE_NUM_LOCK; - _sapp.keycodes[0x79] = SAPP_KEYCODE_PAGE_DOWN; - _sapp.keycodes[0x74] = SAPP_KEYCODE_PAGE_UP; - _sapp.keycodes[0x7C] = SAPP_KEYCODE_RIGHT; - _sapp.keycodes[0x3D] = SAPP_KEYCODE_RIGHT_ALT; - _sapp.keycodes[0x3E] = SAPP_KEYCODE_RIGHT_CONTROL; - _sapp.keycodes[0x3C] = SAPP_KEYCODE_RIGHT_SHIFT; - _sapp.keycodes[0x36] = SAPP_KEYCODE_RIGHT_SUPER; - _sapp.keycodes[0x31] = SAPP_KEYCODE_SPACE; - _sapp.keycodes[0x30] = SAPP_KEYCODE_TAB; - _sapp.keycodes[0x7E] = SAPP_KEYCODE_UP; - _sapp.keycodes[0x52] = SAPP_KEYCODE_KP_0; - _sapp.keycodes[0x53] = SAPP_KEYCODE_KP_1; - _sapp.keycodes[0x54] = SAPP_KEYCODE_KP_2; - _sapp.keycodes[0x55] = SAPP_KEYCODE_KP_3; - _sapp.keycodes[0x56] = SAPP_KEYCODE_KP_4; - _sapp.keycodes[0x57] = SAPP_KEYCODE_KP_5; - _sapp.keycodes[0x58] = SAPP_KEYCODE_KP_6; - _sapp.keycodes[0x59] = SAPP_KEYCODE_KP_7; - _sapp.keycodes[0x5B] = SAPP_KEYCODE_KP_8; - _sapp.keycodes[0x5C] = SAPP_KEYCODE_KP_9; - _sapp.keycodes[0x45] = SAPP_KEYCODE_KP_ADD; - _sapp.keycodes[0x41] = SAPP_KEYCODE_KP_DECIMAL; - _sapp.keycodes[0x4B] = SAPP_KEYCODE_KP_DIVIDE; - _sapp.keycodes[0x4C] = SAPP_KEYCODE_KP_ENTER; - _sapp.keycodes[0x51] = SAPP_KEYCODE_KP_EQUAL; - _sapp.keycodes[0x43] = SAPP_KEYCODE_KP_MULTIPLY; - _sapp.keycodes[0x4E] = SAPP_KEYCODE_KP_SUBTRACT; -} - -_SOKOL_PRIVATE void _sapp_macos_discard_state(void) { - // NOTE: it's safe to call [release] on a nil object - if (_sapp.macos.keyup_monitor != nil) { - [NSEvent removeMonitor:_sapp.macos.keyup_monitor]; - // NOTE: removeMonitor also releases the object - _sapp.macos.keyup_monitor = nil; - } - _SAPP_OBJC_RELEASE(_sapp.macos.tracking_area); - _SAPP_OBJC_RELEASE(_sapp.macos.app_dlg); - _SAPP_OBJC_RELEASE(_sapp.macos.win_dlg); - _SAPP_OBJC_RELEASE(_sapp.macos.view); - #if defined(SOKOL_METAL) - _sapp_macos_mtl_discard_state(); - #elif defined(SOKOL_GLCORE) - _sapp_macos_gl_discard_state(); - #elif defined(SOKOL_WGPU) - _sapp_macos_wgpu_discard_state(); - #endif - _SAPP_OBJC_RELEASE(_sapp.macos.window); -} - -// undocumented methods for creating cursors (see GLFW 3.4 and imgui_impl_osx.mm) -@interface NSCursor() -+ (id)_windowResizeNorthWestSouthEastCursor; -+ (id)_windowResizeNorthEastSouthWestCursor; -+ (id)_windowResizeNorthSouthCursor; -+ (id)_windowResizeEastWestCursor; -@end - -_SOKOL_PRIVATE void _sapp_macos_init_cursors(void) { - for (size_t i = 0; i < _SAPP_MOUSECURSOR_NUM; i++) { - _sapp.macos.standard_cursors[i] = nil; - _sapp.macos.custom_cursors[i] = nil; - } - _sapp.macos.standard_cursors[SAPP_MOUSECURSOR_ARROW] = [NSCursor arrowCursor]; - _sapp.macos.standard_cursors[SAPP_MOUSECURSOR_IBEAM] = [NSCursor IBeamCursor]; - _sapp.macos.standard_cursors[SAPP_MOUSECURSOR_CROSSHAIR] = [NSCursor crosshairCursor]; - _sapp.macos.standard_cursors[SAPP_MOUSECURSOR_POINTING_HAND] = [NSCursor pointingHandCursor]; - _sapp.macos.standard_cursors[SAPP_MOUSECURSOR_RESIZE_EW] = [NSCursor respondsToSelector:@selector(_windowResizeEastWestCursor)] ? [NSCursor _windowResizeEastWestCursor] : [NSCursor resizeLeftRightCursor]; - _sapp.macos.standard_cursors[SAPP_MOUSECURSOR_RESIZE_NS] = [NSCursor respondsToSelector:@selector(_windowResizeNorthSouthCursor)] ? [NSCursor _windowResizeNorthSouthCursor] : [NSCursor resizeUpDownCursor]; - _sapp.macos.standard_cursors[SAPP_MOUSECURSOR_RESIZE_NWSE] = [NSCursor respondsToSelector:@selector(_windowResizeNorthWestSouthEastCursor)] ? [NSCursor _windowResizeNorthWestSouthEastCursor] : [NSCursor closedHandCursor]; - _sapp.macos.standard_cursors[SAPP_MOUSECURSOR_RESIZE_NESW] = [NSCursor respondsToSelector:@selector(_windowResizeNorthEastSouthWestCursor)] ? [NSCursor _windowResizeNorthEastSouthWestCursor] : [NSCursor closedHandCursor]; - _sapp.macos.standard_cursors[SAPP_MOUSECURSOR_RESIZE_ALL] = [NSCursor closedHandCursor]; - _sapp.macos.standard_cursors[SAPP_MOUSECURSOR_NOT_ALLOWED] = [NSCursor operationNotAllowedCursor]; -} - -_SOKOL_PRIVATE void _sapp_macos_run(const sapp_desc* desc) { - _sapp_init_state(desc); - _sapp_macos_init_keytable(); - [NSApplication sharedApplication]; - - // set the application dock icon as early as possible, otherwise - // the dummy icon will be visible for a short time - sapp_set_icon(&_sapp.desc.icon); - _sapp.macos.app_dlg = [[_sapp_macos_app_delegate alloc] init]; - NSApp.delegate = _sapp.macos.app_dlg; - - // workaround for "no key-up sent while Cmd is pressed" taken from GLFW: - NSEvent* (^keyup_monitor)(NSEvent*) = ^NSEvent* (NSEvent* event) { - if ([event modifierFlags] & NSEventModifierFlagCommand) { - [[NSApp keyWindow] sendEvent:event]; - } - return event; - }; - _sapp.macos.keyup_monitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskKeyUp handler:keyup_monitor]; - - [NSApp run]; - // NOTE: [NSApp run] never returns, instead cleanup code - // must be put into applicationWillTerminate -} - -/* MacOS entry function */ -#if !defined(SOKOL_NO_ENTRY) -int main(int argc, char* argv[]) { - sapp_desc desc = sokol_main(argc, argv); - _sapp_macos_run(&desc); - return 0; -} -#endif /* SOKOL_NO_ENTRY */ - -_SOKOL_PRIVATE uint32_t _sapp_macos_mods(NSEvent* ev) { - const NSEventModifierFlags f = (ev == nil) ? NSEvent.modifierFlags : ev.modifierFlags; - const NSUInteger b = NSEvent.pressedMouseButtons; - uint32_t m = 0; - if (f & NSEventModifierFlagShift) { - m |= SAPP_MODIFIER_SHIFT; - } - if (f & NSEventModifierFlagControl) { - m |= SAPP_MODIFIER_CTRL; - } - if (f & NSEventModifierFlagOption) { - m |= SAPP_MODIFIER_ALT; - } - if (f & NSEventModifierFlagCommand) { - m |= SAPP_MODIFIER_SUPER; - } - if (0 != (b & (1<<0))) { - m |= SAPP_MODIFIER_LMB; - } - if (0 != (b & (1<<1))) { - m |= SAPP_MODIFIER_RMB; - } - if (0 != (b & (1<<2))) { - m |= SAPP_MODIFIER_MMB; - } - return m; -} - -_SOKOL_PRIVATE void _sapp_macos_mouse_event(sapp_event_type type, sapp_mousebutton btn, uint32_t mod) { - if (_sapp_events_enabled()) { - _sapp_init_event(type); - _sapp.event.mouse_button = btn; - _sapp.event.modifiers = mod; - _sapp_call_event(&_sapp.event); - } -} - -_SOKOL_PRIVATE void _sapp_macos_key_event(sapp_event_type type, sapp_keycode key, bool repeat, uint32_t mod) { - if (_sapp_events_enabled()) { - _sapp_init_event(type); - _sapp.event.key_code = key; - _sapp.event.key_repeat = repeat; - _sapp.event.modifiers = mod; - _sapp_call_event(&_sapp.event); - } -} - -_SOKOL_PRIVATE void _sapp_macos_app_event(sapp_event_type type) { - if (_sapp_events_enabled()) { - _sapp_init_event(type); - _sapp_call_event(&_sapp.event); - } -} - -// called in applicationDidFinishedLaunching when no window size was provided -_SOKOL_PRIVATE void _sapp_macos_init_default_dimensions(void) { - if (_sapp.desc.high_dpi) { - _sapp.dpi_scale = NSScreen.mainScreen.backingScaleFactor; - } else { - _sapp.dpi_scale = 1.0f; - } - NSRect screen_rect = NSScreen.mainScreen.frame; - // use 4/5 of screen size as default size - const float default_widthf = (screen_rect.size.width * 4.0f) / 5.0f; - const float default_heightf = (screen_rect.size.height * 4.0f) / 5.0f; - if (_sapp.window_width == 0) { - _sapp.window_width = _sapp_roundf_gzero(default_widthf); - } - if (_sapp.window_height == 0) { - _sapp.window_height = _sapp_roundf_gzero(default_heightf); - } - _sapp.framebuffer_width = _sapp_roundf_gzero(default_widthf * _sapp.dpi_scale); - _sapp.framebuffer_height = _sapp_roundf_gzero(default_heightf * _sapp.dpi_scale); -} - -/* NOTE: unlike the iOS version of this function, the macOS version - can dynamically update the DPI scaling factor when a window is moved - between HighDPI / LowDPI screens. -*/ -_SOKOL_PRIVATE void _sapp_macos_update_dimensions(void) { - if (_sapp.desc.high_dpi) { - _sapp.dpi_scale = [_sapp.macos.window screen].backingScaleFactor; - } else { - _sapp.dpi_scale = 1.0f; - } - // NOTE: needed because we set layerContentsPlacement to a non-scaling value in windowWillStartLiveResize. - _sapp.macos.view.layer.contentsScale = _sapp.dpi_scale; - const NSRect bounds = [_sapp.macos.view bounds]; - _sapp.window_width = _sapp_roundf_gzero(bounds.size.width); - _sapp.window_height = _sapp_roundf_gzero(bounds.size.height); - #if defined(SOKOL_METAL) - bool dim_changed = _sapp_macos_mtl_update_framebuffer_dimensions(bounds); - #elif defined(SOKOL_GLCORE) - bool dim_changed = _sapp_macos_gl_update_framebuffer_dimensions(bounds); - #elif defined(SOKOL_WGPU) - bool dim_changed = _sapp_macos_wgpu_update_framebuffer_dimensions(bounds); - #endif - if (dim_changed && !_sapp.first_frame) { - _sapp_macos_app_event(SAPP_EVENTTYPE_RESIZED); - } -} - -_SOKOL_PRIVATE void _sapp_macos_toggle_fullscreen(void) { - /* NOTE: the _sapp.fullscreen flag is also notified by the - windowDidEnterFullscreen / windowDidExitFullscreen - event handlers - */ - _sapp.fullscreen = !_sapp.fullscreen; - [_sapp.macos.window toggleFullScreen:nil]; -} - -_SOKOL_PRIVATE void _sapp_macos_set_clipboard_string(const char* str) { - @autoreleasepool { - NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; - [pasteboard declareTypes:@[NSPasteboardTypeString] owner:nil]; - [pasteboard setString:@(str) forType:NSPasteboardTypeString]; - } -} - -_SOKOL_PRIVATE const char* _sapp_macos_get_clipboard_string(void) { - SOKOL_ASSERT(_sapp.clipboard.buffer); - @autoreleasepool { - _sapp.clipboard.buffer[0] = 0; - NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; - if (![[pasteboard types] containsObject:NSPasteboardTypeString]) { - return _sapp.clipboard.buffer; - } - NSString* str = [pasteboard stringForType:NSPasteboardTypeString]; - if (!str) { - return _sapp.clipboard.buffer; - } - _sapp_strcpy([str UTF8String], _sapp.clipboard.buffer, (size_t)_sapp.clipboard.buf_size); - } - return _sapp.clipboard.buffer; -} - -_SOKOL_PRIVATE void _sapp_macos_update_window_title(void) { - [_sapp.macos.window setTitle: [NSString stringWithUTF8String:_sapp.window_title]]; -} - -_SOKOL_PRIVATE void _sapp_macos_mouse_update_from_nspoint(NSPoint mouse_pos, bool clear_dxdy) { - if (!_sapp.mouse.locked) { - float new_x = mouse_pos.x * _sapp.dpi_scale; - float new_y = _sapp.framebuffer_height - (mouse_pos.y * _sapp.dpi_scale) - 1; - if (clear_dxdy) { - _sapp.mouse.dx = 0.0f; - _sapp.mouse.dy = 0.0f; - } else if (_sapp.mouse.pos_valid) { - // don't update dx/dy in the very first update - _sapp.mouse.dx = new_x - _sapp.mouse.x; - _sapp.mouse.dy = new_y - _sapp.mouse.y; - } - _sapp.mouse.x = new_x; - _sapp.mouse.y = new_y; - _sapp.mouse.pos_valid = true; - } -} - -_SOKOL_PRIVATE void _sapp_macos_mouse_update_from_nsevent(NSEvent* event, bool clear_dxdy) { - _sapp_macos_mouse_update_from_nspoint(event.locationInWindow, clear_dxdy); -} - -_SOKOL_PRIVATE void _sapp_macos_show_mouse(bool visible) { - /* NOTE: this function is only called when the mouse visibility actually changes */ - if (visible) { - CGDisplayShowCursor(kCGDirectMainDisplay); - } else { - CGDisplayHideCursor(kCGDirectMainDisplay); - } -} - -_SOKOL_PRIVATE void _sapp_macos_lock_mouse(bool lock) { - if (lock == _sapp.mouse.locked) { - return; - } - _sapp.mouse.dx = 0.0f; - _sapp.mouse.dy = 0.0f; - _sapp.mouse.locked = lock; - /* - NOTE that this code doesn't warp the mouse cursor to the window - center as everybody else does it. This lead to a spike in the - *second* mouse-moved event after the warp happened. The - mouse centering doesn't seem to be required (mouse-moved events - are reported correctly even when the cursor is at an edge of the screen). - - NOTE also that the hide/show of the mouse cursor should properly - stack with calls to sapp_show_mouse() - */ - if (_sapp.mouse.locked) { - CGAssociateMouseAndMouseCursorPosition(NO); - [NSCursor hide]; - } else { - [NSCursor unhide]; - CGAssociateMouseAndMouseCursorPosition(YES); - } -} - -_SOKOL_PRIVATE void _sapp_macos_update_cursor(sapp_mouse_cursor cursor, bool shown) { - // show/hide cursor only if visibility status has changed (required because show/hide stacks) - if (shown != _sapp.mouse.shown) { - if (shown) { - [NSCursor unhide]; - } else { - [NSCursor hide]; - } - } - - // update cursor - SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); - NSCursor* ns_cursor = 0; - if (_sapp.custom_cursor_bound[cursor]) { - SOKOL_ASSERT(_sapp.macos.custom_cursors[cursor]); - ns_cursor = _sapp.macos.custom_cursors[cursor]; - } else if (_sapp.macos.standard_cursors[cursor]) { - ns_cursor = _sapp.macos.standard_cursors[cursor]; - } else { - ns_cursor = [NSCursor arrowCursor]; - } - [ns_cursor set]; -} - -_SOKOL_PRIVATE bool _sapp_macos_make_custom_mouse_cursor(sapp_mouse_cursor cursor, const sapp_image_desc* desc) { - SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); - SOKOL_ASSERT(_sapp.macos.custom_cursors[cursor] == nil); - - // NOTE: see glfw for reference https://github.com/glfw/glfw/blob/ac10768495837eb98da27d01fe706073d6d251c2/src/cocoa_window.m#L1712 - NSBitmapImageRep* rep = [[NSBitmapImageRep alloc] - initWithBitmapDataPlanes:NULL - pixelsWide:desc->width - pixelsHigh:desc->height - bitsPerSample:8 - samplesPerPixel:4 - hasAlpha:YES - isPlanar:NO - colorSpaceName:NSCalibratedRGBColorSpace - bitmapFormat:NSBitmapFormatAlphaNonpremultiplied - bytesPerRow:desc->width * 4 - bitsPerPixel:32]; - if (rep != nil) { - memcpy([rep bitmapData], desc->pixels.ptr, (size_t) (desc->width * desc->height * 4)); - - NSImage* native = [[NSImage alloc] initWithSize:NSMakeSize(desc->width, desc->height)]; - SOKOL_ASSERT(native); - [native addRepresentation:rep]; - - _sapp.macos.custom_cursors[cursor] = [[NSCursor alloc] - initWithImage:native - hotSpot:NSMakePoint(desc->cursor_hotspot_x, desc->cursor_hotspot_y)]; - SOKOL_ASSERT(_sapp.macos.custom_cursors[cursor] != nil); - - _SAPP_OBJC_RELEASE(native); - _SAPP_OBJC_RELEASE(rep); - return true; - } - return false; -} - -_SOKOL_PRIVATE void _sapp_macos_destroy_custom_mouse_cursor(sapp_mouse_cursor cursor) { - SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); - SOKOL_ASSERT(_sapp.macos.custom_cursors[cursor] != nil); - _SAPP_OBJC_RELEASE(_sapp.macos.custom_cursors[cursor]); -} - -_SOKOL_PRIVATE void _sapp_macos_set_icon(const sapp_icon_desc* icon_desc, int num_images) { - NSDockTile* dock_tile = NSApp.dockTile; - const int wanted_width = (int) dock_tile.size.width; - const int wanted_height = (int) dock_tile.size.height; - const int img_index = _sapp_image_bestmatch(icon_desc->images, num_images, wanted_width, wanted_height); - const sapp_image_desc* img_desc = &icon_desc->images[img_index]; - - CGColorSpaceRef cg_color_space = CGColorSpaceCreateDeviceRGB(); - CFDataRef cf_data = CFDataCreate(kCFAllocatorDefault, (const UInt8*)img_desc->pixels.ptr, (CFIndex)img_desc->pixels.size); - CGDataProviderRef cg_data_provider = CGDataProviderCreateWithCFData(cf_data); - CGImageRef cg_img = CGImageCreate( - (size_t)img_desc->width, // width - (size_t)img_desc->height, // height - 8, // bitsPerComponent - 32, // bitsPerPixel - (size_t)img_desc->width * 4,// bytesPerRow - cg_color_space, // space - (CGBitmapInfo)kCGImageAlphaLast | (CGBitmapInfo)kCGImageByteOrderDefault, // bitmapInfo — Modified by tettou771 for TrussC: silence enum-enum-conversion warning under C++20 - cg_data_provider, // provider - NULL, // decode - false, // shouldInterpolate - kCGRenderingIntentDefault); - CFRelease(cf_data); - CGDataProviderRelease(cg_data_provider); - CGColorSpaceRelease(cg_color_space); - - NSImage* ns_image = [[NSImage alloc] initWithCGImage:cg_img size:dock_tile.size]; - dock_tile.contentView = [NSImageView imageViewWithImage:ns_image]; - [dock_tile display]; - _SAPP_OBJC_RELEASE(ns_image); - CGImageRelease(cg_img); -} - -_SOKOL_PRIVATE void _sapp_macos_frame(void) { - _sapp_timing_update(&_sapp.timing, 0.0); - #if defined(SOKOL_METAL) - _sapp_macos_mtl_timing_update(); - #endif - #if defined(_SAPP_ANY_GL) - glGetIntegerv(GL_FRAMEBUFFER_BINDING, (GLint*)&_sapp.gl.framebuffer); - #endif - @autoreleasepool { - #if defined(SOKOL_WGPU) - _sapp_wgpu_frame(); - #else - _sapp_frame(); - #endif - } - #if defined(_SAPP_ANY_GL) - [[_sapp.macos.view openGLContext] flushBuffer]; - #endif - if (_sapp.quit_requested || _sapp.quit_ordered) { - [_sapp.macos.window performClose:nil]; - } -} - -@implementation _sapp_macos_app_delegate -- (void)applicationDidFinishLaunching:(NSNotification*)aNotification { - _SOKOL_UNUSED(aNotification); - // NOTE: keep activationPolicy in front of window creation (see https://github.com/floooh/sokol/issues/1500) - NSApp.activationPolicy = NSApplicationActivationPolicyRegular; - _sapp_macos_init_cursors(); - if ((_sapp.window_width == 0) || (_sapp.window_height == 0)) { - _sapp_macos_init_default_dimensions(); - } - const NSUInteger style = - NSWindowStyleMaskTitled | - NSWindowStyleMaskClosable | - NSWindowStyleMaskMiniaturizable | - NSWindowStyleMaskResizable; - NSRect window_rect = NSMakeRect(0, 0, _sapp.window_width, _sapp.window_height); - _sapp.macos.window = [[_sapp_macos_window alloc] - initWithContentRect:window_rect - styleMask:style - backing:NSBackingStoreBuffered - defer:NO]; - _sapp.macos.window.releasedWhenClosed = NO; // this is necessary for proper cleanup in applicationWillTerminate - _sapp.macos.window.title = [NSString stringWithUTF8String:_sapp.window_title]; - _sapp.macos.window.acceptsMouseMovedEvents = YES; - _sapp.macos.window.restorable = YES; - - _sapp.macos.win_dlg = [[_sapp_macos_window_delegate alloc] init]; - _sapp.macos.window.delegate = _sapp.macos.win_dlg; - #if defined(SOKOL_METAL) - _sapp_macos_mtl_init(); - #elif defined(SOKOL_GLCORE) - _sapp_macos_gl_init(window_rect); - #elif defined(SOKOL_WGPU) - _sapp_macos_wgpu_init(); - #endif - _sapp.macos.window.contentView = _sapp.macos.view; - [_sapp.macos.window makeFirstResponder:_sapp.macos.view]; - [_sapp.macos.window center]; - _sapp.valid = true; - if (_sapp.fullscreen) { - /* ^^^ on GL, this already toggles a rendered frame, so set the valid flag before */ - [_sapp.macos.window toggleFullScreen:self]; - } - [NSApp activateIgnoringOtherApps:YES]; - [_sapp.macos.window makeKeyAndOrderFront:nil]; - _sapp_macos_update_dimensions(); - [NSEvent setMouseCoalescingEnabled:NO]; - - // workaround for window not being focused during a long init callback - // for details see: https://github.com/floooh/sokol/pull/982 - // also see: https://gitlab.gnome.org/GNOME/gtk/-/issues/2342 - NSEvent *focusevent = [NSEvent otherEventWithType:NSEventTypeAppKitDefined - location:NSZeroPoint - modifierFlags:0x40 - timestamp:0 - windowNumber:0 - context:nil - subtype:NSEventSubtypeApplicationActivated - data1:0 - data2:0]; - [NSApp postEvent:focusevent atStart:YES]; -} - -- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)sender { - _SOKOL_UNUSED(sender); - return YES; -} - -- (void)applicationWillTerminate:(NSNotification*)notification { - _SOKOL_UNUSED(notification); - _sapp_call_cleanup(); - _sapp_macos_discard_state(); - _sapp_discard_state(); -} -@end - -@implementation _sapp_macos_window_delegate -- (BOOL)windowShouldClose:(id)sender { - _SOKOL_UNUSED(sender); - // only give user-code a chance to intervene when sapp_quit() wasn't already called - if (!_sapp.quit_ordered) { - // if window should be closed and event handling is enabled, give user code - // a chance to intervene via sapp_cancel_quit() - _sapp.quit_requested = true; - _sapp_macos_app_event(SAPP_EVENTTYPE_QUIT_REQUESTED); - /* user code hasn't intervened, quit the app */ - if (_sapp.quit_requested) { - _sapp.quit_ordered = true; - } - } - if (_sapp.quit_ordered) { - return YES; - } else { - return NO; - } -} - -- (void)windowWillStartLiveResize:(NSNotification *)notification { - #if defined(SOKOL_METAL) || defined(SOKOL_WGPU) - // Work around the MTKView/CAMetalLayer resizing glitch by "anchoring" the layer to the window corner opposite - // to the currently manipulated corner (or edge). This prevents the content stretching back and - // forth during resizing. This is a workaround for this issue: https://github.com/floooh/sokol/issues/700 - // Can be removed if/when migrating to CAMetalLayer: https://github.com/floooh/sokol/issues/727 - bool resizing_from_left = _sapp.mouse.x < _sapp.window_width/2; - bool resizing_from_top = _sapp.mouse.y < _sapp.window_height/2; - NSViewLayerContentsPlacement placement; - if (resizing_from_left) { - placement = resizing_from_top ? NSViewLayerContentsPlacementBottomRight : NSViewLayerContentsPlacementTopRight; - } else { - placement = resizing_from_top ? NSViewLayerContentsPlacementBottomLeft : NSViewLayerContentsPlacementTopLeft; - } - _sapp.macos.view.layerContentsPlacement = placement; - #endif -} - -- (void)windowDidResize:(NSNotification*)notification { - _SOKOL_UNUSED(notification); - _sapp_macos_update_dimensions(); -} - -- (void)windowDidChangeScreen:(NSNotification*)notification { - _SOKOL_UNUSED(notification); - _sapp_macos_update_dimensions(); -} - -- (void)windowDidMiniaturize:(NSNotification*)notification { - _SOKOL_UNUSED(notification); - #if defined(SOKOL_METAL) - _sapp_macos_mtl_transition_to_occluded(); - #endif - _sapp_macos_app_event(SAPP_EVENTTYPE_ICONIFIED); -} - -- (void)windowDidDeminiaturize:(NSNotification*)notification { - _SOKOL_UNUSED(notification); - #if defined(SOKOL_METAL) - _sapp_macos_mtl_transition_to_visible(); - #endif - _sapp_macos_app_event(SAPP_EVENTTYPE_RESTORED); -} - -- (void)windowDidChangeOcclusionState:(NSNotification*)notification { - #if defined(SOKOL_METAL) - if (_sapp.macos.window.occlusionState & NSWindowOcclusionStateVisible) { - _sapp_macos_mtl_transition_to_visible(); - } else { - _sapp_macos_mtl_transition_to_occluded(); - } - #endif -} - -- (void)windowDidBecomeKey:(NSNotification*)notification { - _SOKOL_UNUSED(notification); - _sapp_macos_app_event(SAPP_EVENTTYPE_FOCUSED); -} - -- (void)windowDidResignKey:(NSNotification*)notification { - _SOKOL_UNUSED(notification); - _sapp_macos_app_event(SAPP_EVENTTYPE_UNFOCUSED); -} - -- (void)windowDidEnterFullScreen:(NSNotification*)notification { - _SOKOL_UNUSED(notification); - _sapp.fullscreen = true; -} - -- (void)windowDidExitFullScreen:(NSNotification*)notification { - _SOKOL_UNUSED(notification); - _sapp.fullscreen = false; -} -@end - -@implementation _sapp_macos_window -- (instancetype)initWithContentRect:(NSRect)contentRect - styleMask:(NSWindowStyleMask)style - backing:(NSBackingStoreType)backingStoreType - defer:(BOOL)flag { - if (self = [super initWithContentRect:contentRect styleMask:style backing:backingStoreType defer:flag]) { - #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101300 - [self registerForDraggedTypes:[NSArray arrayWithObject:NSPasteboardTypeFileURL]]; - #endif - } - return self; -} - -- (NSDragOperation)draggingEntered:(id)sender { - return NSDragOperationCopy; -} - -- (NSDragOperation)draggingUpdated:(id)sender { - return NSDragOperationCopy; -} - -- (BOOL)performDragOperation:(id)sender { - #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101300 - NSPasteboard *pboard = [sender draggingPasteboard]; - if ([pboard.types containsObject:NSPasteboardTypeFileURL]) { - _sapp_clear_drop_buffer(); - _sapp.drop.num_files = ((int)pboard.pasteboardItems.count > _sapp.drop.max_files) ? _sapp.drop.max_files : (int)pboard.pasteboardItems.count; - bool drop_failed = false; - for (int i = 0; i < _sapp.drop.num_files; i++) { - NSURL *fileUrl = [NSURL fileURLWithPath:[pboard.pasteboardItems[(NSUInteger)i] stringForType:NSPasteboardTypeFileURL]]; - if (!_sapp_strcpy(fileUrl.standardizedURL.path.UTF8String, _sapp_dropped_file_path_ptr(i), (size_t)_sapp.drop.max_path_length)) { - _SAPP_ERROR(DROPPED_FILE_PATH_TOO_LONG); - drop_failed = true; - break; - } - } - if (!drop_failed) { - if (_sapp_events_enabled()) { - _sapp_macos_mouse_update_from_nspoint(sender.draggingLocation, true); - _sapp_init_event(SAPP_EVENTTYPE_FILES_DROPPED); - _sapp.event.modifiers = _sapp_macos_mods(nil); - _sapp_call_event(&_sapp.event); - } - } else { - _sapp_clear_drop_buffer(); - _sapp.drop.num_files = 0; - } - return YES; - } - #endif - return NO; -} -@end - -@implementation _sapp_macos_view -#if defined(SOKOL_GLCORE) -- (void)timerFired:(id)sender { - _SOKOL_UNUSED(sender); - [self setNeedsDisplay:YES]; -} -- (void)prepareOpenGL { - [super prepareOpenGL]; - GLint swapInt = 1; - NSOpenGLContext* ctx = [_sapp.macos.view openGLContext]; - [ctx setValues:&swapInt forParameter:NSOpenGLContextParameterSwapInterval]; - [ctx makeCurrentContext]; -} -- (void)drawRect:(NSRect)rect { - _SOKOL_UNUSED(rect); - _sapp_macos_frame(); -} -#elif defined(SOKOL_METAL) || defined(SOKOL_WGPU) -- (void)displayLinkFired:(id)sender { - _SOKOL_UNUSED(sender); - _sapp_macos_frame(); -} -- (void)fallbackTimerFired:(NSTimer*)timer { - _SOKOL_UNUSED(timer); - _sapp_macos_frame(); -} -#endif - -- (BOOL)isOpaque { - return YES; -} -- (BOOL)canBecomeKeyView { - return YES; -} -- (BOOL)acceptsFirstResponder { - return YES; -} -- (void)updateTrackingAreas { - if (_sapp.macos.tracking_area != nil) { - [self removeTrackingArea:_sapp.macos.tracking_area]; - _SAPP_OBJC_RELEASE(_sapp.macos.tracking_area); - } - const NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited | - NSTrackingActiveInKeyWindow | - NSTrackingEnabledDuringMouseDrag | - NSTrackingCursorUpdate | - NSTrackingInVisibleRect | - NSTrackingAssumeInside; - _sapp.macos.tracking_area = [[NSTrackingArea alloc] initWithRect:[self bounds] options:options owner:self userInfo:nil]; - [self addTrackingArea:_sapp.macos.tracking_area]; - [super updateTrackingAreas]; -} - -// helper function to make GL context active -static void _sapp_gl_make_current(void) { - #if defined(SOKOL_GLCORE) - [[_sapp.macos.view openGLContext] makeCurrentContext]; - #endif -} - -- (void)mouseEntered:(NSEvent*)event { - _sapp_gl_make_current(); - _sapp_macos_mouse_update_from_nsevent(event, true); - /* don't send mouse enter/leave while dragging (so that it behaves the same as - on Windows while SetCapture is active - */ - if (0 == _sapp.macos.mouse_buttons) { - _sapp_macos_mouse_event(SAPP_EVENTTYPE_MOUSE_ENTER, SAPP_MOUSEBUTTON_INVALID, _sapp_macos_mods(event)); - } -} -- (void)mouseExited:(NSEvent*)event { - _sapp_gl_make_current(); - _sapp_macos_mouse_update_from_nsevent(event, true); - if (0 == _sapp.macos.mouse_buttons) { - _sapp_macos_mouse_event(SAPP_EVENTTYPE_MOUSE_LEAVE, SAPP_MOUSEBUTTON_INVALID, _sapp_macos_mods(event)); - } -} -- (void)mouseDown:(NSEvent*)event { - _sapp_gl_make_current(); - _sapp_macos_mouse_update_from_nsevent(event, false); - _sapp_macos_mouse_event(SAPP_EVENTTYPE_MOUSE_DOWN, SAPP_MOUSEBUTTON_LEFT, _sapp_macos_mods(event)); - _sapp.macos.mouse_buttons |= (1< 0.0f) || (_sapp_absf(dy) > 0.0f)) { - _sapp_init_event(SAPP_EVENTTYPE_MOUSE_SCROLL); - _sapp.event.modifiers = _sapp_macos_mods(event); - _sapp.event.scroll_x = dx; - _sapp.event.scroll_y = dy; - _sapp_call_event(&_sapp.event); - } - } -} -- (void)keyDown:(NSEvent*)event { - if (_sapp_events_enabled()) { - _sapp_gl_make_current(); - const uint32_t mods = _sapp_macos_mods(event); - const sapp_keycode key_code = _sapp_translate_key(event.keyCode); - _sapp_macos_key_event(SAPP_EVENTTYPE_KEY_DOWN, key_code, event.isARepeat, mods); - const NSString* chars = event.characters; - const NSUInteger len = chars.length; - if (len > 0) { - _sapp_init_event(SAPP_EVENTTYPE_CHAR); - _sapp.event.modifiers = mods; - for (NSUInteger i = 0; i < len; i++) { - const unichar codepoint = [chars characterAtIndex:i]; - if ((codepoint & 0xFF00) == 0xF700) { - continue; - } - _sapp.event.char_code = codepoint; - _sapp.event.key_repeat = event.isARepeat; - _sapp_call_event(&_sapp.event); - } - } - /* if this is a Cmd+V (paste), also send a CLIPBOARD_PASTE event */ - if (_sapp.clipboard.enabled && (mods == SAPP_MODIFIER_SUPER) && (key_code == SAPP_KEYCODE_V)) { - _sapp_init_event(SAPP_EVENTTYPE_CLIPBOARD_PASTED); - _sapp_call_event(&_sapp.event); - } - } -} - -- (BOOL)performKeyEquivalent:(NSEvent*)event { - // fixes Ctrl-Tab keydown not triggering a keyDown event - // - // NOTE: it seems that Ctrl-F1 cannot be intercepted the same way, but since - // this enabled critical accessibility features that's probably a good thing. - switch (_sapp_translate_key(event.keyCode)) { - case SAPP_KEYCODE_TAB: - [_sapp.macos.view keyDown:event]; - return YES; - default: - return NO; - } -} - -- (void)keyUp:(NSEvent*)event { - _sapp_gl_make_current(); - _sapp_macos_key_event(SAPP_EVENTTYPE_KEY_UP, - _sapp_translate_key(event.keyCode), - event.isARepeat, - _sapp_macos_mods(event)); -} - -- (void)flagsChanged:(NSEvent*)event { - const uint32_t old_f = _sapp.macos.flags_changed_store; - const uint32_t new_f = (uint32_t)event.modifierFlags; - _sapp.macos.flags_changed_store = new_f; - sapp_keycode key_code = SAPP_KEYCODE_INVALID; - bool down = false; - if ((new_f ^ old_f) & NSEventModifierFlagShift) { - key_code = SAPP_KEYCODE_LEFT_SHIFT; - down = 0 != (new_f & NSEventModifierFlagShift); - } - if ((new_f ^ old_f) & NSEventModifierFlagControl) { - key_code = SAPP_KEYCODE_LEFT_CONTROL; - down = 0 != (new_f & NSEventModifierFlagControl); - } - if ((new_f ^ old_f) & NSEventModifierFlagOption) { - key_code = SAPP_KEYCODE_LEFT_ALT; - down = 0 != (new_f & NSEventModifierFlagOption); - } - if ((new_f ^ old_f) & NSEventModifierFlagCommand) { - key_code = SAPP_KEYCODE_LEFT_SUPER; - down = 0 != (new_f & NSEventModifierFlagCommand); - } - if (key_code != SAPP_KEYCODE_INVALID) { - _sapp_macos_key_event(down ? SAPP_EVENTTYPE_KEY_DOWN : SAPP_EVENTTYPE_KEY_UP, - key_code, - false, - _sapp_macos_mods(event)); - } -} -- (void)cursorUpdate:(NSEvent *)event { - _sapp_macos_update_cursor(_sapp.mouse.current_cursor, _sapp.mouse.shown); -} -@end - -#endif // macOS - -// ██ ██████ ███████ -// ██ ██ ██ ██ -// ██ ██ ██ ███████ -// ██ ██ ██ ██ -// ██ ██████ ███████ -// -// >>ios -#if defined(_SAPP_IOS) - -_SOKOL_PRIVATE NSInteger _sapp_ios_max_fps(void) { - return _sapp.ios.window.windowScene.screen.maximumFramesPerSecond; -} - -#if defined(SOKOL_METAL) - -_SOKOL_PRIVATE id _sapp_ios_mtl_create_texture(int width, int height, MTLPixelFormat fmt, int sample_count, const char* label) { - MTLTextureDescriptor* mtl_desc = [[MTLTextureDescriptor alloc] init]; - if (sample_count > 1) { - mtl_desc.textureType = MTLTextureType2DMultisample; - } else { - mtl_desc.textureType = MTLTextureType2D; - } - mtl_desc.pixelFormat = fmt; - mtl_desc.width = (NSUInteger)width; - mtl_desc.height = (NSUInteger)height; - mtl_desc.depth = 1; - mtl_desc.mipmapLevelCount = 1; - mtl_desc.arrayLength = 1; - mtl_desc.sampleCount = (NSUInteger)sample_count; - mtl_desc.usage = MTLTextureUsageRenderTarget; - mtl_desc.resourceOptions = MTLResourceStorageModePrivate; - id mtl_tex = [_sapp.ios.mtl.device newTextureWithDescriptor:mtl_desc]; - _SAPP_OBJC_RELEASE(mtl_desc); - #if defined(SOKOL_DEBUG) - if (mtl_tex) { - mtl_tex.label = [NSString stringWithUTF8String:label]; - } - #else - _SOKOL_UNUSED(label); - #endif - return mtl_tex; -} - -_SOKOL_PRIVATE void _sapp_ios_mtl_swapchain_create(int width, int height) { - _sapp.ios.mtl.depth_tex =_sapp_ios_mtl_create_texture(width, height, MTLPixelFormatDepth32Float_Stencil8, _sapp.sample_count, "swapchain_depth_tex"); - if (nil == _sapp.ios.mtl.depth_tex) { - _SAPP_PANIC(METAL_CREATE_SWAPCHAIN_DEPTH_TEXTURE_FAILED); - } - if (_sapp.sample_count > 1) { - _sapp.ios.mtl.msaa_tex = _sapp_ios_mtl_create_texture(width, height, MTLPixelFormatRGB10A2Unorm /* Modified by tettou771 for TrussC: 10-bit color */, _sapp.sample_count, "swapchain_msaa_tex"); - if (nil == _sapp.ios.mtl.msaa_tex) { - _SAPP_PANIC(METAL_CREATE_SWAPCHAIN_MSAA_TEXTURE_FAILED); - } - } -} - -_SOKOL_PRIVATE void _sapp_ios_mtl_swapchain_destroy(void) { - if (_sapp.ios.mtl.depth_tex) { - _SAPP_OBJC_RELEASE(_sapp.ios.mtl.depth_tex); - } - if (_sapp.ios.mtl.msaa_tex) { - _SAPP_OBJC_RELEASE(_sapp.ios.mtl.msaa_tex); - } -} - -_SOKOL_PRIVATE void _sapp_ios_mtl_swapchain_resize(int width, int height) { - _sapp_ios_mtl_swapchain_destroy(); - _sapp_ios_mtl_swapchain_create(width, height); -} - -_SOKOL_PRIVATE id _sapp_ios_mtl_swapchain_next(void) { - id drawable = [_sapp.ios.mtl.layer nextDrawable]; - SOKOL_ASSERT(drawable != nil); - return drawable; -} - -_SOKOL_PRIVATE void _sapp_ios_mtl_timing_init(void) { - _sapp.ios.mtl.timing.timestamp = 0.0; - _sapp.ios.mtl.timing.frame_duration_sec = 1.0 / _sapp_ios_max_fps(); -} - -_SOKOL_PRIVATE void _sapp_ios_mtl_timing_update(void) { - const CFTimeInterval cur_timestamp = _sapp.ios.mtl.display_link.timestamp; - // skip first frame (frame_duration had been initialized to display refresh rate) - if (_sapp.ios.mtl.timing.timestamp > 0.0) { - const double dt = cur_timestamp - _sapp.ios.mtl.timing.timestamp; - _sapp.ios.mtl.timing.frame_duration_sec = _sapp_timing_clamp(&_sapp.timing, dt); - } else { - SOKOL_ASSERT(_sapp.ios.mtl.timing.frame_duration_sec > 0.0); - } - _sapp.ios.mtl.timing.timestamp = cur_timestamp; -} - -_SOKOL_PRIVATE double _sapp_ios_mtl_timing_frame_duration(void) { - SOKOL_ASSERT(_sapp.ios.mtl.timing.frame_duration_sec > 0.0); - return _sapp.ios.mtl.timing.frame_duration_sec; -} - -_SOKOL_PRIVATE void _sapp_ios_mtl_start_display_link(void) { - SOKOL_ASSERT(nil == _sapp.ios.mtl.display_link); - SOKOL_ASSERT(nil != _sapp.ios.view); - _sapp.ios.mtl.display_link = [CADisplayLink displayLinkWithTarget:_sapp.ios.view selector:@selector(displayLinkFired:)]; - const float preferred_fps = _sapp_ios_max_fps() / _sapp.swap_interval; - const CAFrameRateRange frame_rate_range = { preferred_fps, preferred_fps, preferred_fps }; - _sapp.ios.mtl.display_link.preferredFrameRateRange = frame_rate_range; - [_sapp.ios.mtl.display_link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; -} - -_SOKOL_PRIVATE void _sapp_ios_mtl_stop_display_link(void) { - if (nil != _sapp.ios.mtl.display_link) { - [_sapp.ios.mtl.display_link invalidate]; - // NOTE: the run-loop held the only string reference to the display link - _sapp.ios.mtl.display_link = nil; - } -} - -_SOKOL_PRIVATE void _sapp_ios_mtl_init(UIWindowScene* windowScene) { - _sapp.ios.mtl.device = MTLCreateSystemDefaultDevice(); - - _sapp.ios.view = [[_sapp_ios_view alloc] initWithFrame:windowScene.screen.bounds]; - _sapp.ios.view.userInteractionEnabled = YES; - #if !defined(_SAPP_TVOS) - _sapp.ios.view.multipleTouchEnabled = YES; - #endif - _sapp.ios.supported_orientations = UIInterfaceOrientationMaskAll; - - _sapp.ios.mtl.layer = [CAMetalLayer layer]; - _sapp.ios.mtl.layer.device = _sapp.ios.mtl.device; - _sapp.ios.mtl.layer.opaque = true; - _sapp.ios.mtl.layer.framebufferOnly = false /* Modified for TrussC: enable captureWindow() reads (issue #56) */; - _sapp.ios.mtl.layer.pixelFormat = MTLPixelFormatRGB10A2Unorm /* Modified by tettou771 for TrussC: 10-bit color */; - _sapp.ios.mtl.layer.frame = _sapp.ios.view.layer.frame; - - [_sapp.ios.view.layer addSublayer:_sapp.ios.mtl.layer]; - - _sapp.ios.supported_orientations = UIInterfaceOrientationMaskAll; - _sapp.ios.view_ctrl = [[_sapp_ios_view_ctrl alloc] init]; - _sapp.ios.view_ctrl.modalPresentationStyle = UIModalPresentationFullScreen; - _sapp.ios.view_ctrl.view = _sapp.ios.view; - _sapp.ios.window.rootViewController = _sapp.ios.view_ctrl; - - _sapp_ios_mtl_start_display_link(); - _sapp_ios_mtl_timing_init(); -} - -_SOKOL_PRIVATE void _sapp_ios_mtl_discard_state(void) { - _sapp_ios_mtl_stop_display_link(); - _sapp_ios_mtl_swapchain_destroy(); - _SAPP_OBJC_RELEASE(_sapp.ios.mtl.layer); - _SAPP_OBJC_RELEASE(_sapp.ios.view_ctrl); - _SAPP_OBJC_RELEASE(_sapp.ios.mtl.device); -} - -_SOKOL_PRIVATE bool _sapp_ios_mtl_update_framebuffer_dimensions(CGRect screen_rect) { - // Modified by tettou771 for TrussC: use actual drawable dimensions for framebuffer size - // to avoid chicken-egg mismatch between sapp_width()/sapp_height() and Metal render pass dimensions. - _sapp.framebuffer_width = _sapp_roundf_gzero(screen_rect.size.width * _sapp.dpi_scale); - _sapp.framebuffer_height = _sapp_roundf_gzero(screen_rect.size.height * _sapp.dpi_scale); - const CGSize cur_size = _sapp.ios.mtl.layer.drawableSize; - const int cur_width = _sapp_roundf_gzero(cur_size.width); - const int cur_height = _sapp_roundf_gzero(cur_size.height); - const bool dim_changed = (_sapp.framebuffer_width != cur_width) || (_sapp.framebuffer_height != cur_height); - if (dim_changed) { - const CGSize drawable_size = { (CGFloat) _sapp.framebuffer_width, (CGFloat) _sapp.framebuffer_height }; - _sapp.ios.mtl.layer.drawableSize = drawable_size; - _sapp.ios.mtl.layer.frame = screen_rect; - _sapp_ios_mtl_swapchain_resize(_sapp.framebuffer_width, _sapp.framebuffer_height); - } - // Always read back actual drawable dimensions to ensure framebuffer_width/height - // matches the Metal drawable (prevents scissor rect exceeding render pass bounds) - const CGSize actual_size = _sapp.ios.mtl.layer.drawableSize; - _sapp.framebuffer_width = _sapp_roundf_gzero(actual_size.width); - _sapp.framebuffer_height = _sapp_roundf_gzero(actual_size.height); - return dim_changed; -} -#endif - -#if defined(SOKOL_GLES3) -_SOKOL_PRIVATE void _sapp_ios_gles3_init(UIWindowScene* windowScene) { - const CGRect screen_rect = windowScene.screen.bounds; - _sapp.ios.eagl_ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3]; - _sapp.ios.view = [[_sapp_ios_view alloc] initWithFrame:screen_rect]; - _sapp.ios.view.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888; - _sapp.ios.view.drawableDepthFormat = GLKViewDrawableDepthFormat24; - _sapp.ios.view.drawableStencilFormat = GLKViewDrawableStencilFormatNone; - GLKViewDrawableMultisample msaa = _sapp.sample_count > 1 ? GLKViewDrawableMultisample4X : GLKViewDrawableMultisampleNone; - _sapp.ios.view.drawableMultisample = msaa; - _sapp.ios.view.context = _sapp.ios.eagl_ctx; - _sapp.ios.view.enableSetNeedsDisplay = NO; - _sapp.ios.view.userInteractionEnabled = YES; - _sapp.ios.view.multipleTouchEnabled = YES; - // on GLKView, contentScaleFactor appears to work just fine! - if (_sapp.desc.high_dpi) { - _sapp.ios.view.contentScaleFactor = _sapp.dpi_scale; - } else { - _sapp.ios.view.contentScaleFactor = 1.0; - } - _sapp.ios.view_ctrl = [[GLKViewController alloc] init]; - _sapp.ios.view_ctrl.view = _sapp.ios.view; - _sapp.ios.view_ctrl.preferredFramesPerSecond = _sapp_ios_max_fps() / _sapp.swap_interval; - _sapp.ios.window.rootViewController = _sapp.ios.view_ctrl; -} - -_SOKOL_PRIVATE void _sapp_ios_gles3_discard_state(void) { - _SAPP_OBJC_RELEASE(_sapp.ios.view_ctrl); - _SAPP_OBJC_RELEASE(_sapp.ios.eagl_ctx); -} - -_SOKOL_PRIVATE bool _sapp_ios_gles3_update_framebuffer_dimensions(CGRect screen_rect) { - _sapp.framebuffer_width = _sapp_roundf_gzero(screen_rect.size.width * _sapp.dpi_scale); - _sapp.framebuffer_height = _sapp_roundf_gzero(screen_rect.size.height * _sapp.dpi_scale); - int cur_fb_width = _sapp_roundf_gzero(_sapp.ios.view.drawableWidth); - int cur_fb_height = _sapp_roundf_gzero(_sapp.ios.view.drawableHeight); - return (_sapp.framebuffer_width != cur_fb_width) || (_sapp.framebuffer_height != cur_fb_height); -} -#endif - -_SOKOL_PRIVATE void _sapp_ios_discard_state(void) { - // NOTE: it's safe to call [release] on a nil object - _SAPP_OBJC_RELEASE(_sapp.ios.textfield_dlg); - _SAPP_OBJC_RELEASE(_sapp.ios.textfield); - #if defined(SOKOL_METAL) - _sapp_ios_mtl_discard_state(); - #else - _sapp_ios_gles3_discard_state(); - #endif - _SAPP_OBJC_RELEASE(_sapp.ios.view); - _SAPP_OBJC_RELEASE(_sapp.ios.window); -} - -_SOKOL_PRIVATE void _sapp_ios_run(const sapp_desc* desc) { - _sapp_init_state(desc); - static int argc = 1; - static char* argv[] = { (char*)"sokol_app" }; - UIApplicationMain(argc, argv, nil, NSStringFromClass([_sapp_scene_delegate class])); -} - -/* iOS entry function */ -#if !defined(SOKOL_NO_ENTRY) -int main(int argc, char* argv[]) { - sapp_desc desc = sokol_main(argc, argv); - _sapp_ios_run(&desc); - return 0; -} -#endif /* SOKOL_NO_ENTRY */ - -_SOKOL_PRIVATE void _sapp_ios_app_event(sapp_event_type type) { - if (_sapp_events_enabled()) { - _sapp_init_event(type); - _sapp_call_event(&_sapp.event); - } -} - -_SOKOL_PRIVATE void _sapp_tvos_press_event(sapp_event_type type, NSSet* presses) { - if (_sapp_events_enabled()) { - for (UIPress *press in presses) { - sapp_keycode key = SAPP_KEYCODE_INVALID; - switch (press.type) { - case UIPressTypeUpArrow: key = SAPP_KEYCODE_UP; break; - case UIPressTypeDownArrow: key = SAPP_KEYCODE_DOWN; break; - case UIPressTypeLeftArrow: key = SAPP_KEYCODE_LEFT; break; - case UIPressTypeRightArrow: key = SAPP_KEYCODE_RIGHT; break; - case UIPressTypeSelect: key = SAPP_KEYCODE_ENTER; break; - case UIPressTypeMenu: key = SAPP_KEYCODE_MENU; break; - case UIPressTypePlayPause: key = SAPP_KEYCODE_PAUSE; break; - default: break; - } - if (key != SAPP_KEYCODE_INVALID) { - _sapp_init_event(type); - _sapp.event.key_code = key; - _sapp.event.key_repeat = false; - _sapp.event.modifiers = 0; - _sapp_call_event(&_sapp.event); - } - } - } -} - -_SOKOL_PRIVATE void _sapp_ios_touch_event(sapp_event_type type, NSSet* touches, UIEvent* event) { - if (_sapp_events_enabled()) { - _sapp_init_event(type); - NSEnumerator* enumerator = event.allTouches.objectEnumerator; - UITouch* ios_touch; - while ((ios_touch = [enumerator nextObject])) { - if ((_sapp.event.num_touches + 1) < SAPP_MAX_TOUCHPOINTS) { - CGPoint ios_pos = [ios_touch locationInView:_sapp.ios.view]; - sapp_touchpoint* cur_point = &_sapp.event.touches[_sapp.event.num_touches++]; - cur_point->identifier = (uintptr_t) ios_touch; - cur_point->pos_x = ios_pos.x * _sapp.dpi_scale; - cur_point->pos_y = ios_pos.y * _sapp.dpi_scale; - cur_point->changed = [touches containsObject:ios_touch]; - } - } - if (_sapp.event.num_touches > 0) { - _sapp_call_event(&_sapp.event); - } - } -} - -_SOKOL_PRIVATE void _sapp_ios_update_dimensions(void) { - // Modified by tettou771 for TrussC: use view bounds instead of UIScreen.mainScreen.bounds - // (consistent with macOS which uses [view bounds], avoids potential timing issues - // with screen bounds not matching the view's actual layout) - CGRect view_rect = _sapp.ios.view.bounds; - if (view_rect.size.width < 1.0 || view_rect.size.height < 1.0) { - // View not laid out yet, fall back to screen bounds - view_rect = _sapp.ios.window.windowScene.screen.bounds; - } - _sapp.window_width = _sapp_roundf_gzero(view_rect.size.width); - _sapp.window_height = _sapp_roundf_gzero(view_rect.size.height); - #if defined(SOKOL_METAL) - bool dim_changed = _sapp_ios_mtl_update_framebuffer_dimensions(view_rect); - #else - bool dim_changed = _sapp_ios_gles3_update_framebuffer_dimensions(view_rect); - #endif - if (dim_changed && !_sapp.first_frame) { - _sapp_ios_app_event(SAPP_EVENTTYPE_RESIZED); - } -} - -_SOKOL_PRIVATE void _sapp_ios_frame(void) { - _sapp_timing_update(&_sapp.timing, 0.0); - #if defined(SOKOL_METAL) - _sapp_ios_mtl_timing_update(); - #endif - #if defined(_SAPP_ANY_GL) - glGetIntegerv(GL_FRAMEBUFFER_BINDING, (GLint*)&_sapp.gl.framebuffer); - #endif - @autoreleasepool { - _sapp_ios_update_dimensions(); - _sapp_frame(); - } -} - -_SOKOL_PRIVATE void _sapp_ios_show_keyboard(bool shown) { - /* if not happened yet, create an invisible text field */ - if (nil == _sapp.ios.textfield) { - _sapp.ios.textfield_dlg = [[_sapp_textfield_dlg alloc] init]; - _sapp.ios.textfield = [[UITextField alloc] initWithFrame:CGRectMake(10, 10, 100, 50)]; - _sapp.ios.textfield.keyboardType = UIKeyboardTypeDefault; - _sapp.ios.textfield.returnKeyType = UIReturnKeyDefault; - _sapp.ios.textfield.autocapitalizationType = UITextAutocapitalizationTypeNone; - _sapp.ios.textfield.autocorrectionType = UITextAutocorrectionTypeNo; - _sapp.ios.textfield.spellCheckingType = UITextSpellCheckingTypeNo; - _sapp.ios.textfield.hidden = YES; - _sapp.ios.textfield.text = @"x"; - _sapp.ios.textfield.delegate = _sapp.ios.textfield_dlg; - [_sapp.ios.view_ctrl.view addSubview:_sapp.ios.textfield]; - -#if !defined(_SAPP_TVOS) - [[NSNotificationCenter defaultCenter] addObserver:_sapp.ios.textfield_dlg - selector:@selector(keyboardWasShown:) - name:UIKeyboardDidShowNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:_sapp.ios.textfield_dlg - selector:@selector(keyboardWillBeHidden:) - name:UIKeyboardWillHideNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:_sapp.ios.textfield_dlg - selector:@selector(keyboardDidChangeFrame:) - name:UIKeyboardDidChangeFrameNotification object:nil]; -#endif - } - if (shown) { - // setting the text field as first responder brings up the onscreen keyboard - [_sapp.ios.textfield becomeFirstResponder]; - } else { - [_sapp.ios.textfield resignFirstResponder]; - } -} - -@implementation _sapp_scene_delegate -- (UISceneConfiguration*) application:(UIApplication*)application - configurationForConnectingSceneSession:(UISceneSession*)connectingSceneSession - options:(UISceneConnectionOptions*)options -{ - UISceneConfiguration* config = [[UISceneConfiguration alloc] initWithName:@"SokolSceneConfiguration" sessionRole:connectingSceneSession.role]; - config.delegateClass = [_sapp_scene_delegate class]; - return config; -} - -- (void)scene:(UIScene*)scene willConnectToSession:(UISceneSession*)session options:(UISceneConnectionOptions*)connectionOptions { - UIWindowScene* windowScene = (UIWindowScene*)scene; - CGRect screen_rect = windowScene.screen.bounds; - _sapp.ios.window = [[UIWindow alloc] initWithWindowScene:windowScene]; - _sapp.window_width = _sapp_roundf_gzero(screen_rect.size.width); - _sapp.window_height = _sapp_roundf_gzero(screen_rect.size.height); - if (_sapp.desc.high_dpi) { - _sapp.dpi_scale = (float) windowScene.screen.nativeScale; - } else { - _sapp.dpi_scale = 1.0f; - } - _sapp.framebuffer_width = _sapp_roundf_gzero(_sapp.window_width * _sapp.dpi_scale); - _sapp.framebuffer_height = _sapp_roundf_gzero(_sapp.window_height * _sapp.dpi_scale); - #if defined(SOKOL_METAL) - _sapp_ios_mtl_init(windowScene); - #else - _sapp_ios_gles3_init(windowScene); - #endif - [_sapp.ios.window makeKeyAndVisible]; - _sapp.valid = true; -} - -- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions { - return YES; -} - -- (void)sceneWillResignActive:(UIScene*)scene { - if (!_sapp.ios.suspended) { - _sapp.ios.suspended = true; - #if defined(SOKOL_METAL) - if (nil != _sapp.ios.mtl.display_link) { - _sapp.ios.mtl.display_link.paused = YES; - } - #endif - _sapp_ios_app_event(SAPP_EVENTTYPE_SUSPENDED); - } -} - -- (void)sceneDidBecomeActive:(UIScene*)scene { - if (_sapp.ios.suspended) { - _sapp.ios.suspended = false; - #if defined(SOKOL_METAL) - if (nil != _sapp.ios.mtl.display_link) { - _sapp.ios.mtl.display_link.paused = NO; - } - #endif - _sapp_ios_app_event(SAPP_EVENTTYPE_RESUMED); - } -} - -/* NOTE: this method will rarely ever be called, iOS application - which are terminated by the user are usually killed via signal 9 - by the operating system. -*/ -- (void)applicationWillTerminate:(UIApplication *)application { - _SOKOL_UNUSED(application); - _sapp_call_cleanup(); - _sapp_ios_discard_state(); - _sapp_discard_state(); -} -@end - -@implementation _sapp_textfield_dlg -- (void)keyboardWasShown:(NSNotification*)notif { - _sapp.onscreen_keyboard_shown = true; - /* query the keyboard's size, and modify the content view's size */ -#if !defined(_SAPP_TVOS) - if (_sapp.desc.ios.keyboard_resizes_canvas) { - NSDictionary* info = notif.userInfo; - CGFloat kbd_h = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height; - CGRect view_frame = _sapp.ios.window.windowScene.screen.bounds; - view_frame.size.height -= kbd_h; - _sapp.ios.view.frame = view_frame; - } -#endif -} -- (void)keyboardWillBeHidden:(NSNotification*)notif { - _sapp.onscreen_keyboard_shown = false; - if (_sapp.desc.ios.keyboard_resizes_canvas) { - _sapp.ios.view.frame = _sapp.ios.window.windowScene.screen.bounds; - } -} -- (void)keyboardDidChangeFrame:(NSNotification*)notif { - /* this is for the case when the screen rotation changes while the keyboard is open */ -#if !defined(_SAPP_TVOS) - if (_sapp.onscreen_keyboard_shown && _sapp.desc.ios.keyboard_resizes_canvas) { - NSDictionary* info = notif.userInfo; - CGFloat kbd_h = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height; - CGRect view_frame = _sapp.ios.window.windowScene.screen.bounds; - view_frame.size.height -= kbd_h; - _sapp.ios.view.frame = view_frame; - } -#endif -} -- (BOOL)textField:(UITextField*)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString*)string { - if (_sapp_events_enabled()) { - const NSUInteger len = string.length; - if (len > 0) { - for (NSUInteger i = 0; i < len; i++) { - unichar c = [string characterAtIndex:i]; - if (c >= 32) { - /* ignore surrogates for now */ - if ((c < 0xD800) || (c > 0xDFFF)) { - _sapp_init_event(SAPP_EVENTTYPE_CHAR); - _sapp.event.char_code = c; - _sapp_call_event(&_sapp.event); - } - } - if (c <= 32) { - sapp_keycode k = SAPP_KEYCODE_INVALID; - switch (c) { - case 10: k = SAPP_KEYCODE_ENTER; break; - case 32: k = SAPP_KEYCODE_SPACE; break; - default: break; - } - if (k != SAPP_KEYCODE_INVALID) { - _sapp_init_event(SAPP_EVENTTYPE_KEY_DOWN); - _sapp.event.key_code = k; - _sapp_call_event(&_sapp.event); - _sapp_init_event(SAPP_EVENTTYPE_KEY_UP); - _sapp.event.key_code = k; - _sapp_call_event(&_sapp.event); - } - } - } - } else { - // this was a backspace - _sapp_init_event(SAPP_EVENTTYPE_KEY_DOWN); - _sapp.event.key_code = SAPP_KEYCODE_BACKSPACE; - _sapp_call_event(&_sapp.event); - _sapp_init_event(SAPP_EVENTTYPE_KEY_UP); - _sapp.event.key_code = SAPP_KEYCODE_BACKSPACE; - _sapp_call_event(&_sapp.event); - } - } - return NO; -} -@end - -@implementation _sapp_ios_view -#if defined(SOKOL_METAL) -- (void)displayLinkFired:(id)sender { - _SOKOL_UNUSED(sender); - _sapp_ios_frame(); -} -#else -- (void)drawRect:(CGRect)rect { - _SOKOL_UNUSED(rect); - _sapp_ios_frame(); -} -#endif - -- (BOOL)isOpaque { - return YES; -} -- (void)pressesBegan:(NSSet *)presses withEvent:(UIPressesEvent *)event { - _sapp_tvos_press_event(SAPP_EVENTTYPE_KEY_DOWN, presses); -} -- (void)pressesChanged:(NSSet *)presses withEvent:(UIPressesEvent *)event { -} -- (void)pressesEnded:(NSSet *)presses withEvent:(UIPressesEvent *)event { - _sapp_tvos_press_event(SAPP_EVENTTYPE_KEY_UP, presses); -} -- (void)pressesCancelled:(NSSet *)presses withEvent:(UIPressesEvent *)event { - _sapp_tvos_press_event(SAPP_EVENTTYPE_KEY_UP, presses); -} -- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent*)event { - _sapp_ios_touch_event(SAPP_EVENTTYPE_TOUCHES_BEGAN, touches, event); -} -- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent*)event { - _sapp_ios_touch_event(SAPP_EVENTTYPE_TOUCHES_MOVED, touches, event); -} -- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent*)event { - _sapp_ios_touch_event(SAPP_EVENTTYPE_TOUCHES_ENDED, touches, event); -} -- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent*)event { - _sapp_ios_touch_event(SAPP_EVENTTYPE_TOUCHES_CANCELLED, touches, event); -} -@end - -// Modified by tettou771 for TrussC: custom view controller for runtime orientation + immersive mode -// Modified by tettou771 for TrussC: non-static so tcPlatform_ios.mm can access -bool _sapp_ios_immersive_mode = false; -@implementation _sapp_ios_view_ctrl -- (UIInterfaceOrientationMask)supportedInterfaceOrientations { - return _sapp.ios.supported_orientations; -} -- (BOOL)prefersStatusBarHidden { - return _sapp_ios_immersive_mode; -} -- (BOOL)prefersHomeIndicatorAutoHidden { - return _sapp_ios_immersive_mode; -} -@end - -#endif /* TARGET_OS_IPHONE */ - -#endif /* _SAPP_APPLE */ - -// ███████ ███ ███ ███████ ██████ ██████ ██ ██████ ████████ ███████ ███ ██ -// ██ ████ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ -// █████ ██ ████ ██ ███████ ██ ██████ ██ ██████ ██ █████ ██ ██ ██ -// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ███████ ██ ██ ███████ ██████ ██ ██ ██ ██ ██ ███████ ██ ████ -// -// >>emscripten -#if defined(_SAPP_EMSCRIPTEN) - -#if defined(EM_JS_DEPS) -EM_JS_DEPS(sokol_app, "$withStackSave,$stringToUTF8OnStack,$findCanvasEventTarget") -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -typedef void (*_sapp_html5_fetch_callback) (const sapp_html5_fetch_response*); - -EMSCRIPTEN_KEEPALIVE void _sapp_emsc_onpaste(const char* str) { - if (_sapp.clipboard.enabled) { - _sapp_strcpy(str, _sapp.clipboard.buffer, (size_t)_sapp.clipboard.buf_size); - if (_sapp_events_enabled()) { - _sapp_init_event(SAPP_EVENTTYPE_CLIPBOARD_PASTED); - _sapp_call_event(&_sapp.event); - } - } -} - -/* https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload */ -EMSCRIPTEN_KEEPALIVE int _sapp_html5_get_ask_leave_site(void) { - return _sapp.html5_ask_leave_site ? 1 : 0; -} - -EMSCRIPTEN_KEEPALIVE void _sapp_emsc_begin_drop(int num) { - if (!_sapp.drop.enabled) { - return; - } - if (num < 0) { - num = 0; - } - if (num > _sapp.drop.max_files) { - num = _sapp.drop.max_files; - } - _sapp.drop.num_files = num; - _sapp_clear_drop_buffer(); -} - -EMSCRIPTEN_KEEPALIVE void _sapp_emsc_drop(int i, const char* name) { - /* NOTE: name is only the filename part, not a path */ - if (!_sapp.drop.enabled) { - return; - } - if (0 == name) { - return; - } - SOKOL_ASSERT(_sapp.drop.num_files <= _sapp.drop.max_files); - if ((i < 0) || (i >= _sapp.drop.num_files)) { - return; - } - if (!_sapp_strcpy(name, _sapp_dropped_file_path_ptr(i), (size_t)_sapp.drop.max_path_length)) { - _SAPP_ERROR(DROPPED_FILE_PATH_TOO_LONG); - _sapp.drop.num_files = 0; - } -} - -EMSCRIPTEN_KEEPALIVE void _sapp_emsc_end_drop(int x, int y, int mods) { - if (!_sapp.drop.enabled) { - return; - } - if (0 == _sapp.drop.num_files) { - /* there was an error copying the filenames */ - _sapp_clear_drop_buffer(); - return; - - } - if (_sapp_events_enabled()) { - _sapp.mouse.x = (float)x * _sapp.dpi_scale; - _sapp.mouse.y = (float)y * _sapp.dpi_scale; - _sapp.mouse.dx = 0.0f; - _sapp.mouse.dy = 0.0f; - _sapp_init_event(SAPP_EVENTTYPE_FILES_DROPPED); - // see sapp_js_add_dragndrop_listeners for mods constants - if (mods & 1) { _sapp.event.modifiers |= SAPP_MODIFIER_SHIFT; } - if (mods & 2) { _sapp.event.modifiers |= SAPP_MODIFIER_CTRL; } - if (mods & 4) { _sapp.event.modifiers |= SAPP_MODIFIER_ALT; } - if (mods & 8) { _sapp.event.modifiers |= SAPP_MODIFIER_SUPER; } - _sapp_call_event(&_sapp.event); - } -} - -EMSCRIPTEN_KEEPALIVE void _sapp_emsc_invoke_fetch_cb(int index, int success, int error_code, _sapp_html5_fetch_callback callback, uint32_t fetched_size, void* buf_ptr, uint32_t buf_size, void* user_data) { - _SAPP_STRUCT(sapp_html5_fetch_response, response); - response.succeeded = (0 != success); - response.error_code = (sapp_html5_fetch_error) error_code; - response.file_index = index; - response.data.ptr = buf_ptr; - response.data.size = fetched_size; - response.buffer.ptr = buf_ptr; - response.buffer.size = buf_size; - response.user_data = user_data; - callback(&response); -} - -// will be called after the request/exitFullscreen promise rejects -// to restore the _sapp.fullscreen flag to the actual fullscreen state -EMSCRIPTEN_KEEPALIVE void _sapp_emsc_set_fullscreen_flag(int f) { - _sapp.fullscreen = (bool)f; -} - -#ifdef __cplusplus -} /* extern "C" */ -#endif - -EM_JS(void, sapp_js_add_beforeunload_listener, (void), { - Module.sokol_beforeunload = (event) => { - if (__sapp_html5_get_ask_leave_site() != 0) { - event.preventDefault(); - event.returnValue = ' '; - } - }; - window.addEventListener('beforeunload', Module.sokol_beforeunload); -}) - -EM_JS(void, sapp_js_remove_beforeunload_listener, (void), { - window.removeEventListener('beforeunload', Module.sokol_beforeunload); -}) - -EM_JS(void, sapp_js_add_clipboard_listener, (void), { - Module.sokol_paste = (event) => { - const pasted_str = event.clipboardData.getData('text'); - withStackSave(() => { - const cstr = stringToUTF8OnStack(pasted_str); - __sapp_emsc_onpaste(cstr); - }); - }; - window.addEventListener('paste', Module.sokol_paste); -}) - -EM_JS(void, sapp_js_remove_clipboard_listener, (void), { - window.removeEventListener('paste', Module.sokol_paste); -}) - -EM_JS(void, sapp_js_write_clipboard, (const char* c_str), { - const str = UTF8ToString(c_str); - const ta = document.createElement('textarea'); - ta.setAttribute('autocomplete', 'off'); - ta.setAttribute('autocorrect', 'off'); - ta.setAttribute('autocapitalize', 'off'); - ta.setAttribute('spellcheck', 'false'); - ta.style.left = -100 + 'px'; - ta.style.top = -100 + 'px'; - ta.style.height = 1; - ta.style.width = 1; - ta.value = str; - document.body.appendChild(ta); - ta.select(); - document.execCommand('copy'); - document.body.removeChild(ta); -}) - -_SOKOL_PRIVATE void _sapp_emsc_set_clipboard_string(const char* str) { - sapp_js_write_clipboard(str); -} - -EM_JS(void, sapp_js_add_dragndrop_listeners, (void), { - Module.sokol_drop_files = []; - Module.sokol_dragenter = (event) => { - event.stopPropagation(); - event.preventDefault(); - }; - Module.sokol_dragleave = (event) => { - event.stopPropagation(); - event.preventDefault(); - }; - Module.sokol_dragover = (event) => { - event.stopPropagation(); - event.preventDefault(); - }; - Module.sokol_drop = (event) => { - event.stopPropagation(); - event.preventDefault(); - const files = event.dataTransfer.files; - Module.sokol_dropped_files = files; - __sapp_emsc_begin_drop(files.length); - for (let i = 0; i < files.length; i++) { - withStackSave(() => { - const cstr = stringToUTF8OnStack(files[i].name); - __sapp_emsc_drop(i, cstr); - }); - } - let mods = 0; - if (event.shiftKey) { mods |= 1; } - if (event.ctrlKey) { mods |= 2; } - if (event.altKey) { mods |= 4; } - if (event.metaKey) { mods |= 8; } - // FIXME? see computation of targetX/targetY in emscripten via getClientBoundingRect - __sapp_emsc_end_drop(event.clientX, event.clientY, mods); - }; - \x2F\x2A\x2A @suppress {missingProperties} \x2A\x2F - const canvas = Module.sapp_emsc_target; - canvas.addEventListener('dragenter', Module.sokol_dragenter, false); - canvas.addEventListener('dragleave', Module.sokol_dragleave, false); - canvas.addEventListener('dragover', Module.sokol_dragover, false); - canvas.addEventListener('drop', Module.sokol_drop, false); -}) - -EM_JS(uint32_t, sapp_js_dropped_file_size, (int index), { - \x2F\x2A\x2A @suppress {missingProperties} \x2A\x2F - const files = Module.sokol_dropped_files; - if ((index < 0) || (index >= files.length)) { - return 0; - } else { - return files[index].size; - } -}) - -EM_JS(void, sapp_js_fetch_dropped_file, (int index, _sapp_html5_fetch_callback callback, void* buf_ptr, uint32_t buf_size, void* user_data), { - const reader = new FileReader(); - reader.onload = (loadEvent) => { - const content = loadEvent.target.result; - if (content.byteLength > buf_size) { - // SAPP_HTML5_FETCH_ERROR_BUFFER_TOO_SMALL - __sapp_emsc_invoke_fetch_cb(index, 0, 1, callback, 0, buf_ptr, buf_size, user_data); - } else { - HEAPU8.set(new Uint8Array(content), buf_ptr); - __sapp_emsc_invoke_fetch_cb(index, 1, 0, callback, content.byteLength, buf_ptr, buf_size, user_data); - } - }; - reader.onerror = () => { - // SAPP_HTML5_FETCH_ERROR_OTHER - __sapp_emsc_invoke_fetch_cb(index, 0, 2, callback, 0, buf_ptr, buf_size, user_data); - }; - \x2F\x2A\x2A @suppress {missingProperties} \x2A\x2F - const files = Module.sokol_dropped_files; - reader.readAsArrayBuffer(files[index]); -}) - -EM_JS(void, sapp_js_remove_dragndrop_listeners, (void), { - \x2F\x2A\x2A @suppress {missingProperties} \x2A\x2F - const canvas = Module.sapp_emsc_target; - canvas.removeEventListener('dragenter', Module.sokol_dragenter); - canvas.removeEventListener('dragleave', Module.sokol_dragleave); - canvas.removeEventListener('dragover', Module.sokol_dragover); - canvas.removeEventListener('drop', Module.sokol_drop); -}) - -EM_JS(void, sapp_js_init, (const char* c_str_target_selector, const char* c_str_document_title), { - if (c_str_document_title !== 0) { - document.title = UTF8ToString(c_str_document_title); - } - const target_selector_str = UTF8ToString(c_str_target_selector); - if (Module['canvas'] !== undefined) { - if (typeof Module['canvas'] === 'object') { - specialHTMLTargets[target_selector_str] = Module['canvas']; - } else { - console.warn("sokol_app.h: Module['canvas'] is set but is not an object"); - } - } - Module.sapp_emsc_target = findCanvasEventTarget(target_selector_str); - if (!Module.sapp_emsc_target) { - console.warn("sokol_app.h: can't find html5_canvas_selector ", target_selector_str); - } - if (!Module.sapp_emsc_target.requestPointerLock) { - console.warn("sokol_app.h: target doesn't support requestPointerLock: ", target_selector_str); - } -}) - -_SOKOL_PRIVATE EM_BOOL _sapp_emsc_pointerlockchange_cb(int emsc_type, const EmscriptenPointerlockChangeEvent* emsc_event, void* user_data) { - _SOKOL_UNUSED(emsc_type); - _SOKOL_UNUSED(user_data); - _sapp.mouse.locked = emsc_event->isActive; - return EM_TRUE; -} - -_SOKOL_PRIVATE EM_BOOL _sapp_emsc_pointerlockerror_cb(int emsc_type, const void* reserved, void* user_data) { - _SOKOL_UNUSED(emsc_type); - _SOKOL_UNUSED(reserved); - _SOKOL_UNUSED(user_data); - _sapp.mouse.locked = false; - _sapp.emsc.mouse_lock_requested = false; - return true; -} - -EM_JS(void, sapp_js_request_pointerlock, (void), { - if (Module.sapp_emsc_target) { - if (Module.sapp_emsc_target.requestPointerLock) { - Module.sapp_emsc_target.requestPointerLock(); - } - } -}) - -EM_JS(void, sapp_js_exit_pointerlock, (void), { - if (document.exitPointerLock) { - document.exitPointerLock(); - } -}) - -_SOKOL_PRIVATE void _sapp_emsc_lock_mouse(bool lock) { - if (lock) { - /* request mouse-lock during event handler invocation (see _sapp_emsc_update_mouse_lock_state) */ - _sapp.emsc.mouse_lock_requested = true; - } else { - /* NOTE: the _sapp.mouse_locked state will be set in the pointerlockchange callback */ - _sapp.emsc.mouse_lock_requested = false; - sapp_js_exit_pointerlock(); - } -} - -/* called from inside event handlers to check if mouse lock had been requested, - and if yes, actually enter mouse lock. -*/ -_SOKOL_PRIVATE void _sapp_emsc_update_mouse_lock_state(void) { - if (_sapp.emsc.mouse_lock_requested) { - _sapp.emsc.mouse_lock_requested = false; - sapp_js_request_pointerlock(); - } -} - -// set mouse cursor type -EM_JS(void, sapp_js_set_cursor, (int cursor_type, int shown, int use_custom_cursor_image), { - if (Module.sapp_emsc_target) { - let cursor; - if (shown === 0) { - cursor = "none"; - } else if (use_custom_cursor_image != 0) { - cursor = Module.__sapp_custom_cursors[cursor_type].css_property; - } else switch (cursor_type) { - case 0: cursor = "auto"; break; // SAPP_MOUSECURSOR_DEFAULT - case 1: cursor = "default"; break; // SAPP_MOUSECURSOR_ARROW - case 2: cursor = "text"; break; // SAPP_MOUSECURSOR_IBEAM - case 3: cursor = "crosshair"; break; // SAPP_MOUSECURSOR_CROSSHAIR - case 4: cursor = "pointer"; break; // SAPP_MOUSECURSOR_POINTING_HAND - case 5: cursor = "ew-resize"; break; // SAPP_MOUSECURSOR_RESIZE_EW - case 6: cursor = "ns-resize"; break; // SAPP_MOUSECURSOR_RESIZE_NS - case 7: cursor = "nwse-resize"; break; // SAPP_MOUSECURSOR_RESIZE_NWSE - case 8: cursor = "nesw-resize"; break; // SAPP_MOUSECURSOR_RESIZE_NESW - case 9: cursor = "all-scroll"; break; // SAPP_MOUSECURSOR_RESIZE_ALL - case 10: cursor = "not-allowed"; break; // SAPP_MOUSECURSOR_NOT_ALLOWED - default: cursor = "auto"; break; - } - Module.sapp_emsc_target.style.cursor = cursor; - } -}) - -_SOKOL_PRIVATE void _sapp_emsc_update_cursor(sapp_mouse_cursor cursor, bool shown) { - SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); - bool custom_cursor = _sapp.custom_cursor_bound[cursor]; - sapp_js_set_cursor((int)cursor, shown ? 1 : 0, custom_cursor ? 1 : 0); -} - -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdollar-in-identifier-extension" -EM_JS(void, sapp_js_make_custom_mouse_cursor, (int cursor_slot_idx, int width, int height, const void* pixels_ptr, int hotspot_x, int hotspot_y), { - // encode the cursor pixels into a BMP which then is encoded into an 'object url' - const bmp_hdr_size = 14; - const dib_hdr_size = 124; // common values are 56, I saw 124 for the rgba32-1.bmp file of the test suite included in firefox, and 108 from wikipedia example 2 (transparent) - const pixels_size = width * height * 4; - const bmp_size = bmp_hdr_size + dib_hdr_size + pixels_size; - const bmp = new Uint8Array(bmp_size); - let idx = 0; - const w8 = (val) => { - bmp[idx++] = val & 255; - }; - const w16 = (val) => { - bmp[idx++] = val & 255; - bmp[idx++] = (val >> 8) & 255; - }; - const w32 = (val) => { - bmp[idx++] = val & 255; - bmp[idx++] = (val >> 8) & 255; - bmp[idx++] = (val >> 16) & 255; - bmp[idx++] = (val >> 24) & 255; - }; - - // bmp file header - w8(66); // 'B' - w8(77); // 'M' - w32(bmp_size); - w32(0); // reserved - w32(bmp_hdr_size + dib_hdr_size); // offset to pixel data - assert(idx == bmp_hdr_size); - - // DIB header - w32(dib_hdr_size); // header size - w32(width); - w32(height); - w16(1); // planes - w16(32); // bits per pixel - w32(3); // compression method. 3 = BI_BITFIELDS - w32(pixels_size); // image size - w32(2835); // pixel per metre horizontal - w32(2835); // pixel per metre vertical - w32(0); // colors number - w32(0); // important colors - w32(0x000000ff); // red channel bit mask (big endian) - w32(0x0000ff00); // green channel bit mask (big endian) - w32(0x00ff0000); // blue channel bit mask (big endian) - w32(0xff000000); // alpha channel bit mask (big endian) - w8(66); w8(71); w8(82); w8(115); // color space type: 'sRGB' - idx += 64; // color space stuff, unused for 'Win ' or 'sRGB' - assert(idx == bmp_hdr_size + dib_hdr_size); - const row_pitch = width * 4; - for (let y = 0; y < height; y++) { - const src_idx = pixels_ptr + y * row_pitch; - const dst_idx = idx + (height - y - 1) * row_pitch; - const row_data = HEAPU8.slice(src_idx, src_idx + row_pitch); - bmp.set(row_data, dst_idx); - } - const blob = new Blob([bmp.buffer], { type: 'image/bmp' }); - const url = URL.createObjectURL(blob); - - const cursor_slot = { - css_property: `url('${url}') ${hotspot_x} ${hotspot_y}, auto`, - blob_url: url // so we can release it later - }; - - // Store a reference to the js cursor object in a global table, indexed by its sapp_mouse_cursor - if (!Module.__sapp_custom_cursors) { - Module.__sapp_custom_cursors = Array().fill(null); - } - Module.__sapp_custom_cursors[cursor_slot_idx] = cursor_slot; -}) - -#pragma GCC diagnostic pop -EM_JS(void, sapp_js_destroy_custom_mouse_cursor, (int cursor_slot_idx), { - if (Module.__sapp_custom_cursors) { - const cursor = Module.__sapp_custom_cursors[cursor_slot_idx]; - URL.revokeObjectURL(cursor.blob_url); // release the url, which should allow the blob to be garbage collected. - Module.__sapp_custom_cursors[cursor_slot_idx] = null; // clear this array entry - } -}) - -_SOKOL_PRIVATE bool _sapp_emsc_make_custom_mouse_cursor(sapp_mouse_cursor cursor, const sapp_image_desc* desc) { - sapp_js_make_custom_mouse_cursor((int)cursor, desc->width, desc->height, desc->pixels.ptr, desc->cursor_hotspot_x, desc->cursor_hotspot_y); - return true; -} - -_SOKOL_PRIVATE void _sapp_emsc_destroy_custom_mouse_cursor(sapp_mouse_cursor cursor) { - sapp_js_destroy_custom_mouse_cursor((int) cursor); -} - -// NOTE: this callback is needed to react to the user actively leaving fullscreen mode via Esc -_SOKOL_PRIVATE EM_BOOL _sapp_emsc_fullscreenchange_cb(int emsc_type, const EmscriptenFullscreenChangeEvent* emsc_event, void* user_data) { - _SOKOL_UNUSED(emsc_type); - _SOKOL_UNUSED(user_data); - _sapp.fullscreen = emsc_event->isFullscreen; - return true; -} - -EM_JS(void, sapp_js_toggle_fullscreen, (void), { - const canvas = Module.sapp_emsc_target; - if (canvas) { - // NOTE: Safari had the prefix until 2023, Firefox until 2018 - const fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement; - let p = undefined; - if (!fullscreenElement) { - if (canvas.requestFullscreen) { - p = canvas.requestFullscreen(); - } else if (canvas.webkitRequestFullscreen) { - p = canvas.webkitRequestFullscreen(); - } else if (canvas.mozRequestFullScreen) { - p = canvas.mozRequestFullScreen(); - } - if (p) { - p.catch((err) => { - console.warn('sapp_js_toggle_fullscreen(): failed to enter fullscreen mode with', err); - __sapp_emsc_set_fullscreen_flag(0); - }); - } else { - console.warn('sapp_js_toogle_fullscreen(): browser has no [webkit|moz]requestFullscreen function'); - __sapp_emsc_set_fullscreen_flag(0); - } - } else { - if (document.exitFullscreen) { - p = document.exitFullscreen(); - } else if (document.webkitExitFullscreen) { - p = document.webkitExitFullscreen(); - } else if (document.mozCancelFullScreen) { - p = document.mozCancelFullScreen(); - } - if (p) { - p.catch((err) => { - console.warn('sapp_js_toggle_fullscreen(): failed to exit fullscreen mode with', err); - __sapp_emsc_set_fullscreen_flag(1); - }); - } else { - console.warn('sapp_js_toggle_fullscreen(): browser has no [wekbit|moz]exitFullscreen'); - // NOTE: don't need to explicitly set the fullscreen flag here - } - } - } -}) - -_SOKOL_PRIVATE void _sapp_emsc_toggle_fullscreen(void) { - // toggle the fullscreen flag preliminary, this may be undone - // when requesting/exiting fullscreen mode actually fails - _sapp.fullscreen = !_sapp.fullscreen; - sapp_js_toggle_fullscreen(); -} - -/* JS helper functions to update browser tab favicon */ -EM_JS(void, sapp_js_clear_favicon, (void), { - const link = document.getElementById('sokol-app-favicon'); - if (link) { - document.head.removeChild(link); - } -}) - -EM_JS(void, sapp_js_set_favicon, (int w, int h, const uint8_t* pixels), { - const canvas = document.createElement('canvas'); - canvas.width = w; - canvas.height = h; - const ctx = canvas.getContext('2d'); - const img_data = ctx.createImageData(w, h); - img_data.data.set(HEAPU8.subarray(pixels, pixels + w*h*4)); - ctx.putImageData(img_data, 0, 0); - const new_link = document.createElement('link'); - new_link.id = 'sokol-app-favicon'; - new_link.rel = 'shortcut icon'; - new_link.href = canvas.toDataURL(); - document.head.appendChild(new_link); -}) - -_SOKOL_PRIVATE void _sapp_emsc_set_icon(const sapp_icon_desc* icon_desc, int num_images) { - SOKOL_ASSERT((num_images > 0) && (num_images <= SAPP_MAX_ICONIMAGES)); - sapp_js_clear_favicon(); - // find the best matching image candidate for 16x16 pixels - int img_index = _sapp_image_bestmatch(icon_desc->images, num_images, 16, 16); - const sapp_image_desc* img_desc = &icon_desc->images[img_index]; - sapp_js_set_favicon(img_desc->width, img_desc->height, (const uint8_t*) img_desc->pixels.ptr); -} - -_SOKOL_PRIVATE uint32_t _sapp_emsc_mouse_button_mods(uint16_t buttons) { - uint32_t m = 0; - if (0 != (buttons & (1<<0))) { m |= SAPP_MODIFIER_LMB; } - if (0 != (buttons & (1<<1))) { m |= SAPP_MODIFIER_RMB; } // not a bug - if (0 != (buttons & (1<<2))) { m |= SAPP_MODIFIER_MMB; } // not a bug - return m; -} - -_SOKOL_PRIVATE uint32_t _sapp_emsc_mouse_event_mods(const EmscriptenMouseEvent* ev) { - uint32_t m = 0; - if (ev->ctrlKey) { m |= SAPP_MODIFIER_CTRL; } - if (ev->shiftKey) { m |= SAPP_MODIFIER_SHIFT; } - if (ev->altKey) { m |= SAPP_MODIFIER_ALT; } - if (ev->metaKey) { m |= SAPP_MODIFIER_SUPER; } - m |= _sapp_emsc_mouse_button_mods(_sapp.emsc.mouse_buttons); - return m; -} - -_SOKOL_PRIVATE uint32_t _sapp_emsc_key_event_mods(const EmscriptenKeyboardEvent* ev) { - uint32_t m = 0; - if (ev->ctrlKey) { m |= SAPP_MODIFIER_CTRL; } - if (ev->shiftKey) { m |= SAPP_MODIFIER_SHIFT; } - if (ev->altKey) { m |= SAPP_MODIFIER_ALT; } - if (ev->metaKey) { m |= SAPP_MODIFIER_SUPER; } - m |= _sapp_emsc_mouse_button_mods(_sapp.emsc.mouse_buttons); - return m; -} - -_SOKOL_PRIVATE uint32_t _sapp_emsc_touch_event_mods(const EmscriptenTouchEvent* ev) { - uint32_t m = 0; - if (ev->ctrlKey) { m |= SAPP_MODIFIER_CTRL; } - if (ev->shiftKey) { m |= SAPP_MODIFIER_SHIFT; } - if (ev->altKey) { m |= SAPP_MODIFIER_ALT; } - if (ev->metaKey) { m |= SAPP_MODIFIER_SUPER; } - m |= _sapp_emsc_mouse_button_mods(_sapp.emsc.mouse_buttons); - return m; -} - -_SOKOL_PRIVATE EM_BOOL _sapp_emsc_size_changed(int event_type, const EmscriptenUiEvent* ui_event, void* user_data) { - _SOKOL_UNUSED(event_type); - _SOKOL_UNUSED(user_data); - double w, h; - emscripten_get_element_css_size(_sapp.html5_canvas_selector, &w, &h); - /* The above method might report zero when toggling HTML5 fullscreen, - in that case use the window's inner width reported by the - emscripten event. This works ok when toggling *into* fullscreen - but doesn't properly restore the previous canvas size when switching - back from fullscreen. - - In general, due to the HTML5's fullscreen API's flaky nature it is - recommended to use 'soft fullscreen' (stretching the WebGL canvas - over the browser windows client rect) with a CSS definition like this: - - position: absolute; - top: 0px; - left: 0px; - margin: 0px; - border: 0; - width: 100%; - height: 100%; - overflow: hidden; - display: block; - */ - if (w < 1.0) { - w = ui_event->windowInnerWidth; - } else { - _sapp.window_width = _sapp_roundf_gzero(w); - } - if (h < 1.0) { - h = ui_event->windowInnerHeight; - } else { - _sapp.window_height = _sapp_roundf_gzero(h); - } - if (_sapp.desc.high_dpi) { - _sapp.dpi_scale = emscripten_get_device_pixel_ratio(); - } - _sapp.framebuffer_width = _sapp_roundf_gzero(w * _sapp.dpi_scale); - _sapp.framebuffer_height = _sapp_roundf_gzero(h * _sapp.dpi_scale); - emscripten_set_canvas_element_size(_sapp.html5_canvas_selector, _sapp.framebuffer_width, _sapp.framebuffer_height); - #if defined(SOKOL_WGPU) - // on WebGPU: recreate size-dependent rendering surfaces - _sapp_wgpu_swapchain_size_changed(); - #endif - if (_sapp_events_enabled()) { - _sapp_init_event(SAPP_EVENTTYPE_RESIZED); - _sapp_call_event(&_sapp.event); - } - return true; -} - -_SOKOL_PRIVATE EM_BOOL _sapp_emsc_mouse_cb(int emsc_type, const EmscriptenMouseEvent* emsc_event, void* user_data) { - _SOKOL_UNUSED(user_data); - bool consume_event = !_sapp.desc.html5.bubble_mouse_events; - _sapp.emsc.mouse_buttons = emsc_event->buttons; - if (_sapp.mouse.locked) { - _sapp.mouse.dx = (float) emsc_event->movementX; - _sapp.mouse.dy = (float) emsc_event->movementY; - } else { - float new_x = emsc_event->targetX * _sapp.dpi_scale; - float new_y = emsc_event->targetY * _sapp.dpi_scale; - if (_sapp.mouse.pos_valid) { - _sapp.mouse.dx = new_x - _sapp.mouse.x; - _sapp.mouse.dy = new_y - _sapp.mouse.y; - } - _sapp.mouse.x = new_x; - _sapp.mouse.y = new_y; - _sapp.mouse.pos_valid = true; - } - if (_sapp_events_enabled() && (emsc_event->button >= 0) && (emsc_event->button < SAPP_MAX_MOUSEBUTTONS)) { - sapp_event_type type; - bool is_button_event = false; - bool clear_dxdy = false; - switch (emsc_type) { - case EMSCRIPTEN_EVENT_MOUSEDOWN: - type = SAPP_EVENTTYPE_MOUSE_DOWN; - is_button_event = true; - break; - case EMSCRIPTEN_EVENT_MOUSEUP: - type = SAPP_EVENTTYPE_MOUSE_UP; - is_button_event = true; - break; - case EMSCRIPTEN_EVENT_MOUSEMOVE: - type = SAPP_EVENTTYPE_MOUSE_MOVE; - break; - case EMSCRIPTEN_EVENT_MOUSEENTER: - type = SAPP_EVENTTYPE_MOUSE_ENTER; - clear_dxdy = true; - break; - case EMSCRIPTEN_EVENT_MOUSELEAVE: - type = SAPP_EVENTTYPE_MOUSE_LEAVE; - clear_dxdy = true; - break; - default: - type = SAPP_EVENTTYPE_INVALID; - break; - } - if (clear_dxdy) { - _sapp.mouse.dx = 0.0f; - _sapp.mouse.dy = 0.0f; - } - if (type != SAPP_EVENTTYPE_INVALID) { - _sapp_init_event(type); - _sapp.event.modifiers = _sapp_emsc_mouse_event_mods(emsc_event); - if (is_button_event) { - switch (emsc_event->button) { - case 0: _sapp.event.mouse_button = SAPP_MOUSEBUTTON_LEFT; break; - case 1: _sapp.event.mouse_button = SAPP_MOUSEBUTTON_MIDDLE; break; - case 2: _sapp.event.mouse_button = SAPP_MOUSEBUTTON_RIGHT; break; - default: _sapp.event.mouse_button = (sapp_mousebutton)emsc_event->button; break; - } - } else { - _sapp.event.mouse_button = SAPP_MOUSEBUTTON_INVALID; - } - consume_event |= _sapp_call_event(&_sapp.event); - } - // mouse lock can only be activated in mouse button events (not in move, enter or leave) - if (is_button_event) { - _sapp_emsc_update_mouse_lock_state(); - } - } - return consume_event; -} - -_SOKOL_PRIVATE EM_BOOL _sapp_emsc_wheel_cb(int emsc_type, const EmscriptenWheelEvent* emsc_event, void* user_data) { - _SOKOL_UNUSED(emsc_type); - _SOKOL_UNUSED(user_data); - bool consume_event = !_sapp.desc.html5.bubble_wheel_events; - _sapp.emsc.mouse_buttons = emsc_event->mouse.buttons; - if (_sapp_events_enabled()) { - _sapp_init_event(SAPP_EVENTTYPE_MOUSE_SCROLL); - _sapp.event.modifiers = _sapp_emsc_mouse_event_mods(&emsc_event->mouse); - /* see https://github.com/floooh/sokol/issues/339 */ - float scale; - switch (emsc_event->deltaMode) { - case DOM_DELTA_PIXEL: scale = -0.01f; break; - case DOM_DELTA_LINE: scale = -1.33f; break; - case DOM_DELTA_PAGE: scale = -10.0f; break; // FIXME: this is a guess - default: scale = -0.1f; break; // shouldn't happen - } - _sapp.event.scroll_x = scale * (float)emsc_event->deltaX; - _sapp.event.scroll_y = scale * (float)emsc_event->deltaY; - consume_event |= _sapp_call_event(&_sapp.event); - } - _sapp_emsc_update_mouse_lock_state(); - return consume_event; -} - -static struct { - const char* str; - sapp_keycode code; -} _sapp_emsc_keymap[] = { - { "Backspace", SAPP_KEYCODE_BACKSPACE }, - { "Tab", SAPP_KEYCODE_TAB }, - { "Enter", SAPP_KEYCODE_ENTER }, - { "ShiftLeft", SAPP_KEYCODE_LEFT_SHIFT }, - { "ShiftRight", SAPP_KEYCODE_RIGHT_SHIFT }, - { "ControlLeft", SAPP_KEYCODE_LEFT_CONTROL }, - { "ControlRight", SAPP_KEYCODE_RIGHT_CONTROL }, - { "AltLeft", SAPP_KEYCODE_LEFT_ALT }, - { "AltRight", SAPP_KEYCODE_RIGHT_ALT }, - { "Pause", SAPP_KEYCODE_PAUSE }, - { "CapsLock", SAPP_KEYCODE_CAPS_LOCK }, - { "Escape", SAPP_KEYCODE_ESCAPE }, - { "Space", SAPP_KEYCODE_SPACE }, - { "PageUp", SAPP_KEYCODE_PAGE_UP }, - { "PageDown", SAPP_KEYCODE_PAGE_DOWN }, - { "End", SAPP_KEYCODE_END }, - { "Home", SAPP_KEYCODE_HOME }, - { "ArrowLeft", SAPP_KEYCODE_LEFT }, - { "ArrowUp", SAPP_KEYCODE_UP }, - { "ArrowRight", SAPP_KEYCODE_RIGHT }, - { "ArrowDown", SAPP_KEYCODE_DOWN }, - { "PrintScreen", SAPP_KEYCODE_PRINT_SCREEN }, - { "Insert", SAPP_KEYCODE_INSERT }, - { "Delete", SAPP_KEYCODE_DELETE }, - { "Digit0", SAPP_KEYCODE_0 }, - { "Digit1", SAPP_KEYCODE_1 }, - { "Digit2", SAPP_KEYCODE_2 }, - { "Digit3", SAPP_KEYCODE_3 }, - { "Digit4", SAPP_KEYCODE_4 }, - { "Digit5", SAPP_KEYCODE_5 }, - { "Digit6", SAPP_KEYCODE_6 }, - { "Digit7", SAPP_KEYCODE_7 }, - { "Digit8", SAPP_KEYCODE_8 }, - { "Digit9", SAPP_KEYCODE_9 }, - { "KeyA", SAPP_KEYCODE_A }, - { "KeyB", SAPP_KEYCODE_B }, - { "KeyC", SAPP_KEYCODE_C }, - { "KeyD", SAPP_KEYCODE_D }, - { "KeyE", SAPP_KEYCODE_E }, - { "KeyF", SAPP_KEYCODE_F }, - { "KeyG", SAPP_KEYCODE_G }, - { "KeyH", SAPP_KEYCODE_H }, - { "KeyI", SAPP_KEYCODE_I }, - { "KeyJ", SAPP_KEYCODE_J }, - { "KeyK", SAPP_KEYCODE_K }, - { "KeyL", SAPP_KEYCODE_L }, - { "KeyM", SAPP_KEYCODE_M }, - { "KeyN", SAPP_KEYCODE_N }, - { "KeyO", SAPP_KEYCODE_O }, - { "KeyP", SAPP_KEYCODE_P }, - { "KeyQ", SAPP_KEYCODE_Q }, - { "KeyR", SAPP_KEYCODE_R }, - { "KeyS", SAPP_KEYCODE_S }, - { "KeyT", SAPP_KEYCODE_T }, - { "KeyU", SAPP_KEYCODE_U }, - { "KeyV", SAPP_KEYCODE_V }, - { "KeyW", SAPP_KEYCODE_W }, - { "KeyX", SAPP_KEYCODE_X }, - { "KeyY", SAPP_KEYCODE_Y }, - { "KeyZ", SAPP_KEYCODE_Z }, - { "MetaLeft", SAPP_KEYCODE_LEFT_SUPER }, - { "MetaRight", SAPP_KEYCODE_RIGHT_SUPER }, - { "Numpad0", SAPP_KEYCODE_KP_0 }, - { "Numpad1", SAPP_KEYCODE_KP_1 }, - { "Numpad2", SAPP_KEYCODE_KP_2 }, - { "Numpad3", SAPP_KEYCODE_KP_3 }, - { "Numpad4", SAPP_KEYCODE_KP_4 }, - { "Numpad5", SAPP_KEYCODE_KP_5 }, - { "Numpad6", SAPP_KEYCODE_KP_6 }, - { "Numpad7", SAPP_KEYCODE_KP_7 }, - { "Numpad8", SAPP_KEYCODE_KP_8 }, - { "Numpad9", SAPP_KEYCODE_KP_9 }, - { "NumpadMultiply", SAPP_KEYCODE_KP_MULTIPLY }, - { "NumpadAdd", SAPP_KEYCODE_KP_ADD }, - { "NumpadSubtract", SAPP_KEYCODE_KP_SUBTRACT }, - { "NumpadDecimal", SAPP_KEYCODE_KP_DECIMAL }, - { "NumpadDivide", SAPP_KEYCODE_KP_DIVIDE }, - { "F1", SAPP_KEYCODE_F1 }, - { "F2", SAPP_KEYCODE_F2 }, - { "F3", SAPP_KEYCODE_F3 }, - { "F4", SAPP_KEYCODE_F4 }, - { "F5", SAPP_KEYCODE_F5 }, - { "F6", SAPP_KEYCODE_F6 }, - { "F7", SAPP_KEYCODE_F7 }, - { "F8", SAPP_KEYCODE_F8 }, - { "F9", SAPP_KEYCODE_F9 }, - { "F10", SAPP_KEYCODE_F10 }, - { "F11", SAPP_KEYCODE_F11 }, - { "F12", SAPP_KEYCODE_F12 }, - { "NumLock", SAPP_KEYCODE_NUM_LOCK }, - { "ScrollLock", SAPP_KEYCODE_SCROLL_LOCK }, - { "Semicolon", SAPP_KEYCODE_SEMICOLON }, - { "Equal", SAPP_KEYCODE_EQUAL }, - { "Comma", SAPP_KEYCODE_COMMA }, - { "Minus", SAPP_KEYCODE_MINUS }, - { "Period", SAPP_KEYCODE_PERIOD }, - { "Slash", SAPP_KEYCODE_SLASH }, - { "Backquote", SAPP_KEYCODE_GRAVE_ACCENT }, - { "BracketLeft", SAPP_KEYCODE_LEFT_BRACKET }, - { "Backslash", SAPP_KEYCODE_BACKSLASH }, - { "BracketRight", SAPP_KEYCODE_RIGHT_BRACKET }, - { "Quote", SAPP_KEYCODE_GRAVE_ACCENT }, // FIXME: ??? - { 0, SAPP_KEYCODE_INVALID }, -}; - -_SOKOL_PRIVATE sapp_keycode _sapp_emsc_translate_key(const char* str) { - int i = 0; - const char* keystr; - while (( keystr = _sapp_emsc_keymap[i].str )) { - if (0 == strcmp(str, keystr)) { - return _sapp_emsc_keymap[i].code; - } - i += 1; - } - return SAPP_KEYCODE_INVALID; -} - -// returns true if the key code is a 'character key', this is used to decide -// if a key event needs to bubble up to create a char event -_SOKOL_PRIVATE bool _sapp_emsc_is_char_key(sapp_keycode key_code) { - return key_code < SAPP_KEYCODE_WORLD_1; -} - -_SOKOL_PRIVATE EM_BOOL _sapp_emsc_key_cb(int emsc_type, const EmscriptenKeyboardEvent* emsc_event, void* user_data) { - _SOKOL_UNUSED(user_data); - bool consume_event = false; - if (_sapp_events_enabled()) { - sapp_event_type type; - switch (emsc_type) { - case EMSCRIPTEN_EVENT_KEYDOWN: - type = SAPP_EVENTTYPE_KEY_DOWN; - break; - case EMSCRIPTEN_EVENT_KEYUP: - type = SAPP_EVENTTYPE_KEY_UP; - break; - case EMSCRIPTEN_EVENT_KEYPRESS: - type = SAPP_EVENTTYPE_CHAR; - break; - default: - type = SAPP_EVENTTYPE_INVALID; - break; - } - if (type != SAPP_EVENTTYPE_INVALID) { - bool send_keyup_followup = false; - _sapp_init_event(type); - _sapp.event.key_repeat = emsc_event->repeat; - _sapp.event.modifiers = _sapp_emsc_key_event_mods(emsc_event); - if (type == SAPP_EVENTTYPE_CHAR) { - // NOTE: charCode doesn't appear to be supported on Android Chrome - _sapp.event.char_code = emsc_event->charCode; - consume_event |= !_sapp.desc.html5.bubble_char_events; - } else { - if (0 != emsc_event->code[0]) { - // This code path is for desktop browsers which send untranslated 'physical' key code strings - // (which is what we actually want for key events) - _sapp.event.key_code = _sapp_emsc_translate_key(emsc_event->code); - } else { - // This code path is for mobile browsers which only send localized key code - // strings. Note that the translation will only work for a small subset - // of localization-agnostic keys (like Enter, arrow keys, etc...), but - // regular alpha-numeric keys will all result in an SAPP_KEYCODE_INVALID) - _sapp.event.key_code = _sapp_emsc_translate_key(emsc_event->key); - } - - // Special hack for macOS: if the Super key is pressed, macOS doesn't - // send keyUp events. As a workaround, to prevent keys from - // "sticking", we'll send a keyup event following a keydown - // when the SUPER key is pressed - if ((type == SAPP_EVENTTYPE_KEY_DOWN) && - (_sapp.event.key_code != SAPP_KEYCODE_LEFT_SUPER) && - (_sapp.event.key_code != SAPP_KEYCODE_RIGHT_SUPER) && - (_sapp.event.modifiers & SAPP_MODIFIER_SUPER)) - { - send_keyup_followup = true; - } - - // 'character key events' will always need to bubble up, otherwise the browser - // wouldn't be able to generate character events. - if (!_sapp_emsc_is_char_key(_sapp.event.key_code)) { - consume_event |= !_sapp.desc.html5.bubble_key_events; - } - } - consume_event |= _sapp_call_event(&_sapp.event); - if (send_keyup_followup) { - _sapp.event.type = SAPP_EVENTTYPE_KEY_UP; - consume_event |= _sapp_call_event(&_sapp.event); - } - } - } - _sapp_emsc_update_mouse_lock_state(); - return consume_event; -} - -_SOKOL_PRIVATE EM_BOOL _sapp_emsc_touch_cb(int emsc_type, const EmscriptenTouchEvent* emsc_event, void* user_data) { - _SOKOL_UNUSED(user_data); - bool consume_event = !_sapp.desc.html5.bubble_touch_events; - if (_sapp_events_enabled()) { - sapp_event_type type; - switch (emsc_type) { - case EMSCRIPTEN_EVENT_TOUCHSTART: - type = SAPP_EVENTTYPE_TOUCHES_BEGAN; - break; - case EMSCRIPTEN_EVENT_TOUCHMOVE: - type = SAPP_EVENTTYPE_TOUCHES_MOVED; - break; - case EMSCRIPTEN_EVENT_TOUCHEND: - type = SAPP_EVENTTYPE_TOUCHES_ENDED; - break; - case EMSCRIPTEN_EVENT_TOUCHCANCEL: - type = SAPP_EVENTTYPE_TOUCHES_CANCELLED; - break; - default: - type = SAPP_EVENTTYPE_INVALID; - break; - } - if (type != SAPP_EVENTTYPE_INVALID) { - _sapp_init_event(type); - _sapp.event.modifiers = _sapp_emsc_touch_event_mods(emsc_event); - _sapp.event.num_touches = emsc_event->numTouches; - if (_sapp.event.num_touches > SAPP_MAX_TOUCHPOINTS) { - _sapp.event.num_touches = SAPP_MAX_TOUCHPOINTS; - } - for (int i = 0; i < _sapp.event.num_touches; i++) { - const EmscriptenTouchPoint* src = &emsc_event->touches[i]; - sapp_touchpoint* dst = &_sapp.event.touches[i]; - dst->identifier = (uintptr_t)src->identifier; - dst->pos_x = src->targetX * _sapp.dpi_scale; - dst->pos_y = src->targetY * _sapp.dpi_scale; - dst->changed = src->isChanged; - } - consume_event |= _sapp_call_event(&_sapp.event); - } - } - return consume_event; -} - -_SOKOL_PRIVATE EM_BOOL _sapp_emsc_focus_cb(int emsc_type, const EmscriptenFocusEvent* emsc_event, void* user_data) { - _SOKOL_UNUSED(emsc_type); - _SOKOL_UNUSED(emsc_event); - _SOKOL_UNUSED(user_data); - if (_sapp_events_enabled()) { - _sapp_init_event(SAPP_EVENTTYPE_FOCUSED); - _sapp_call_event(&_sapp.event); - } - return true; -} - -_SOKOL_PRIVATE EM_BOOL _sapp_emsc_blur_cb(int emsc_type, const EmscriptenFocusEvent* emsc_event, void* user_data) { - _SOKOL_UNUSED(emsc_type); - _SOKOL_UNUSED(emsc_event); - _SOKOL_UNUSED(user_data); - if (_sapp_events_enabled()) { - _sapp_init_event(SAPP_EVENTTYPE_UNFOCUSED); - _sapp_call_event(&_sapp.event); - } - return true; -} - -#if defined(SOKOL_GLES3) -_SOKOL_PRIVATE EM_BOOL _sapp_emsc_webgl_context_cb(int emsc_type, const void* reserved, void* user_data) { - _SOKOL_UNUSED(reserved); - _SOKOL_UNUSED(user_data); - sapp_event_type type; - switch (emsc_type) { - case EMSCRIPTEN_EVENT_WEBGLCONTEXTLOST: type = SAPP_EVENTTYPE_SUSPENDED; break; - case EMSCRIPTEN_EVENT_WEBGLCONTEXTRESTORED: type = SAPP_EVENTTYPE_RESUMED; break; - default: type = SAPP_EVENTTYPE_INVALID; break; - } - if (_sapp_events_enabled() && (SAPP_EVENTTYPE_INVALID != type)) { - _sapp_init_event(type); - _sapp_call_event(&_sapp.event); - } - return true; -} - -_SOKOL_PRIVATE void _sapp_emsc_webgl_init(void) { - EmscriptenWebGLContextAttributes attrs; - emscripten_webgl_init_context_attributes(&attrs); - attrs.alpha = _sapp.desc.alpha; - attrs.depth = true; - attrs.stencil = true; - attrs.antialias = _sapp.sample_count > 1; - attrs.premultipliedAlpha = _sapp.desc.html5.premultiplied_alpha; - attrs.preserveDrawingBuffer = _sapp.desc.html5.preserve_drawing_buffer; - attrs.enableExtensionsByDefault = true; - attrs.majorVersion = 2; - EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_create_context(_sapp.html5_canvas_selector, &attrs); - // FIXME: error message? - emscripten_webgl_make_context_current(ctx); - glGetIntegerv(GL_FRAMEBUFFER_BINDING, (GLint*)&_sapp.gl.framebuffer); -} -#endif - -_SOKOL_PRIVATE void _sapp_emsc_register_eventhandlers(void) { - // NOTE: HTML canvas doesn't receive input focus, this is why key event handlers are added - // to the window object (this could be worked around by adding a "tab index" to the - // canvas) - emscripten_set_mousedown_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_mouse_cb); - emscripten_set_mouseup_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_mouse_cb); - emscripten_set_mousemove_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_mouse_cb); - emscripten_set_mouseenter_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_mouse_cb); - emscripten_set_mouseleave_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_mouse_cb); - emscripten_set_wheel_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_wheel_cb); - // Modified by tettou771 for TrussC: register keyboard events on canvas instead of window - // This allows other page elements (like Monaco editor) to handle keyboard events - // Canvas must have tabindex attribute to receive focus - emscripten_set_keydown_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_key_cb); - emscripten_set_keyup_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_key_cb); - emscripten_set_keypress_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_key_cb); - emscripten_set_touchstart_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_touch_cb); - emscripten_set_touchmove_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_touch_cb); - emscripten_set_touchend_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_touch_cb); - emscripten_set_touchcancel_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_touch_cb); - emscripten_set_pointerlockchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, 0, true, _sapp_emsc_pointerlockchange_cb); - emscripten_set_pointerlockerror_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, 0, true, _sapp_emsc_pointerlockerror_cb); - emscripten_set_focus_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, true, _sapp_emsc_focus_cb); - emscripten_set_blur_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, true, _sapp_emsc_blur_cb); - emscripten_set_fullscreenchange_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_fullscreenchange_cb); - sapp_js_add_beforeunload_listener(); - if (_sapp.clipboard.enabled) { - sapp_js_add_clipboard_listener(); - } - if (_sapp.drop.enabled) { - sapp_js_add_dragndrop_listeners(); - } - #if defined(SOKOL_GLES3) - emscripten_set_webglcontextlost_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_webgl_context_cb); - emscripten_set_webglcontextrestored_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_webgl_context_cb); - #endif -} - -_SOKOL_PRIVATE void _sapp_emsc_unregister_eventhandlers(void) { - emscripten_set_mousedown_callback(_sapp.html5_canvas_selector, 0, true, 0); - emscripten_set_mouseup_callback(_sapp.html5_canvas_selector, 0, true, 0); - emscripten_set_mousemove_callback(_sapp.html5_canvas_selector, 0, true, 0); - emscripten_set_mouseenter_callback(_sapp.html5_canvas_selector, 0, true, 0); - emscripten_set_mouseleave_callback(_sapp.html5_canvas_selector, 0, true, 0); - emscripten_set_wheel_callback(_sapp.html5_canvas_selector, 0, true, 0); - // Modified by tettou771 for TrussC: unregister from canvas (see register above) - emscripten_set_keydown_callback(_sapp.html5_canvas_selector, 0, true, 0); - emscripten_set_keyup_callback(_sapp.html5_canvas_selector, 0, true, 0); - emscripten_set_keypress_callback(_sapp.html5_canvas_selector, 0, true, 0); - emscripten_set_touchstart_callback(_sapp.html5_canvas_selector, 0, true, 0); - emscripten_set_touchmove_callback(_sapp.html5_canvas_selector, 0, true, 0); - emscripten_set_touchend_callback(_sapp.html5_canvas_selector, 0, true, 0); - emscripten_set_touchcancel_callback(_sapp.html5_canvas_selector, 0, true, 0); - emscripten_set_pointerlockchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, 0, true, 0); - emscripten_set_pointerlockerror_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, 0, true, 0); - emscripten_set_focus_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, true, 0); - emscripten_set_blur_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, true, 0); - emscripten_set_fullscreenchange_callback(_sapp.html5_canvas_selector, 0, true, 0); - if (!_sapp.desc.html5.canvas_resize) { - emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, true, 0); - } - sapp_js_remove_beforeunload_listener(); - if (_sapp.clipboard.enabled) { - sapp_js_remove_clipboard_listener(); - } - if (_sapp.drop.enabled) { - sapp_js_remove_dragndrop_listeners(); - } - #if defined(SOKOL_GLES3) - emscripten_set_webglcontextlost_callback(_sapp.html5_canvas_selector, 0, true, 0); - emscripten_set_webglcontextrestored_callback(_sapp.html5_canvas_selector, 0, true, 0); - #endif -} - -_SOKOL_PRIVATE EM_BOOL _sapp_emsc_frame_animation_loop(double time, void* userData) { - _SOKOL_UNUSED(userData); - _sapp_timing_update(&_sapp.timing, time / 1000.0); - - #if defined(SOKOL_WGPU) - _sapp_wgpu_frame(); - #else - _sapp_frame(); - #endif - - // quit-handling - if (_sapp.quit_requested) { - _sapp_init_event(SAPP_EVENTTYPE_QUIT_REQUESTED); - _sapp_call_event(&_sapp.event); - if (_sapp.quit_requested) { - _sapp.quit_ordered = true; - } - } - if (_sapp.quit_ordered) { - _sapp_emsc_unregister_eventhandlers(); - #if defined(SOKOL_WGPU) - _sapp_wgpu_discard(); - #endif - _sapp_call_cleanup(); - _sapp_discard_state(); - return EM_FALSE; - } - return EM_TRUE; -} - -_SOKOL_PRIVATE void _sapp_emsc_frame_main_loop(void) { - const double time = emscripten_performance_now(); - if (!_sapp_emsc_frame_animation_loop(time, 0)) { - emscripten_cancel_main_loop(); - } -} - -_SOKOL_PRIVATE void _sapp_emsc_run(const sapp_desc* desc) { - _sapp_init_state(desc); - _sapp.fullscreen = false; // override user provided fullscreen state: can't start in fullscreen on the web! - const char* document_title = desc->html5.update_document_title ? _sapp.window_title : 0; - sapp_js_init(_sapp.html5_canvas_selector, document_title); - double w, h; - if (_sapp.desc.html5.canvas_resize) { - w = (double) _sapp_def(_sapp.desc.width, _SAPP_FALLBACK_DEFAULT_WINDOW_WIDTH); - h = (double) _sapp_def(_sapp.desc.height, _SAPP_FALLBACK_DEFAULT_WINDOW_HEIGHT); - } else { - emscripten_get_element_css_size(_sapp.html5_canvas_selector, &w, &h); - emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, false, _sapp_emsc_size_changed); - } - if (_sapp.desc.high_dpi) { - _sapp.dpi_scale = emscripten_get_device_pixel_ratio(); - } - _sapp.window_width = _sapp_roundf_gzero(w); - _sapp.window_height = _sapp_roundf_gzero(h); - _sapp.framebuffer_width = _sapp_roundf_gzero(w * _sapp.dpi_scale); - _sapp.framebuffer_height = _sapp_roundf_gzero(h * _sapp.dpi_scale); - emscripten_set_canvas_element_size(_sapp.html5_canvas_selector, _sapp.framebuffer_width, _sapp.framebuffer_height); - #if defined(SOKOL_GLES3) - _sapp_emsc_webgl_init(); - #elif defined(SOKOL_WGPU) - _sapp_wgpu_init(); - #endif - _sapp.valid = true; - _sapp_emsc_register_eventhandlers(); - sapp_set_icon(&desc->icon); - - // start the frame loop - if (_sapp.desc.html5.use_emsc_set_main_loop) { - emscripten_set_main_loop(_sapp_emsc_frame_main_loop, 0, _sapp.desc.html5.emsc_set_main_loop_simulate_infinite_loop); - } else { - emscripten_request_animation_frame_loop(_sapp_emsc_frame_animation_loop, 0); - } - // NOT A BUG: do not call _sapp_discard_state() here, instead this is - // called in _sapp_emsc_frame() when the application is ordered to quit -} - -#if !defined(SOKOL_NO_ENTRY) -int main(int argc, char* argv[]) { - sapp_desc desc = sokol_main(argc, argv); - _sapp_emsc_run(&desc); - return 0; -} -#endif /* SOKOL_NO_ENTRY */ -#endif /* _SAPP_EMSCRIPTEN */ - -// ██████ ██ ██ ██ ███████ ██ ██████ ███████ ██████ ███████ -// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ██ ███ ██ ███████ █████ ██ ██████ █████ ██████ ███████ -// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ██████ ███████ ██ ██ ███████ ███████ ██ ███████ ██ ██ ███████ -// -// >>gl helpers -#if defined(SOKOL_GLCORE) -typedef struct { - int red_bits; - int green_bits; - int blue_bits; - int alpha_bits; - int depth_bits; - int stencil_bits; - int samples; - bool doublebuffer; - uintptr_t handle; -} _sapp_gl_fbconfig; - -_SOKOL_PRIVATE void _sapp_gl_init_fbconfig(_sapp_gl_fbconfig* fbconfig) { - _sapp_clear(fbconfig, sizeof(_sapp_gl_fbconfig)); - /* -1 means "don't care" */ - fbconfig->red_bits = -1; - fbconfig->green_bits = -1; - fbconfig->blue_bits = -1; - fbconfig->alpha_bits = -1; - fbconfig->depth_bits = -1; - fbconfig->stencil_bits = -1; - fbconfig->samples = -1; -} - -typedef struct { - int least_missing; - int least_color_diff; - int least_extra_diff; - bool best_match; -} _sapp_gl_fbselect; - -_SOKOL_PRIVATE void _sapp_gl_init_fbselect(_sapp_gl_fbselect* fbselect) { - _sapp_clear(fbselect, sizeof(_sapp_gl_fbselect)); - fbselect->least_missing = 1000000; - fbselect->least_color_diff = 10000000; - fbselect->least_extra_diff = 10000000; - fbselect->best_match = false; -} - -// NOTE: this is used only in the WGL code path -_SOKOL_PRIVATE bool _sapp_gl_select_fbconfig(_sapp_gl_fbselect* fbselect, const _sapp_gl_fbconfig* desired, const _sapp_gl_fbconfig* current) { - int missing = 0; - if (desired->doublebuffer != current->doublebuffer) { - return false; - } - - if ((desired->alpha_bits > 0) && (current->alpha_bits == 0)) { - missing++; - } - if ((desired->depth_bits > 0) && (current->depth_bits == 0)) { - missing++; - } - if ((desired->stencil_bits > 0) && (current->stencil_bits == 0)) { - missing++; - } - if ((desired->samples > 0) && (current->samples == 0)) { - /* Technically, several multisampling buffers could be - involved, but that's a lower level implementation detail and - not important to us here, so we count them as one - */ - missing++; - } - - /* These polynomials make many small channel size differences matter - less than one large channel size difference - Calculate color channel size difference value - */ - int color_diff = 0; - if (desired->red_bits != -1) { - color_diff += (desired->red_bits - current->red_bits) * (desired->red_bits - current->red_bits); - } - if (desired->green_bits != -1) { - color_diff += (desired->green_bits - current->green_bits) * (desired->green_bits - current->green_bits); - } - if (desired->blue_bits != -1) { - color_diff += (desired->blue_bits - current->blue_bits) * (desired->blue_bits - current->blue_bits); - } - - /* Calculate non-color channel size difference value */ - int extra_diff = 0; - if (desired->alpha_bits != -1) { - extra_diff += (desired->alpha_bits - current->alpha_bits) * (desired->alpha_bits - current->alpha_bits); - } - if (desired->depth_bits != -1) { - extra_diff += (desired->depth_bits - current->depth_bits) * (desired->depth_bits - current->depth_bits); - } - if (desired->stencil_bits != -1) { - extra_diff += (desired->stencil_bits - current->stencil_bits) * (desired->stencil_bits - current->stencil_bits); - } - if (desired->samples != -1) { - extra_diff += (desired->samples - current->samples) * (desired->samples - current->samples); - } - - /* Figure out if the current one is better than the best one found so far - Least number of missing buffers is the most important heuristic, - then color buffer size match and lastly size match for other buffers - */ - bool new_closest = false; - if (missing < fbselect->least_missing) { - new_closest = true; - } else if (missing == fbselect->least_missing) { - if ((color_diff < fbselect->least_color_diff) || - ((color_diff == fbselect->least_color_diff) && (extra_diff < fbselect->least_extra_diff))) - { - new_closest = true; - } - } - if (new_closest) { - fbselect->least_missing = missing; - fbselect->least_color_diff = color_diff; - fbselect->least_extra_diff = extra_diff; - fbselect->best_match = (missing | color_diff | extra_diff) == 0; - } - return new_closest; -} - -// NOTE: this is used only in the GLX code path -_SOKOL_PRIVATE const _sapp_gl_fbconfig* _sapp_gl_choose_fbconfig(const _sapp_gl_fbconfig* desired, const _sapp_gl_fbconfig* alternatives, int count) { - int missing, least_missing = 1000000; - int color_diff, least_color_diff = 10000000; - int extra_diff, least_extra_diff = 10000000; - const _sapp_gl_fbconfig* current; - const _sapp_gl_fbconfig* closest = 0; - for (int i = 0; i < count; i++) { - current = alternatives + i; - if (desired->doublebuffer != current->doublebuffer) { - continue; - } - missing = 0; - if (desired->alpha_bits > 0 && current->alpha_bits == 0) { - missing++; - } - if (desired->depth_bits > 0 && current->depth_bits == 0) { - missing++; - } - if (desired->stencil_bits > 0 && current->stencil_bits == 0) { - missing++; - } - if (desired->samples > 0 && current->samples == 0) { - /* Technically, several multisampling buffers could be - involved, but that's a lower level implementation detail and - not important to us here, so we count them as one - */ - missing++; - } - - /* These polynomials make many small channel size differences matter - less than one large channel size difference - Calculate color channel size difference value - */ - color_diff = 0; - if (desired->red_bits != -1) { - color_diff += (desired->red_bits - current->red_bits) * (desired->red_bits - current->red_bits); - } - if (desired->green_bits != -1) { - color_diff += (desired->green_bits - current->green_bits) * (desired->green_bits - current->green_bits); - } - if (desired->blue_bits != -1) { - color_diff += (desired->blue_bits - current->blue_bits) * (desired->blue_bits - current->blue_bits); - } - - /* Calculate non-color channel size difference value */ - extra_diff = 0; - if (desired->alpha_bits != -1) { - extra_diff += (desired->alpha_bits - current->alpha_bits) * (desired->alpha_bits - current->alpha_bits); - } - if (desired->depth_bits != -1) { - extra_diff += (desired->depth_bits - current->depth_bits) * (desired->depth_bits - current->depth_bits); - } - if (desired->stencil_bits != -1) { - extra_diff += (desired->stencil_bits - current->stencil_bits) * (desired->stencil_bits - current->stencil_bits); - } - if (desired->samples != -1) { - extra_diff += (desired->samples - current->samples) * (desired->samples - current->samples); - } - - /* Figure out if the current one is better than the best one found so far - Least number of missing buffers is the most important heuristic, - then color buffer size match and lastly size match for other buffers - */ - if (missing < least_missing) { - closest = current; - } else if (missing == least_missing) { - if ((color_diff < least_color_diff) || - (color_diff == least_color_diff && extra_diff < least_extra_diff)) - { - closest = current; - } - } - if (current == closest) { - least_missing = missing; - least_color_diff = color_diff; - least_extra_diff = extra_diff; - } - } - return closest; -} -#endif - -// ██ ██ ██ ███ ██ ██████ ██████ ██ ██ ███████ -// ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ -// ██ █ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █ ██ ███████ -// ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███ ██ ██ -// ███ ███ ██ ██ ████ ██████ ██████ ███ ███ ███████ -// -// >>windows -#if defined(_SAPP_WIN32) -_SOKOL_PRIVATE bool _sapp_win32_utf8_to_wide(const char* src, wchar_t* dst, int dst_num_bytes) { - SOKOL_ASSERT(src && dst && (dst_num_bytes > 1)); - _sapp_clear(dst, (size_t)dst_num_bytes); - const int dst_chars = dst_num_bytes / (int)sizeof(wchar_t); - const int dst_needed = MultiByteToWideChar(CP_UTF8, 0, src, -1, 0, 0); - if ((dst_needed > 0) && (dst_needed < dst_chars)) { - MultiByteToWideChar(CP_UTF8, 0, src, -1, dst, dst_chars); - return true; - } else { - // input string doesn't fit into destination buffer - return false; - } -} - -_SOKOL_PRIVATE void _sapp_win32_app_event(sapp_event_type type) { - if (_sapp_events_enabled()) { - _sapp_init_event(type); - _sapp_call_event(&_sapp.event); - } -} - -_SOKOL_PRIVATE void _sapp_win32_init_keytable(void) { - /* same as GLFW */ - _sapp.keycodes[0x00B] = SAPP_KEYCODE_0; - _sapp.keycodes[0x002] = SAPP_KEYCODE_1; - _sapp.keycodes[0x003] = SAPP_KEYCODE_2; - _sapp.keycodes[0x004] = SAPP_KEYCODE_3; - _sapp.keycodes[0x005] = SAPP_KEYCODE_4; - _sapp.keycodes[0x006] = SAPP_KEYCODE_5; - _sapp.keycodes[0x007] = SAPP_KEYCODE_6; - _sapp.keycodes[0x008] = SAPP_KEYCODE_7; - _sapp.keycodes[0x009] = SAPP_KEYCODE_8; - _sapp.keycodes[0x00A] = SAPP_KEYCODE_9; - _sapp.keycodes[0x01E] = SAPP_KEYCODE_A; - _sapp.keycodes[0x030] = SAPP_KEYCODE_B; - _sapp.keycodes[0x02E] = SAPP_KEYCODE_C; - _sapp.keycodes[0x020] = SAPP_KEYCODE_D; - _sapp.keycodes[0x012] = SAPP_KEYCODE_E; - _sapp.keycodes[0x021] = SAPP_KEYCODE_F; - _sapp.keycodes[0x022] = SAPP_KEYCODE_G; - _sapp.keycodes[0x023] = SAPP_KEYCODE_H; - _sapp.keycodes[0x017] = SAPP_KEYCODE_I; - _sapp.keycodes[0x024] = SAPP_KEYCODE_J; - _sapp.keycodes[0x025] = SAPP_KEYCODE_K; - _sapp.keycodes[0x026] = SAPP_KEYCODE_L; - _sapp.keycodes[0x032] = SAPP_KEYCODE_M; - _sapp.keycodes[0x031] = SAPP_KEYCODE_N; - _sapp.keycodes[0x018] = SAPP_KEYCODE_O; - _sapp.keycodes[0x019] = SAPP_KEYCODE_P; - _sapp.keycodes[0x010] = SAPP_KEYCODE_Q; - _sapp.keycodes[0x013] = SAPP_KEYCODE_R; - _sapp.keycodes[0x01F] = SAPP_KEYCODE_S; - _sapp.keycodes[0x014] = SAPP_KEYCODE_T; - _sapp.keycodes[0x016] = SAPP_KEYCODE_U; - _sapp.keycodes[0x02F] = SAPP_KEYCODE_V; - _sapp.keycodes[0x011] = SAPP_KEYCODE_W; - _sapp.keycodes[0x02D] = SAPP_KEYCODE_X; - _sapp.keycodes[0x015] = SAPP_KEYCODE_Y; - _sapp.keycodes[0x02C] = SAPP_KEYCODE_Z; - _sapp.keycodes[0x028] = SAPP_KEYCODE_APOSTROPHE; - _sapp.keycodes[0x02B] = SAPP_KEYCODE_BACKSLASH; - _sapp.keycodes[0x033] = SAPP_KEYCODE_COMMA; - _sapp.keycodes[0x00D] = SAPP_KEYCODE_EQUAL; - _sapp.keycodes[0x029] = SAPP_KEYCODE_GRAVE_ACCENT; - _sapp.keycodes[0x01A] = SAPP_KEYCODE_LEFT_BRACKET; - _sapp.keycodes[0x00C] = SAPP_KEYCODE_MINUS; - _sapp.keycodes[0x034] = SAPP_KEYCODE_PERIOD; - _sapp.keycodes[0x01B] = SAPP_KEYCODE_RIGHT_BRACKET; - _sapp.keycodes[0x027] = SAPP_KEYCODE_SEMICOLON; - _sapp.keycodes[0x035] = SAPP_KEYCODE_SLASH; - _sapp.keycodes[0x056] = SAPP_KEYCODE_WORLD_2; - _sapp.keycodes[0x00E] = SAPP_KEYCODE_BACKSPACE; - _sapp.keycodes[0x153] = SAPP_KEYCODE_DELETE; - _sapp.keycodes[0x14F] = SAPP_KEYCODE_END; - _sapp.keycodes[0x01C] = SAPP_KEYCODE_ENTER; - _sapp.keycodes[0x001] = SAPP_KEYCODE_ESCAPE; - _sapp.keycodes[0x147] = SAPP_KEYCODE_HOME; - _sapp.keycodes[0x152] = SAPP_KEYCODE_INSERT; - _sapp.keycodes[0x15D] = SAPP_KEYCODE_MENU; - _sapp.keycodes[0x151] = SAPP_KEYCODE_PAGE_DOWN; - _sapp.keycodes[0x149] = SAPP_KEYCODE_PAGE_UP; - _sapp.keycodes[0x045] = SAPP_KEYCODE_PAUSE; - _sapp.keycodes[0x146] = SAPP_KEYCODE_PAUSE; - _sapp.keycodes[0x039] = SAPP_KEYCODE_SPACE; - _sapp.keycodes[0x00F] = SAPP_KEYCODE_TAB; - _sapp.keycodes[0x03A] = SAPP_KEYCODE_CAPS_LOCK; - _sapp.keycodes[0x145] = SAPP_KEYCODE_NUM_LOCK; - _sapp.keycodes[0x046] = SAPP_KEYCODE_SCROLL_LOCK; - _sapp.keycodes[0x03B] = SAPP_KEYCODE_F1; - _sapp.keycodes[0x03C] = SAPP_KEYCODE_F2; - _sapp.keycodes[0x03D] = SAPP_KEYCODE_F3; - _sapp.keycodes[0x03E] = SAPP_KEYCODE_F4; - _sapp.keycodes[0x03F] = SAPP_KEYCODE_F5; - _sapp.keycodes[0x040] = SAPP_KEYCODE_F6; - _sapp.keycodes[0x041] = SAPP_KEYCODE_F7; - _sapp.keycodes[0x042] = SAPP_KEYCODE_F8; - _sapp.keycodes[0x043] = SAPP_KEYCODE_F9; - _sapp.keycodes[0x044] = SAPP_KEYCODE_F10; - _sapp.keycodes[0x057] = SAPP_KEYCODE_F11; - _sapp.keycodes[0x058] = SAPP_KEYCODE_F12; - _sapp.keycodes[0x064] = SAPP_KEYCODE_F13; - _sapp.keycodes[0x065] = SAPP_KEYCODE_F14; - _sapp.keycodes[0x066] = SAPP_KEYCODE_F15; - _sapp.keycodes[0x067] = SAPP_KEYCODE_F16; - _sapp.keycodes[0x068] = SAPP_KEYCODE_F17; - _sapp.keycodes[0x069] = SAPP_KEYCODE_F18; - _sapp.keycodes[0x06A] = SAPP_KEYCODE_F19; - _sapp.keycodes[0x06B] = SAPP_KEYCODE_F20; - _sapp.keycodes[0x06C] = SAPP_KEYCODE_F21; - _sapp.keycodes[0x06D] = SAPP_KEYCODE_F22; - _sapp.keycodes[0x06E] = SAPP_KEYCODE_F23; - _sapp.keycodes[0x076] = SAPP_KEYCODE_F24; - _sapp.keycodes[0x038] = SAPP_KEYCODE_LEFT_ALT; - _sapp.keycodes[0x01D] = SAPP_KEYCODE_LEFT_CONTROL; - _sapp.keycodes[0x02A] = SAPP_KEYCODE_LEFT_SHIFT; - _sapp.keycodes[0x15B] = SAPP_KEYCODE_LEFT_SUPER; - _sapp.keycodes[0x137] = SAPP_KEYCODE_PRINT_SCREEN; - _sapp.keycodes[0x138] = SAPP_KEYCODE_RIGHT_ALT; - _sapp.keycodes[0x11D] = SAPP_KEYCODE_RIGHT_CONTROL; - _sapp.keycodes[0x036] = SAPP_KEYCODE_RIGHT_SHIFT; - _sapp.keycodes[0x136] = SAPP_KEYCODE_RIGHT_SHIFT; - _sapp.keycodes[0x15C] = SAPP_KEYCODE_RIGHT_SUPER; - _sapp.keycodes[0x150] = SAPP_KEYCODE_DOWN; - _sapp.keycodes[0x14B] = SAPP_KEYCODE_LEFT; - _sapp.keycodes[0x14D] = SAPP_KEYCODE_RIGHT; - _sapp.keycodes[0x148] = SAPP_KEYCODE_UP; - _sapp.keycodes[0x052] = SAPP_KEYCODE_KP_0; - _sapp.keycodes[0x04F] = SAPP_KEYCODE_KP_1; - _sapp.keycodes[0x050] = SAPP_KEYCODE_KP_2; - _sapp.keycodes[0x051] = SAPP_KEYCODE_KP_3; - _sapp.keycodes[0x04B] = SAPP_KEYCODE_KP_4; - _sapp.keycodes[0x04C] = SAPP_KEYCODE_KP_5; - _sapp.keycodes[0x04D] = SAPP_KEYCODE_KP_6; - _sapp.keycodes[0x047] = SAPP_KEYCODE_KP_7; - _sapp.keycodes[0x048] = SAPP_KEYCODE_KP_8; - _sapp.keycodes[0x049] = SAPP_KEYCODE_KP_9; - _sapp.keycodes[0x04E] = SAPP_KEYCODE_KP_ADD; - _sapp.keycodes[0x053] = SAPP_KEYCODE_KP_DECIMAL; - _sapp.keycodes[0x135] = SAPP_KEYCODE_KP_DIVIDE; - _sapp.keycodes[0x11C] = SAPP_KEYCODE_KP_ENTER; - _sapp.keycodes[0x037] = SAPP_KEYCODE_KP_MULTIPLY; - _sapp.keycodes[0x04A] = SAPP_KEYCODE_KP_SUBTRACT; -} -#endif // _SAPP_WIN32 - -#if defined(_SAPP_WIN32) - -#if defined(SOKOL_D3D11) - -#if defined(__cplusplus) -#define _sapp_d3d11_Release(self) (self)->Release() -#define _sapp_win32_refiid(iid) iid -#else -#define _sapp_d3d11_Release(self) (self)->lpVtbl->Release(self) -#define _sapp_win32_refiid(iid) &iid -#endif - -#define _SAPP_SAFE_RELEASE(obj) if (obj) { _sapp_d3d11_Release(obj); obj=0; } - - -static const IID _sapp_IID_ID3D11Texture2D = { 0x6f15aaf2,0xd208,0x4e89, {0x9a,0xb4,0x48,0x95,0x35,0xd3,0x4f,0x9c} }; -static const IID _sapp_IID_IDXGIDevice1 = { 0x77db970f,0x6276,0x48ba, {0xba,0x28,0x07,0x01,0x43,0xb4,0x39,0x2c} }; -static const IID _sapp_IID_IDXGIFactory = { 0x7b7166ec,0x21c7,0x44ae, {0xb2,0x1a,0xc9,0xae,0x32,0x1a,0xe3,0x69} }; - -static inline HRESULT _sapp_dxgi_GetBuffer(IDXGISwapChain* self, UINT Buffer, REFIID riid, void** ppSurface) { - #if defined(__cplusplus) - return self->GetBuffer(Buffer, riid, ppSurface); - #else - return self->lpVtbl->GetBuffer(self, Buffer, riid, ppSurface); - #endif -} - -static inline HRESULT _sapp_d3d11_QueryInterface(ID3D11Device* self, REFIID riid, void** ppvObject) { - #if defined(__cplusplus) - return self->QueryInterface(riid, ppvObject); - #else - return self->lpVtbl->QueryInterface(self, riid, ppvObject); - #endif -} - -static inline HRESULT _sapp_d3d11_CreateRenderTargetView(ID3D11Device* self, ID3D11Resource *pResource, const D3D11_RENDER_TARGET_VIEW_DESC* pDesc, ID3D11RenderTargetView** ppRTView) { - #if defined(__cplusplus) - return self->CreateRenderTargetView(pResource, pDesc, ppRTView); - #else - return self->lpVtbl->CreateRenderTargetView(self, pResource, pDesc, ppRTView); - #endif -} - -static inline HRESULT _sapp_d3d11_CreateTexture2D(ID3D11Device* self, const D3D11_TEXTURE2D_DESC* pDesc, const D3D11_SUBRESOURCE_DATA* pInitialData, ID3D11Texture2D** ppTexture2D) { - #if defined(__cplusplus) - return self->CreateTexture2D(pDesc, pInitialData, ppTexture2D); - #else - return self->lpVtbl->CreateTexture2D(self, pDesc, pInitialData, ppTexture2D); - #endif -} - -static inline HRESULT _sapp_d3d11_CreateDepthStencilView(ID3D11Device* self, ID3D11Resource* pResource, const D3D11_DEPTH_STENCIL_VIEW_DESC* pDesc, ID3D11DepthStencilView** ppDepthStencilView) { - #if defined(__cplusplus) - return self->CreateDepthStencilView(pResource, pDesc, ppDepthStencilView); - #else - return self->lpVtbl->CreateDepthStencilView(self, pResource, pDesc, ppDepthStencilView); - #endif -} - -static inline HRESULT _sapp_dxgi_ResizeBuffers(IDXGISwapChain* self, UINT BufferCount, UINT Width, UINT Height, DXGI_FORMAT NewFormat, UINT SwapChainFlags) { - #if defined(__cplusplus) - return self->ResizeBuffers(BufferCount, Width, Height, NewFormat, SwapChainFlags); - #else - return self->lpVtbl->ResizeBuffers(self, BufferCount, Width, Height, NewFormat, SwapChainFlags); - #endif -} - -static inline HRESULT _sapp_dxgi_Present(IDXGISwapChain* self, UINT SyncInterval, UINT Flags) { - #if defined(__cplusplus) - return self->Present(SyncInterval, Flags); - #else - return self->lpVtbl->Present(self, SyncInterval, Flags); - #endif -} - -static inline HRESULT _sapp_dxgi_GetFrameStatistics(IDXGISwapChain* self, DXGI_FRAME_STATISTICS* pStats) { - #if defined(__cplusplus) - return self->GetFrameStatistics(pStats); - #else - return self->lpVtbl->GetFrameStatistics(self, pStats); - #endif -} - -static inline HRESULT _sapp_dxgi_SetMaximumFrameLatency(IDXGIDevice1* self, UINT MaxLatency) { - #if defined(__cplusplus) - return self->SetMaximumFrameLatency(MaxLatency); - #else - return self->lpVtbl->SetMaximumFrameLatency(self, MaxLatency); - #endif -} - -static inline HRESULT _sapp_dxgi_GetAdapter(IDXGIDevice1* self, IDXGIAdapter** pAdapter) { - #if defined(__cplusplus) - return self->GetAdapter(pAdapter); - #else - return self->lpVtbl->GetAdapter(self, pAdapter); - #endif -} - -static inline HRESULT _sapp_dxgi_GetParent(IDXGIObject* self, REFIID riid, void** ppParent) { - #if defined(__cplusplus) - return self->GetParent(riid, ppParent); - #else - return self->lpVtbl->GetParent(self, riid, ppParent); - #endif -} - -static inline HRESULT _sapp_dxgi_MakeWindowAssociation(IDXGIFactory* self, HWND WindowHandle, UINT Flags) { - #if defined(__cplusplus) - return self->MakeWindowAssociation(WindowHandle, Flags); - #else - return self->lpVtbl->MakeWindowAssociation(self, WindowHandle, Flags); - #endif -} - -_SOKOL_PRIVATE void _sapp_d3d11_create_device_and_swapchain(void) { - DXGI_SWAP_CHAIN_DESC* sc_desc = &_sapp.d3d11.swap_chain_desc; - sc_desc->BufferDesc.Width = (UINT)_sapp.framebuffer_width; - sc_desc->BufferDesc.Height = (UINT)_sapp.framebuffer_height; - sc_desc->BufferDesc.Format = DXGI_FORMAT_R10G10B10A2_UNORM /* Modified by tettou771 for TrussC: 10-bit color */; - sc_desc->BufferDesc.RefreshRate.Numerator = 60; - sc_desc->BufferDesc.RefreshRate.Denominator = 1; - sc_desc->OutputWindow = _sapp.win32.hwnd; - sc_desc->Windowed = true; - if (_sapp.win32.is_win10_or_greater) { - sc_desc->BufferCount = 2; - sc_desc->SwapEffect = (DXGI_SWAP_EFFECT) _SAPP_DXGI_SWAP_EFFECT_FLIP_DISCARD; - } else { - sc_desc->BufferCount = 1; - sc_desc->SwapEffect = DXGI_SWAP_EFFECT_DISCARD; - } - sc_desc->SampleDesc.Count = 1; - sc_desc->SampleDesc.Quality = 0; - sc_desc->BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; - UINT create_flags = D3D11_CREATE_DEVICE_SINGLETHREADED | D3D11_CREATE_DEVICE_BGRA_SUPPORT; - #if defined(SOKOL_DEBUG) - create_flags |= D3D11_CREATE_DEVICE_DEBUG; - #endif - D3D_FEATURE_LEVEL requested_feature_levels[] = { D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0 }; - D3D_FEATURE_LEVEL result_feature_level; - HRESULT hr = D3D11CreateDeviceAndSwapChain( - NULL, /* pAdapter (use default) */ - D3D_DRIVER_TYPE_HARDWARE, /* DriverType */ - NULL, /* Software */ - create_flags, /* Flags */ - requested_feature_levels, /* pFeatureLevels */ - 2, /* FeatureLevels */ - D3D11_SDK_VERSION, /* SDKVersion */ - sc_desc, /* pSwapChainDesc */ - &_sapp.d3d11.swap_chain, /* ppSwapChain */ - &_sapp.d3d11.device, /* ppDevice */ - &result_feature_level, /* pFeatureLevel */ - &_sapp.d3d11.device_context); /* ppImmediateContext */ - _SOKOL_UNUSED(hr); - #if defined(SOKOL_DEBUG) - if (!SUCCEEDED(hr)) { - // if initialization with D3D11_CREATE_DEVICE_DEBUG fails, this could be because the - // 'D3D11 debug layer' stopped working, indicated by the error message: - // === - // D3D11CreateDevice: Flags (0x2) were specified which require the D3D11 SDK Layers for Windows 10, but they are not present on the system. - // These flags must be removed, or the Windows 10 SDK must be installed. - // Flags include: D3D11_CREATE_DEVICE_DEBUG - // === - // - // ...just retry with the DEBUG flag switched off - _SAPP_ERROR(WIN32_D3D11_CREATE_DEVICE_AND_SWAPCHAIN_WITH_DEBUG_FAILED); - create_flags &= ~(UINT)D3D11_CREATE_DEVICE_DEBUG; - hr = D3D11CreateDeviceAndSwapChain( - NULL, /* pAdapter (use default) */ - D3D_DRIVER_TYPE_HARDWARE, /* DriverType */ - NULL, /* Software */ - create_flags, /* Flags */ - requested_feature_levels, /* pFeatureLevels */ - 2, /* FeatureLevels */ - D3D11_SDK_VERSION, /* SDKVersion */ - sc_desc, /* pSwapChainDesc */ - &_sapp.d3d11.swap_chain, /* ppSwapChain */ - &_sapp.d3d11.device, /* ppDevice */ - &result_feature_level, /* pFeatureLevel */ - &_sapp.d3d11.device_context); /* ppImmediateContext */ - } - #endif - SOKOL_ASSERT(SUCCEEDED(hr) && _sapp.d3d11.swap_chain && _sapp.d3d11.device && _sapp.d3d11.device_context); - - // minimize frame latency, disable Alt-Enter - hr = _sapp_d3d11_QueryInterface(_sapp.d3d11.device, _sapp_win32_refiid(_sapp_IID_IDXGIDevice1), (void**)&_sapp.d3d11.dxgi_device); - if (SUCCEEDED(hr) && _sapp.d3d11.dxgi_device) { - _sapp_dxgi_SetMaximumFrameLatency(_sapp.d3d11.dxgi_device, 1); - IDXGIAdapter* dxgi_adapter = 0; - hr = _sapp_dxgi_GetAdapter(_sapp.d3d11.dxgi_device, &dxgi_adapter); - if (SUCCEEDED(hr) && dxgi_adapter) { - IDXGIFactory* dxgi_factory = 0; - hr = _sapp_dxgi_GetParent((IDXGIObject*)dxgi_adapter, _sapp_win32_refiid(_sapp_IID_IDXGIFactory), (void**)&dxgi_factory); - if (SUCCEEDED(hr)) { - _sapp_dxgi_MakeWindowAssociation(dxgi_factory, _sapp.win32.hwnd, DXGI_MWA_NO_ALT_ENTER|DXGI_MWA_NO_PRINT_SCREEN); - _SAPP_SAFE_RELEASE(dxgi_factory); - } else { - _SAPP_ERROR(WIN32_D3D11_GET_IDXGIFACTORY_FAILED); - } - _SAPP_SAFE_RELEASE(dxgi_adapter); - } else { - _SAPP_ERROR(WIN32_D3D11_GET_IDXGIADAPTER_FAILED); - } - } else { - _SAPP_PANIC(WIN32_D3D11_QUERY_INTERFACE_IDXGIDEVICE1_FAILED); - } -} - -_SOKOL_PRIVATE void _sapp_d3d11_destroy_device_and_swapchain(void) { - _SAPP_SAFE_RELEASE(_sapp.d3d11.swap_chain); - _SAPP_SAFE_RELEASE(_sapp.d3d11.dxgi_device); - _SAPP_SAFE_RELEASE(_sapp.d3d11.device_context); - _SAPP_SAFE_RELEASE(_sapp.d3d11.device); -} - -_SOKOL_PRIVATE void _sapp_d3d11_create_default_render_target(void) { - SOKOL_ASSERT(0 == _sapp.d3d11.rt); - SOKOL_ASSERT(0 == _sapp.d3d11.rtv); - SOKOL_ASSERT(0 == _sapp.d3d11.msaa_rt); - SOKOL_ASSERT(0 == _sapp.d3d11.msaa_rtv); - SOKOL_ASSERT(0 == _sapp.d3d11.ds); - SOKOL_ASSERT(0 == _sapp.d3d11.dsv); - - HRESULT hr; _SOKOL_UNUSED(hr); - - /* view for the swapchain-created framebuffer */ - hr = _sapp_dxgi_GetBuffer(_sapp.d3d11.swap_chain, 0, _sapp_win32_refiid(_sapp_IID_ID3D11Texture2D), (void**)&_sapp.d3d11.rt); - SOKOL_ASSERT(SUCCEEDED(hr) && _sapp.d3d11.rt); - hr = _sapp_d3d11_CreateRenderTargetView(_sapp.d3d11.device, (ID3D11Resource*)_sapp.d3d11.rt, NULL, &_sapp.d3d11.rtv); - SOKOL_ASSERT(SUCCEEDED(hr) && _sapp.d3d11.rtv); - - /* common desc for MSAA and depth-stencil texture */ - _SAPP_STRUCT(D3D11_TEXTURE2D_DESC, tex_desc); - tex_desc.Width = (UINT)_sapp.framebuffer_width; - tex_desc.Height = (UINT)_sapp.framebuffer_height; - tex_desc.MipLevels = 1; - tex_desc.ArraySize = 1; - tex_desc.Usage = D3D11_USAGE_DEFAULT; - tex_desc.BindFlags = D3D11_BIND_RENDER_TARGET; - tex_desc.SampleDesc.Count = (UINT) _sapp.sample_count; - tex_desc.SampleDesc.Quality = (UINT) (_sapp.sample_count > 1 ? D3D11_STANDARD_MULTISAMPLE_PATTERN : 0); - - /* create MSAA texture and view if antialiasing requested */ - if (_sapp.sample_count > 1) { - tex_desc.Format = DXGI_FORMAT_R10G10B10A2_UNORM /* Modified by tettou771 for TrussC: 10-bit color */; - hr = _sapp_d3d11_CreateTexture2D(_sapp.d3d11.device, &tex_desc, NULL, &_sapp.d3d11.msaa_rt); - SOKOL_ASSERT(SUCCEEDED(hr) && _sapp.d3d11.msaa_rt); - hr = _sapp_d3d11_CreateRenderTargetView(_sapp.d3d11.device, (ID3D11Resource*)_sapp.d3d11.msaa_rt, NULL, &_sapp.d3d11.msaa_rtv); - SOKOL_ASSERT(SUCCEEDED(hr) && _sapp.d3d11.msaa_rtv); - } - - /* texture and view for the depth-stencil-surface */ - tex_desc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT; - tex_desc.BindFlags = D3D11_BIND_DEPTH_STENCIL; - hr = _sapp_d3d11_CreateTexture2D(_sapp.d3d11.device, &tex_desc, NULL, &_sapp.d3d11.ds); - SOKOL_ASSERT(SUCCEEDED(hr) && _sapp.d3d11.ds); - hr = _sapp_d3d11_CreateDepthStencilView(_sapp.d3d11.device, (ID3D11Resource*)_sapp.d3d11.ds, NULL, &_sapp.d3d11.dsv); - SOKOL_ASSERT(SUCCEEDED(hr) && _sapp.d3d11.dsv); -} - -_SOKOL_PRIVATE void _sapp_d3d11_destroy_default_render_target(void) { - _SAPP_SAFE_RELEASE(_sapp.d3d11.rt); - _SAPP_SAFE_RELEASE(_sapp.d3d11.rtv); - _SAPP_SAFE_RELEASE(_sapp.d3d11.msaa_rt); - _SAPP_SAFE_RELEASE(_sapp.d3d11.msaa_rtv); - _SAPP_SAFE_RELEASE(_sapp.d3d11.ds); - _SAPP_SAFE_RELEASE(_sapp.d3d11.dsv); -} - -_SOKOL_PRIVATE void _sapp_d3d11_resize_default_render_target(void) { - if (_sapp.d3d11.swap_chain) { - _sapp_d3d11_destroy_default_render_target(); - _sapp_dxgi_ResizeBuffers(_sapp.d3d11.swap_chain, _sapp.d3d11.swap_chain_desc.BufferCount, (UINT)_sapp.framebuffer_width, (UINT)_sapp.framebuffer_height, DXGI_FORMAT_R10G10B10A2_UNORM /* Modified by tettou771 for TrussC: 10-bit color */, 0); - _sapp_d3d11_create_default_render_target(); - } -} - -_SOKOL_PRIVATE void _sapp_d3d11_present(bool do_not_wait) { - // Modified by tettou771 for TrussC: skip present if flag is set (fix D3D11 flickering) - if (_sapp.skip_present) { - _sapp.skip_present = false; - return; - } - // end of modification - UINT flags = 0; - if (_sapp.win32.is_win10_or_greater && do_not_wait) { - /* this hack/workaround somewhat improves window-movement and -sizing - responsiveness when rendering is controlled via WM_TIMER during window - move and resize on NVIDIA cards on Win10 with recent drivers. - */ - flags = DXGI_PRESENT_DO_NOT_WAIT; - } - _sapp_dxgi_Present(_sapp.d3d11.swap_chain, (UINT)_sapp.swap_interval, flags); -} - -#endif /* SOKOL_D3D11 */ - -#if defined(SOKOL_GLCORE) -_SOKOL_PRIVATE void _sapp_wgl_init(void) { - _sapp.wgl.opengl32 = LoadLibraryA("opengl32.dll"); - if (!_sapp.wgl.opengl32) { - _SAPP_PANIC(WIN32_LOAD_OPENGL32_DLL_FAILED); - } - SOKOL_ASSERT(_sapp.wgl.opengl32); - _sapp.wgl.CreateContext = (PFN_wglCreateContext)(void*) GetProcAddress(_sapp.wgl.opengl32, "wglCreateContext"); - SOKOL_ASSERT(_sapp.wgl.CreateContext); - _sapp.wgl.DeleteContext = (PFN_wglDeleteContext)(void*) GetProcAddress(_sapp.wgl.opengl32, "wglDeleteContext"); - SOKOL_ASSERT(_sapp.wgl.DeleteContext); - _sapp.wgl.GetProcAddress = (PFN_wglGetProcAddress)(void*) GetProcAddress(_sapp.wgl.opengl32, "wglGetProcAddress"); - SOKOL_ASSERT(_sapp.wgl.GetProcAddress); - _sapp.wgl.GetCurrentDC = (PFN_wglGetCurrentDC)(void*) GetProcAddress(_sapp.wgl.opengl32, "wglGetCurrentDC"); - SOKOL_ASSERT(_sapp.wgl.GetCurrentDC); - _sapp.wgl.MakeCurrent = (PFN_wglMakeCurrent)(void*) GetProcAddress(_sapp.wgl.opengl32, "wglMakeCurrent"); - SOKOL_ASSERT(_sapp.wgl.MakeCurrent); - _sapp.wgl.GetIntegerv = (void(WINAPI*)(uint32_t, int32_t*)) GetProcAddress(_sapp.wgl.opengl32, "glGetIntegerv"); - SOKOL_ASSERT(_sapp.wgl.GetIntegerv); - - _sapp.wgl.msg_hwnd = CreateWindowExW(WS_EX_OVERLAPPEDWINDOW, - L"SOKOLAPP", - L"sokol-app message window", - WS_CLIPSIBLINGS|WS_CLIPCHILDREN, - 0, 0, 1, 1, - NULL, NULL, - GetModuleHandleW(NULL), - NULL); - if (!_sapp.wgl.msg_hwnd) { - _SAPP_PANIC(WIN32_CREATE_HELPER_WINDOW_FAILED); - } - SOKOL_ASSERT(_sapp.wgl.msg_hwnd); - ShowWindow(_sapp.wgl.msg_hwnd, SW_HIDE); - MSG msg; - while (PeekMessageW(&msg, _sapp.wgl.msg_hwnd, 0, 0, PM_REMOVE)) { - TranslateMessage(&msg); - DispatchMessageW(&msg); - } - _sapp.wgl.msg_dc = GetDC(_sapp.wgl.msg_hwnd); - if (!_sapp.wgl.msg_dc) { - _SAPP_PANIC(WIN32_HELPER_WINDOW_GETDC_FAILED); - } -} - -_SOKOL_PRIVATE void _sapp_wgl_shutdown(void) { - SOKOL_ASSERT(_sapp.wgl.opengl32 && _sapp.wgl.msg_hwnd); - DestroyWindow(_sapp.wgl.msg_hwnd); _sapp.wgl.msg_hwnd = 0; - FreeLibrary(_sapp.wgl.opengl32); _sapp.wgl.opengl32 = 0; -} - -_SOKOL_PRIVATE bool _sapp_wgl_has_ext(const char* ext, const char* extensions) { - SOKOL_ASSERT(ext && extensions); - const char* start = extensions; - while (true) { - const char* where = strstr(start, ext); - if (!where) { - return false; - } - const char* terminator = where + strlen(ext); - if ((where == start) || (*(where - 1) == ' ')) { - if (*terminator == ' ' || *terminator == '\0') { - break; - } - } - start = terminator; - } - return true; -} - -_SOKOL_PRIVATE bool _sapp_wgl_ext_supported(const char* ext) { - SOKOL_ASSERT(ext); - if (_sapp.wgl.GetExtensionsStringEXT) { - const char* extensions = _sapp.wgl.GetExtensionsStringEXT(); - if (extensions) { - if (_sapp_wgl_has_ext(ext, extensions)) { - return true; - } - } - } - if (_sapp.wgl.GetExtensionsStringARB) { - const char* extensions = _sapp.wgl.GetExtensionsStringARB(_sapp.wgl.GetCurrentDC()); - if (extensions) { - if (_sapp_wgl_has_ext(ext, extensions)) { - return true; - } - } - } - return false; -} - -_SOKOL_PRIVATE void _sapp_wgl_load_extensions(void) { - SOKOL_ASSERT(_sapp.wgl.msg_dc); - _SAPP_STRUCT(PIXELFORMATDESCRIPTOR, pfd); - pfd.nSize = sizeof(pfd); - pfd.nVersion = 1; - pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER; - pfd.iPixelType = PFD_TYPE_RGBA; - pfd.cColorBits = 24; - if (!SetPixelFormat(_sapp.wgl.msg_dc, ChoosePixelFormat(_sapp.wgl.msg_dc, &pfd), &pfd)) { - _SAPP_PANIC(WIN32_DUMMY_CONTEXT_SET_PIXELFORMAT_FAILED); - } - HGLRC rc = _sapp.wgl.CreateContext(_sapp.wgl.msg_dc); - if (!rc) { - _SAPP_PANIC(WIN32_CREATE_DUMMY_CONTEXT_FAILED); - } - if (!_sapp.wgl.MakeCurrent(_sapp.wgl.msg_dc, rc)) { - _SAPP_PANIC(WIN32_DUMMY_CONTEXT_MAKE_CURRENT_FAILED); - } - _sapp.wgl.GetExtensionsStringEXT = (PFNWGLGETEXTENSIONSSTRINGEXTPROC)(void*) _sapp.wgl.GetProcAddress("wglGetExtensionsStringEXT"); - _sapp.wgl.GetExtensionsStringARB = (PFNWGLGETEXTENSIONSSTRINGARBPROC)(void*) _sapp.wgl.GetProcAddress("wglGetExtensionsStringARB"); - _sapp.wgl.CreateContextAttribsARB = (PFNWGLCREATECONTEXTATTRIBSARBPROC)(void*) _sapp.wgl.GetProcAddress("wglCreateContextAttribsARB"); - _sapp.wgl.SwapIntervalEXT = (PFNWGLSWAPINTERVALEXTPROC)(void*) _sapp.wgl.GetProcAddress("wglSwapIntervalEXT"); - _sapp.wgl.GetPixelFormatAttribivARB = (PFNWGLGETPIXELFORMATATTRIBIVARBPROC)(void*) _sapp.wgl.GetProcAddress("wglGetPixelFormatAttribivARB"); - _sapp.wgl.arb_multisample = _sapp_wgl_ext_supported("WGL_ARB_multisample"); - _sapp.wgl.arb_create_context = _sapp_wgl_ext_supported("WGL_ARB_create_context"); - _sapp.wgl.arb_create_context_profile = _sapp_wgl_ext_supported("WGL_ARB_create_context_profile"); - _sapp.wgl.ext_swap_control = _sapp_wgl_ext_supported("WGL_EXT_swap_control"); - _sapp.wgl.arb_pixel_format = _sapp_wgl_ext_supported("WGL_ARB_pixel_format"); - _sapp.wgl.MakeCurrent(_sapp.wgl.msg_dc, 0); - _sapp.wgl.DeleteContext(rc); -} - -_SOKOL_PRIVATE int _sapp_wgl_attrib(int pixel_format, int attrib) { - SOKOL_ASSERT(_sapp.wgl.arb_pixel_format); - int value = 0; - if (!_sapp.wgl.GetPixelFormatAttribivARB(_sapp.win32.dc, pixel_format, 0, 1, &attrib, &value)) { - _SAPP_PANIC(WIN32_GET_PIXELFORMAT_ATTRIB_FAILED); - } - return value; -} - -_SOKOL_PRIVATE void _sapp_wgl_attribiv(int pixel_format, int num_attribs, const int* attribs, int* results) { - SOKOL_ASSERT(_sapp.wgl.arb_pixel_format); - if (!_sapp.wgl.GetPixelFormatAttribivARB(_sapp.win32.dc, pixel_format, 0, num_attribs, attribs, results)) { - _SAPP_PANIC(WIN32_GET_PIXELFORMAT_ATTRIB_FAILED); - } -} - -_SOKOL_PRIVATE int _sapp_wgl_find_pixel_format(void) { - SOKOL_ASSERT(_sapp.win32.dc); - SOKOL_ASSERT(_sapp.wgl.arb_pixel_format); - - #define _sapp_wgl_num_query_tags (12) - const int query_tags[_sapp_wgl_num_query_tags] = { - WGL_SUPPORT_OPENGL_ARB, - WGL_DRAW_TO_WINDOW_ARB, - WGL_PIXEL_TYPE_ARB, - WGL_ACCELERATION_ARB, - WGL_DOUBLE_BUFFER_ARB, - WGL_RED_BITS_ARB, - WGL_GREEN_BITS_ARB, - WGL_BLUE_BITS_ARB, - WGL_ALPHA_BITS_ARB, - WGL_DEPTH_BITS_ARB, - WGL_STENCIL_BITS_ARB, - WGL_SAMPLES_ARB, - }; - const int result_support_opengl_index = 0; - const int result_draw_to_window_index = 1; - const int result_pixel_type_index = 2; - const int result_acceleration_index = 3; - const int result_double_buffer_index = 4; - const int result_red_bits_index = 5; - const int result_green_bits_index = 6; - const int result_blue_bits_index = 7; - const int result_alpha_bits_index = 8; - const int result_depth_bits_index = 9; - const int result_stencil_bits_index = 10; - const int result_samples_index = 11; - - int query_results[_sapp_wgl_num_query_tags] = {0}; - // Drop the last item if multisample extension is not supported. - // If in future querying with multiple extensions, will have to shuffle index values to have active extensions on the end. - int query_count = _sapp_wgl_num_query_tags; - if (!_sapp.wgl.arb_multisample) { - query_count = _sapp_wgl_num_query_tags - 1; - } - - int native_count = _sapp_wgl_attrib(1, WGL_NUMBER_PIXEL_FORMATS_ARB); - - _sapp_gl_fbconfig desired; - _sapp_gl_init_fbconfig(&desired); - desired.red_bits = 8; - desired.green_bits = 8; - desired.blue_bits = 8; - desired.alpha_bits = 8; - desired.depth_bits = 24; - desired.stencil_bits = 8; - desired.doublebuffer = true; - desired.samples = (_sapp.sample_count > 1) ? _sapp.sample_count : 0; - - int pixel_format = 0; - - _sapp_gl_fbselect fbselect; - _sapp_gl_init_fbselect(&fbselect); - for (int i = 0; i < native_count; i++) { - const int n = i + 1; - _sapp_wgl_attribiv(n, query_count, query_tags, query_results); - - if (query_results[result_support_opengl_index] == 0 - || query_results[result_draw_to_window_index] == 0 - || query_results[result_pixel_type_index] != WGL_TYPE_RGBA_ARB - || query_results[result_acceleration_index] == WGL_NO_ACCELERATION_ARB) - { - continue; - } - - _SAPP_STRUCT(_sapp_gl_fbconfig, u); - u.red_bits = query_results[result_red_bits_index]; - u.green_bits = query_results[result_green_bits_index]; - u.blue_bits = query_results[result_blue_bits_index]; - u.alpha_bits = query_results[result_alpha_bits_index]; - u.depth_bits = query_results[result_depth_bits_index]; - u.stencil_bits = query_results[result_stencil_bits_index]; - u.doublebuffer = 0 != query_results[result_double_buffer_index]; - u.samples = query_results[result_samples_index]; // NOTE: If arb_multisample is not supported - just takes the default 0 - - // Test if this pixel format is better than the previous one - if (_sapp_gl_select_fbconfig(&fbselect, &desired, &u)) { - pixel_format = (uintptr_t)n; - - // Early exit if matching as good as possible - if (fbselect.best_match) { - break; - } - } - } - - return pixel_format; -} - -_SOKOL_PRIVATE void _sapp_wgl_create_context(void) { - int pixel_format = _sapp_wgl_find_pixel_format(); - if (0 == pixel_format) { - _SAPP_PANIC(WIN32_WGL_FIND_PIXELFORMAT_FAILED); - } - PIXELFORMATDESCRIPTOR pfd; - if (!DescribePixelFormat(_sapp.win32.dc, pixel_format, sizeof(pfd), &pfd)) { - _SAPP_PANIC(WIN32_WGL_DESCRIBE_PIXELFORMAT_FAILED); - } - if (!SetPixelFormat(_sapp.win32.dc, pixel_format, &pfd)) { - _SAPP_PANIC(WIN32_WGL_SET_PIXELFORMAT_FAILED); - } - if (!_sapp.wgl.arb_create_context) { - _SAPP_PANIC(WIN32_WGL_ARB_CREATE_CONTEXT_REQUIRED); - } - if (!_sapp.wgl.arb_create_context_profile) { - _SAPP_PANIC(WIN32_WGL_ARB_CREATE_CONTEXT_PROFILE_REQUIRED); - } - const int attrs[] = { - WGL_CONTEXT_MAJOR_VERSION_ARB, _sapp.desc.gl.major_version, - WGL_CONTEXT_MINOR_VERSION_ARB, _sapp.desc.gl.minor_version, -#if defined(SOKOL_DEBUG) - WGL_CONTEXT_FLAGS_ARB, WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB | WGL_CONTEXT_DEBUG_BIT_ARB, -#else - WGL_CONTEXT_FLAGS_ARB, WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB, -#endif - WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB, - 0, 0 - }; - _sapp.wgl.gl_ctx = _sapp.wgl.CreateContextAttribsARB(_sapp.win32.dc, 0, attrs); - if (!_sapp.wgl.gl_ctx) { - const DWORD err = GetLastError(); - if (err == (0xc0070000 | ERROR_INVALID_VERSION_ARB)) { - _SAPP_PANIC(WIN32_WGL_OPENGL_VERSION_NOT_SUPPORTED); - } else if (err == (0xc0070000 | ERROR_INVALID_PROFILE_ARB)) { - _SAPP_PANIC(WIN32_WGL_OPENGL_PROFILE_NOT_SUPPORTED); - } else if (err == (0xc0070000 | ERROR_INCOMPATIBLE_DEVICE_CONTEXTS_ARB)) { - _SAPP_PANIC(WIN32_WGL_INCOMPATIBLE_DEVICE_CONTEXT); - } else { - _SAPP_PANIC(WIN32_WGL_CREATE_CONTEXT_ATTRIBS_FAILED_OTHER); - } - } - _sapp.wgl.MakeCurrent(_sapp.win32.dc, _sapp.wgl.gl_ctx); - if (_sapp.wgl.ext_swap_control) { - /* FIXME: DwmIsCompositionEnabled() (see GLFW) */ - _sapp.wgl.SwapIntervalEXT(_sapp.swap_interval); - } - const uint32_t gl_framebuffer_binding = 0x8CA6; - _sapp.wgl.GetIntegerv(gl_framebuffer_binding, (int32_t*)&_sapp.gl.framebuffer); -} - -_SOKOL_PRIVATE void _sapp_wgl_destroy_context(void) { - SOKOL_ASSERT(_sapp.wgl.gl_ctx); - _sapp.wgl.DeleteContext(_sapp.wgl.gl_ctx); - _sapp.wgl.gl_ctx = 0; -} - -_SOKOL_PRIVATE void _sapp_wgl_swap_buffers(void) { - // Modified by tettou771 for TrussC: skip present support - if (_sapp.skip_present) { _sapp.skip_present = false; return; } - SOKOL_ASSERT(_sapp.win32.dc); - /* FIXME: DwmIsCompositionEnabled? (see GLFW) */ - SwapBuffers(_sapp.win32.dc); -} -#endif /* SOKOL_GLCORE */ - -_SOKOL_PRIVATE bool _sapp_win32_wide_to_utf8(const wchar_t* src, char* dst, int dst_num_bytes) { - SOKOL_ASSERT(src && dst && (dst_num_bytes > 1)); - _sapp_clear(dst, (size_t)dst_num_bytes); - const int bytes_needed = WideCharToMultiByte(CP_UTF8, 0, src, -1, NULL, 0, NULL, NULL); - if (bytes_needed <= dst_num_bytes) { - WideCharToMultiByte(CP_UTF8, 0, src, -1, dst, dst_num_bytes, NULL, NULL); - return true; - } else { - return false; - } -} - -/* updates current window and framebuffer size from the window's client rect, returns true if size has changed */ -_SOKOL_PRIVATE bool _sapp_win32_update_dimensions(void) { - RECT rect; - if (GetClientRect(_sapp.win32.hwnd, &rect)) { - float window_width = (float)(rect.right - rect.left) / _sapp.win32.dpi.window_scale; - float window_height = (float)(rect.bottom - rect.top) / _sapp.win32.dpi.window_scale; - if ((window_width == 0.0f) && (window_height == 0.0f)) { - // both width and height being zero means the window is minimized, in that - // case pretend that the size didn't change (this is consistent with other - // window systems) - also see: https://github.com/floooh/sokol/issues/1465 - return false; - } - _sapp.window_width = _sapp_roundf_gzero(window_width); - _sapp.window_height = _sapp_roundf_gzero(window_height); - // NOTE: on Vulkan, updating the framebuffer dimensions and firing the resize-event - // is handled entirely by the swapchain management code - #if !defined(SOKOL_VULKAN) - int fb_width = _sapp_roundf_gzero(window_width * _sapp.win32.dpi.content_scale); - int fb_height = _sapp_roundf_gzero(window_height * _sapp.win32.dpi.content_scale); - if ((fb_width != _sapp.framebuffer_width) || (fb_height != _sapp.framebuffer_height)) { - _sapp.framebuffer_width = fb_width; - _sapp.framebuffer_height = fb_height; - return true; - } - #endif - } else { - _sapp.window_width = _sapp.window_height = 1; - #if !defined(SOKOL_VULKAN) - _sapp.framebuffer_width = _sapp.framebuffer_height = 1; - #endif - } - return false; -} - -_SOKOL_PRIVATE void _sapp_win32_set_fullscreen(bool fullscreen, UINT swp_flags) { - HMONITOR monitor = MonitorFromWindow(_sapp.win32.hwnd, MONITOR_DEFAULTTONEAREST); - _SAPP_STRUCT(MONITORINFO, minfo); - minfo.cbSize = sizeof(MONITORINFO); - GetMonitorInfo(monitor, &minfo); - const RECT mr = minfo.rcMonitor; - const int monitor_w = mr.right - mr.left; - const int monitor_h = mr.bottom - mr.top; - - const DWORD win_ex_style = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE; - DWORD win_style; - RECT rect = { 0, 0, 0, 0 }; - - _sapp.fullscreen = fullscreen; - if (!_sapp.fullscreen) { - win_style = WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SIZEBOX; - rect = _sapp.win32.stored_window_rect; - } else { - GetWindowRect(_sapp.win32.hwnd, &_sapp.win32.stored_window_rect); - win_style = WS_POPUP | WS_SYSMENU | WS_VISIBLE; - rect.left = mr.left; - rect.top = mr.top; - rect.right = rect.left + monitor_w; - rect.bottom = rect.top + monitor_h; - AdjustWindowRectEx(&rect, win_style, FALSE, win_ex_style); - } - const int win_w = rect.right - rect.left; - const int win_h = rect.bottom - rect.top; - const int win_x = rect.left; - const int win_y = rect.top; - SetWindowLongPtr(_sapp.win32.hwnd, GWL_STYLE, win_style); - SetWindowPos(_sapp.win32.hwnd, HWND_TOP, win_x, win_y, win_w, win_h, swp_flags | SWP_FRAMECHANGED); -} - -_SOKOL_PRIVATE void _sapp_win32_toggle_fullscreen(void) { - _sapp_win32_set_fullscreen(!_sapp.fullscreen, SWP_SHOWWINDOW); -} - -_SOKOL_PRIVATE void _sapp_win32_init_cursor(sapp_mouse_cursor cursor) { - SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); - // NOTE: the OCR_* constants are only defined if OEMRESOURCE is defined - // before windows.h is included, but we can't guarantee that because - // the sokol_app.h implementation may be included with other implementations - // in the same compilation unit - int id = 0; - switch (cursor) { - case SAPP_MOUSECURSOR_ARROW: id = 32512; break; // OCR_NORMAL - case SAPP_MOUSECURSOR_IBEAM: id = 32513; break; // OCR_IBEAM - case SAPP_MOUSECURSOR_CROSSHAIR: id = 32515; break; // OCR_CROSS - case SAPP_MOUSECURSOR_POINTING_HAND: id = 32649; break; // OCR_HAND - case SAPP_MOUSECURSOR_RESIZE_EW: id = 32644; break; // OCR_SIZEWE - case SAPP_MOUSECURSOR_RESIZE_NS: id = 32645; break; // OCR_SIZENS - case SAPP_MOUSECURSOR_RESIZE_NWSE: id = 32642; break; // OCR_SIZENWSE - case SAPP_MOUSECURSOR_RESIZE_NESW: id = 32643; break; // OCR_SIZENESW - case SAPP_MOUSECURSOR_RESIZE_ALL: id = 32646; break; // OCR_SIZEALL - case SAPP_MOUSECURSOR_NOT_ALLOWED: id = 32648; break; // OCR_NO - default: break; - } - if (id != 0) { - _sapp.win32.standard_cursors[cursor] = (HCURSOR)LoadImageW(NULL, MAKEINTRESOURCEW(id), IMAGE_CURSOR, 0, 0, LR_DEFAULTSIZE|LR_SHARED); - } - // fallback: default cursor - if (0 == _sapp.win32.standard_cursors[cursor]) { - // 32512 => IDC_ARROW - _sapp.win32.standard_cursors[cursor] = LoadCursorW(NULL, MAKEINTRESOURCEW(32512)); - } - SOKOL_ASSERT(0 != _sapp.win32.standard_cursors[cursor]); -} - -_SOKOL_PRIVATE void _sapp_win32_init_cursors(void) { - for (int i = 0; i < _SAPP_MOUSECURSOR_NUM; i++) { - _sapp_win32_init_cursor((sapp_mouse_cursor)i); - } -} - -_SOKOL_PRIVATE bool _sapp_win32_cursor_in_content_area(void) { - POINT pos; - if (!GetCursorPos(&pos)) { - return false; - } - if (WindowFromPoint(pos) != _sapp.win32.hwnd) { - return false; - } - RECT area; - GetClientRect(_sapp.win32.hwnd, &area); - ClientToScreen(_sapp.win32.hwnd, (POINT*)&area.left); - ClientToScreen(_sapp.win32.hwnd, (POINT*)&area.right); - return PtInRect(&area, pos) == TRUE; -} - -_SOKOL_PRIVATE void _sapp_win32_update_cursor(sapp_mouse_cursor cursor, bool shown, bool skip_area_test) { - SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); - - // NOTE: when called from WM_SETCURSOR, the area test would be redundant - if (!skip_area_test) { - if (!_sapp_win32_cursor_in_content_area()) { - return; - } - } - HCURSOR cursor_handle = NULL; - if (shown) { - if (_sapp.custom_cursor_bound[cursor]) { - SOKOL_ASSERT(_sapp.win32.custom_cursors[cursor]); - cursor_handle = _sapp.win32.custom_cursors[cursor]; - SOKOL_ASSERT(0 != cursor_handle); - } else { - cursor_handle = _sapp.win32.standard_cursors[cursor]; - SOKOL_ASSERT(0 != cursor_handle); - } - } - SetCursor(cursor_handle); -} - -_SOKOL_PRIVATE void _sapp_win32_capture_mouse(uint8_t btn_mask) { - if (0 == _sapp.win32.mouse.capture_mask) { - SetCapture(_sapp.win32.hwnd); - } - _sapp.win32.mouse.capture_mask |= btn_mask; -} - -_SOKOL_PRIVATE void _sapp_win32_release_mouse(uint8_t btn_mask) { - if (0 != _sapp.win32.mouse.capture_mask) { - _sapp.win32.mouse.capture_mask &= ~btn_mask; - if (0 == _sapp.win32.mouse.capture_mask) { - ReleaseCapture(); - } - } -} - -_SOKOL_PRIVATE bool _sapp_win32_is_foreground_window(void) { - return _sapp.win32.hwnd == GetForegroundWindow(); -} - -_SOKOL_PRIVATE void _sapp_win32_lock_mouse(bool lock) { - _sapp.win32.mouse.requested_lock = lock; -} - -_SOKOL_PRIVATE void _sapp_win32_free_raw_input_data(void) { - if (_sapp.win32.raw_input_data.ptr) { - _sapp_free(_sapp.win32.raw_input_data.ptr); - _sapp.win32.raw_input_data.ptr = 0; - _sapp.win32.raw_input_data.size = 0; - } -} - -_SOKOL_PRIVATE void _sapp_win32_alloc_raw_input_data(size_t size) { - SOKOL_ASSERT(!_sapp.win32.raw_input_data.ptr); - SOKOL_ASSERT(size > 0); - _sapp.win32.raw_input_data.ptr = _sapp_malloc(size); - _sapp.win32.raw_input_data.size = size; - SOKOL_ASSERT(_sapp.win32.raw_input_data.ptr); -} - -_SOKOL_PRIVATE void* _sapp_win32_ensure_raw_input_data(size_t required_size) { - if (required_size > _sapp.win32.raw_input_data.size) { - _sapp_win32_free_raw_input_data(); - _sapp_win32_alloc_raw_input_data(required_size); - } - // we expect that malloc() returns at least 8-byte aligned memory - SOKOL_ASSERT((((uintptr_t)_sapp.win32.raw_input_data.ptr) & 7) == 0); - return _sapp.win32.raw_input_data.ptr; -} - -_SOKOL_PRIVATE void _sapp_win32_do_lock_mouse(void) { - _sapp.mouse.locked = true; - - // hide mouse cursor (NOTE: this maintains a hidden counter, but since - // only mouse-lock uses ShowCursor this doesn't matter) - ShowCursor(FALSE); - - // reset dx/dy and release any active mouse capture - _sapp.mouse.dx = 0.0f; - _sapp.mouse.dy = 0.0f; - _sapp_win32_release_mouse(0xFF); - - // store current mouse position so that it can be restored when unlocked - POINT pos; - if (GetCursorPos(&pos)) { - _sapp.win32.mouse.lock.pos_valid = true; - _sapp.win32.mouse.lock.pos_x = pos.x; - _sapp.win32.mouse.lock.pos_y = pos.y; - } else { - _sapp.win32.mouse.lock.pos_valid = false; - } - - // while mouse is locked, restrict cursor movement to the client - // rectangle so that we don't loose any mouse movement events - RECT client_rect; - GetClientRect(_sapp.win32.hwnd, &client_rect); - POINT mid_point; - mid_point.x = (client_rect.right - client_rect.left) / 2; - mid_point.y = (client_rect.bottom - client_rect.top) / 2; - ClientToScreen(_sapp.win32.hwnd, &mid_point); - RECT clip_rect; - clip_rect.left = clip_rect.right = mid_point.x; - clip_rect.top = clip_rect.bottom = mid_point.y; - ClipCursor(&clip_rect); - - // enable raw input for mouse, starts sending WM_INPUT messages to WinProc (see GLFW) - const RAWINPUTDEVICE rid = { - 0x01, // usUsagePage: HID_USAGE_PAGE_GENERIC - 0x02, // usUsage: HID_USAGE_GENERIC_MOUSE - 0, // dwFlags - _sapp.win32.hwnd // hwndTarget - }; - if (!RegisterRawInputDevices(&rid, 1, sizeof(rid))) { - _SAPP_ERROR(WIN32_REGISTER_RAW_INPUT_DEVICES_FAILED_MOUSE_LOCK); - } - // in case the raw mouse device only supports absolute position reporting, - // we need to skip the dx/dy compution for the first WM_INPUT event - _sapp.win32.mouse.raw_input.pos_valid = false; -} - -_SOKOL_PRIVATE void _sapp_win32_do_unlock_mouse(void) { - _sapp.mouse.locked = false; - - // make mouse cursor visible - ShowCursor(TRUE); - - // reset dx/dy and release any active mouse capture - _sapp.mouse.dx = 0.0f; - _sapp.mouse.dy = 0.0f; - _sapp_win32_release_mouse(0xFF); - - // disable raw input for mouse - const RAWINPUTDEVICE rid = { 0x01, 0x02, RIDEV_REMOVE, NULL }; - if (!RegisterRawInputDevices(&rid, 1, sizeof(rid))) { - _SAPP_ERROR(WIN32_REGISTER_RAW_INPUT_DEVICES_FAILED_MOUSE_UNLOCK); - } - - // unrestrict mouse movement - ClipCursor(NULL); - - // restore the 'pre-locked' mouse position - if (_sapp.win32.mouse.lock.pos_valid) { - SetCursorPos(_sapp.win32.mouse.lock.pos_x, _sapp.win32.mouse.lock.pos_y); - _sapp.win32.mouse.lock.pos_valid = false; - } -} - -_SOKOL_PRIVATE void _sapp_win32_update_mouse_lock(void) { - // mouse lock can only be active when we're the active window - if (!_sapp_win32_is_foreground_window()) { - // unlock mouse if currently locked - if (_sapp.mouse.locked) { - _sapp_win32_do_unlock_mouse(); - } - return; - } - - // nothing to do if requested lock state matches current lock state - const bool lock = _sapp.win32.mouse.requested_lock; - if (lock == _sapp.mouse.locked) { - return; - } - - // otherwise change into desired state - if (lock) { - _sapp_win32_do_lock_mouse(); - } else { - _sapp_win32_do_unlock_mouse(); - } -} - -_SOKOL_PRIVATE bool _sapp_win32_update_monitor(void) { - const HMONITOR cur_monitor = MonitorFromWindow(_sapp.win32.hwnd, MONITOR_DEFAULTTONULL); - if (cur_monitor != _sapp.win32.hmonitor) { - _sapp.win32.hmonitor = cur_monitor; - return true; - } else { - return false; - } -} - -_SOKOL_PRIVATE uint32_t _sapp_win32_mods(void) { - uint32_t mods = 0; - if (GetKeyState(VK_SHIFT) & (1<<15)) { - mods |= SAPP_MODIFIER_SHIFT; - } - if (GetKeyState(VK_CONTROL) & (1<<15)) { - mods |= SAPP_MODIFIER_CTRL; - } - if (GetKeyState(VK_MENU) & (1<<15)) { - mods |= SAPP_MODIFIER_ALT; - } - if ((GetKeyState(VK_LWIN) | GetKeyState(VK_RWIN)) & (1<<15)) { - mods |= SAPP_MODIFIER_SUPER; - } - const bool swapped = (TRUE == GetSystemMetrics(SM_SWAPBUTTON)); - if (GetAsyncKeyState(VK_LBUTTON)) { - mods |= swapped ? SAPP_MODIFIER_RMB : SAPP_MODIFIER_LMB; - } - if (GetAsyncKeyState(VK_RBUTTON)) { - mods |= swapped ? SAPP_MODIFIER_LMB : SAPP_MODIFIER_RMB; - } - if (GetAsyncKeyState(VK_MBUTTON)) { - mods |= SAPP_MODIFIER_MMB; - } - return mods; -} - -_SOKOL_PRIVATE void _sapp_win32_mouse_update(LPARAM lParam) { - if (!_sapp.mouse.locked) { - const float new_x = (float)GET_X_LPARAM(lParam) * _sapp.win32.dpi.mouse_scale; - const float new_y = (float)GET_Y_LPARAM(lParam) * _sapp.win32.dpi.mouse_scale; - if (_sapp.mouse.pos_valid) { - // don't update dx/dy in the very first event - _sapp.mouse.dx = new_x - _sapp.mouse.x; - _sapp.mouse.dy = new_y - _sapp.mouse.y; - } - _sapp.mouse.x = new_x; - _sapp.mouse.y = new_y; - _sapp.mouse.pos_valid = true; - } -} - -_SOKOL_PRIVATE void _sapp_win32_mouse_event(sapp_event_type type, sapp_mousebutton btn) { - if (_sapp_events_enabled()) { - _sapp_init_event(type); - _sapp.event.modifiers = _sapp_win32_mods(); - _sapp.event.mouse_button = btn; - _sapp_call_event(&_sapp.event); - } -} - -_SOKOL_PRIVATE void _sapp_win32_scroll_event(float x, float y) { - if (_sapp_events_enabled()) { - _sapp_init_event(SAPP_EVENTTYPE_MOUSE_SCROLL); - _sapp.event.modifiers = _sapp_win32_mods(); - _sapp.event.scroll_x = x; - _sapp.event.scroll_y = y; - _sapp_call_event(&_sapp.event); - } -} - -_SOKOL_PRIVATE void _sapp_win32_key_event(sapp_event_type type, int vk, bool repeat) { - if (_sapp_events_enabled() && (vk < SAPP_MAX_KEYCODES)) { - _sapp_init_event(type); - _sapp.event.modifiers = _sapp_win32_mods(); - _sapp.event.key_code = _sapp.keycodes[vk]; - _sapp.event.key_repeat = repeat; - _sapp_call_event(&_sapp.event); - /* check if a CLIPBOARD_PASTED event must be sent too */ - if (_sapp.clipboard.enabled && - (type == SAPP_EVENTTYPE_KEY_DOWN) && - (_sapp.event.modifiers == SAPP_MODIFIER_CTRL) && - (_sapp.event.key_code == SAPP_KEYCODE_V)) - { - _sapp_init_event(SAPP_EVENTTYPE_CLIPBOARD_PASTED); - _sapp_call_event(&_sapp.event); - } - } -} - -_SOKOL_PRIVATE void _sapp_win32_char_event(uint32_t c, bool repeat) { - if (_sapp_events_enabled() && (c >= 32)) { - if (c >= 0xD800 && c <= 0xDBFF) { - _sapp.win32.surrogate = (WCHAR)c - 0xD800; - } else { - if (c > 0xDC00 && c <= 0xDFFF) { - c = (uint32_t)(_sapp.win32.surrogate) << 10 | (c - 0xDC00); - c += 0x10000; - _sapp.win32.surrogate = 0; - } - _sapp_init_event(SAPP_EVENTTYPE_CHAR); - _sapp.event.modifiers = _sapp_win32_mods(); - _sapp.event.char_code = c; - _sapp.event.key_repeat = repeat; - _sapp_call_event(&_sapp.event); - } - } -} - -_SOKOL_PRIVATE void _sapp_win32_dpi_changed(HWND hWnd, LPRECT proposed_win_rect) { - if (!_sapp.win32.dpi.aware) { - return; - } - HINSTANCE user32 = LoadLibraryA("user32.dll"); - if (!user32) { - return; - } - typedef UINT(WINAPI * GETDPIFORWINDOW_T)(HWND hwnd); - GETDPIFORWINDOW_T fn_getdpiforwindow = (GETDPIFORWINDOW_T)(void*)GetProcAddress(user32, "GetDpiForWindow"); - if (fn_getdpiforwindow) { - UINT dpix = fn_getdpiforwindow(_sapp.win32.hwnd); - _sapp.win32.dpi.window_scale = (float)dpix / 96.0f; - if (_sapp.desc.high_dpi) { - _sapp.win32.dpi.content_scale = _sapp.win32.dpi.window_scale; - _sapp.win32.dpi.mouse_scale = 1.0f; - } else { - _sapp.win32.dpi.content_scale = 1.0f; - _sapp.win32.dpi.mouse_scale = 1.0f / _sapp.win32.dpi.window_scale; - } - _sapp.dpi_scale = _sapp.win32.dpi.content_scale; - SetWindowPos(hWnd, 0, - proposed_win_rect->left, - proposed_win_rect->top, - proposed_win_rect->right - proposed_win_rect->left, - proposed_win_rect->bottom - proposed_win_rect->top, - SWP_NOZORDER | SWP_NOACTIVATE); - } - FreeLibrary(user32); -} - -_SOKOL_PRIVATE void _sapp_win32_files_dropped(HDROP hdrop) { - if (!_sapp.drop.enabled) { - return; - } - _sapp_clear_drop_buffer(); - bool drop_failed = false; - const int count = (int) DragQueryFileW(hdrop, 0xffffffff, NULL, 0); - _sapp.drop.num_files = (count > _sapp.drop.max_files) ? _sapp.drop.max_files : count; - for (UINT i = 0; i < (UINT)_sapp.drop.num_files; i++) { - const UINT num_chars = DragQueryFileW(hdrop, i, NULL, 0) + 1; - WCHAR* buffer = (WCHAR*) _sapp_malloc_clear(num_chars * sizeof(WCHAR)); - DragQueryFileW(hdrop, i, buffer, num_chars); - if (!_sapp_win32_wide_to_utf8(buffer, _sapp_dropped_file_path_ptr((int)i), _sapp.drop.max_path_length)) { - _SAPP_ERROR(DROPPED_FILE_PATH_TOO_LONG); - drop_failed = true; - } - _sapp_free(buffer); - } - DragFinish(hdrop); - if (!drop_failed) { - if (_sapp_events_enabled()) { - _sapp_init_event(SAPP_EVENTTYPE_FILES_DROPPED); - _sapp.event.modifiers = _sapp_win32_mods(); - _sapp_call_event(&_sapp.event); - } - } else { - _sapp_clear_drop_buffer(); - _sapp.drop.num_files = 0; - } -} - -_SOKOL_PRIVATE void _sapp_win32_frame(bool from_winproc) { - #if defined(SOKOL_WGPU) - _sapp_wgpu_frame(); - #elif defined(SOKOL_VULKAN) - _sapp_vk_frame(); - #else - _sapp_frame(); - #endif - #if defined(SOKOL_D3D11) - bool do_not_wait = from_winproc; - _sapp_d3d11_present(do_not_wait); - #endif - #if defined(SOKOL_GLCORE) - _sapp_wgl_swap_buffers(); - #endif - if (!from_winproc) { - if (IsIconic(_sapp.win32.hwnd)) { - Sleep((DWORD)(16 * _sapp.swap_interval)); - } - } -} - -_SOKOL_PRIVATE LRESULT CALLBACK _sapp_win32_wndproc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { - if (!_sapp.win32.in_create_window) { - switch (uMsg) { - case WM_CLOSE: - /* only give user a chance to intervene when sapp_quit() wasn't already called */ - if (!_sapp.quit_ordered) { - /* if window should be closed and event handling is enabled, give user code - a change to intervene via sapp_cancel_quit() - */ - _sapp.quit_requested = true; - _sapp_win32_app_event(SAPP_EVENTTYPE_QUIT_REQUESTED); - /* if user code hasn't intervened, quit the app */ - if (_sapp.quit_requested) { - _sapp.quit_ordered = true; - } - } - if (_sapp.quit_ordered) { - PostQuitMessage(0); - } - return 0; - case WM_SYSCOMMAND: - switch (wParam & 0xFFF0) { - case SC_SCREENSAVE: - case SC_MONITORPOWER: - if (_sapp.fullscreen) { - /* disable screen saver and blanking in fullscreen mode */ - return 0; - } - break; - case SC_KEYMENU: - /* user trying to access menu via ALT */ - return 0; - } - break; - case WM_ERASEBKGND: - return 1; - case WM_SIZE: - { - const bool iconified = wParam == SIZE_MINIMIZED; - if (iconified != _sapp.win32.iconified) { - _sapp.win32.iconified = iconified; - if (iconified) { - _sapp_win32_app_event(SAPP_EVENTTYPE_ICONIFIED); - } else { - _sapp_win32_app_event(SAPP_EVENTTYPE_RESTORED); - } - } - } - break; - case WM_SETFOCUS: - _sapp_win32_app_event(SAPP_EVENTTYPE_FOCUSED); - break; - case WM_KILLFOCUS: - _sapp_win32_app_event(SAPP_EVENTTYPE_UNFOCUSED); - break; - case WM_SETCURSOR: - if (LOWORD(lParam) == HTCLIENT) { - _sapp_win32_update_cursor(_sapp.mouse.current_cursor, _sapp.mouse.shown, true); - return TRUE; - } - break; - case WM_DPICHANGED: - { - /* Update window's DPI and size if its moved to another monitor with a different DPI - Only sent if DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 is used. - */ - _sapp_win32_dpi_changed(hWnd, (LPRECT)lParam); - break; - } - case WM_LBUTTONDOWN: - _sapp_win32_mouse_update(lParam); - _sapp_win32_mouse_event(SAPP_EVENTTYPE_MOUSE_DOWN, SAPP_MOUSEBUTTON_LEFT); - _sapp_win32_capture_mouse(1<data.mouse.usFlags & MOUSE_MOVE_ABSOLUTE) { - /* mouse only reports absolute position - NOTE: This code is untested and will most likely behave wrong in Remote Desktop sessions. - (such remote desktop sessions are setting the MOUSE_MOVE_ABSOLUTE flag). - See: https://github.com/floooh/sokol/issues/806 and - https://github.com/microsoft/DirectXTK/commit/ef56b63f3739381e451f7a5a5bd2c9779d2a7555) - */ - LONG new_x = raw_mouse_data->data.mouse.lLastX; - LONG new_y = raw_mouse_data->data.mouse.lLastY; - if (_sapp.win32.mouse.raw_input.pos_valid) { - _sapp.mouse.dx = (float) (new_x - _sapp.win32.mouse.raw_input.pos_x); - _sapp.mouse.dy = (float) (new_y - _sapp.win32.mouse.raw_input.pos_y); - } - _sapp.win32.mouse.raw_input.pos_x = new_x; - _sapp.win32.mouse.raw_input.pos_y = new_y; - _sapp.win32.mouse.raw_input.pos_valid = true; - } else { - /* mouse reports movement delta (this seems to be the common case) */ - _sapp.mouse.dx = (float) raw_mouse_data->data.mouse.lLastX; - _sapp.mouse.dy = (float) raw_mouse_data->data.mouse.lLastY; - } - _sapp_win32_mouse_event(SAPP_EVENTTYPE_MOUSE_MOVE, SAPP_MOUSEBUTTON_INVALID); - } - break; - - case WM_MOUSELEAVE: - if (!_sapp.mouse.locked) { - _sapp.mouse.dx = 0.0f; - _sapp.mouse.dy = 0.0f; - _sapp.win32.mouse.tracked = false; - _sapp_win32_mouse_event(SAPP_EVENTTYPE_MOUSE_LEAVE, SAPP_MOUSEBUTTON_INVALID); - } - break; - case WM_MOUSEWHEEL: - _sapp_win32_scroll_event(0.0f, (float)GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA); - break; - case WM_MOUSEHWHEEL: - _sapp_win32_scroll_event(-(float)GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA, 0.0f); - break; - case WM_CHAR: - _sapp_win32_char_event((uint32_t)wParam, !!(lParam&0x40000000)); - break; - case WM_KEYDOWN: - case WM_SYSKEYDOWN: - _sapp_win32_key_event(SAPP_EVENTTYPE_KEY_DOWN, (int)(HIWORD(lParam)&0x1FF), !!(lParam&0x40000000)); - break; - case WM_KEYUP: - case WM_SYSKEYUP: - _sapp_win32_key_event(SAPP_EVENTTYPE_KEY_UP, (int)(HIWORD(lParam)&0x1FF), false); - break; - case WM_ENTERSIZEMOVE: - SetTimer(_sapp.win32.hwnd, 1, USER_TIMER_MINIMUM, NULL); - break; - case WM_EXITSIZEMOVE: - KillTimer(_sapp.win32.hwnd, 1); - break; - case WM_TIMER: - _sapp_timing_update(&_sapp.timing, 0.0); - _sapp_win32_frame(true); - /* - * NOTE: resizing each frame explodes memory usage - * - if (_sapp_win32_update_dimensions()) { - #if defined(SOKOL_D3D11) - _sapp_d3d11_resize_default_render_target(); - #elif defined(SOKOL_WGPU) - _sapp_wgpu_swapchain_size_changed(); - #endif - _sapp_win32_app_event(SAPP_EVENTTYPE_RESIZED); - } - */ - break; - case WM_NCLBUTTONDOWN: - /* workaround for half-second pause when starting to move window - see: https://gamedev.net/forums/topic/672094-keeping-things-moving-during-win32-moveresize-events/5254386/ - */ - if (SendMessage(_sapp.win32.hwnd, WM_NCHITTEST, wParam, lParam) == HTCAPTION) { - POINT point = { 0, 0 }; - if (GetCursorPos(&point)) { - ScreenToClient(_sapp.win32.hwnd, &point); - PostMessage(_sapp.win32.hwnd, WM_MOUSEMOVE, 0, ((uint32_t)point.x)|(((uint32_t)point.y) << 16)); - } - } - break; - case WM_DROPFILES: - _sapp_win32_files_dropped((HDROP)wParam); - break; - - default: - break; - } - } - return DefWindowProcW(hWnd, uMsg, wParam, lParam); -} - -_SOKOL_PRIVATE void _sapp_win32_create_window(void) { - _SAPP_STRUCT(WNDCLASSW, wndclassw); - wndclassw.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; - wndclassw.lpfnWndProc = (WNDPROC) _sapp_win32_wndproc; - wndclassw.hInstance = GetModuleHandleW(NULL); - wndclassw.hCursor = LoadCursor(NULL, IDC_ARROW); - wndclassw.hIcon = LoadIcon(NULL, IDI_WINLOGO); - wndclassw.lpszClassName = L"SOKOLAPP"; - RegisterClassW(&wndclassw); - - /* NOTE: regardless whether fullscreen is requested or not, a regular - windowed-mode window will always be created first (however in hidden - mode, so that no windowed-mode window pops up before the fullscreen window) - */ - const DWORD win_ex_style = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE; - RECT rect = { 0, 0, 0, 0 }; - DWORD win_style = WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SIZEBOX; - rect.right = (int) ((float)_sapp.window_width * _sapp.win32.dpi.window_scale); - rect.bottom = (int) ((float)_sapp.window_height * _sapp.win32.dpi.window_scale); - const bool use_default_width = 0 == _sapp.window_width; - const bool use_default_height = 0 == _sapp.window_height; - AdjustWindowRectEx(&rect, win_style, FALSE, win_ex_style); - const int win_width = rect.right - rect.left; - const int win_height = rect.bottom - rect.top; - _sapp.win32.in_create_window = true; - _sapp.win32.surrogate = 0; - _sapp.win32.hwnd = CreateWindowExW( - win_ex_style, // dwExStyle - L"SOKOLAPP", // lpClassName - _sapp.window_title_wide, // lpWindowName - win_style, // dwStyle - CW_USEDEFAULT, // X - SW_HIDE, // Y (NOTE: CW_USEDEFAULT is not used for position here, but internally calls ShowWindow! - use_default_width ? CW_USEDEFAULT : win_width, // nWidth - use_default_height ? CW_USEDEFAULT : win_height, // nHeight (NOTE: if width is CW_USEDEFAULT, height is actually ignored) - NULL, // hWndParent - NULL, // hMenu - GetModuleHandle(NULL), // hInstance - NULL); // lParam - _sapp.win32.in_create_window = false; - _sapp.win32.dc = GetDC(_sapp.win32.hwnd); - _sapp.win32.hmonitor = MonitorFromWindow(_sapp.win32.hwnd, MONITOR_DEFAULTTONULL); - SOKOL_ASSERT(_sapp.win32.dc); - - /* this will get the actual windowed-mode window size, if fullscreen - is requested, the set_fullscreen function will then capture the - current window rectangle, which then might be used later to - restore the window position when switching back to windowed - */ - _sapp_win32_update_dimensions(); - if (_sapp.fullscreen) { - _sapp_win32_set_fullscreen(_sapp.fullscreen, SWP_HIDEWINDOW); - _sapp_win32_update_dimensions(); - } - ShowWindow(_sapp.win32.hwnd, SW_SHOW); - DragAcceptFiles(_sapp.win32.hwnd, 1); -} - -_SOKOL_PRIVATE void _sapp_win32_destroy_window(void) { - DestroyWindow(_sapp.win32.hwnd); _sapp.win32.hwnd = 0; - UnregisterClassW(L"SOKOLAPP", GetModuleHandleW(NULL)); -} - -_SOKOL_PRIVATE void _sapp_win32_destroy_icons(void) { - if (_sapp.win32.big_icon) { - DestroyIcon(_sapp.win32.big_icon); - _sapp.win32.big_icon = 0; - } - if (_sapp.win32.small_icon) { - DestroyIcon(_sapp.win32.small_icon); - _sapp.win32.small_icon = 0; - } -} - -_SOKOL_PRIVATE void _sapp_win32_init_console(void) { - if (_sapp.desc.win32.console_create || _sapp.desc.win32.console_attach) { - BOOL con_valid = FALSE; - if (_sapp.desc.win32.console_attach) { - con_valid = AttachConsole(ATTACH_PARENT_PROCESS); - } - if (!con_valid && _sapp.desc.win32.console_create) { - con_valid = AllocConsole(); - } - if (con_valid) { - FILE* res_fp = 0; - errno_t err; - err = freopen_s(&res_fp, "CON", "w", stdout); - (void)err; - err = freopen_s(&res_fp, "CON", "w", stderr); - (void)err; - } - } - if (_sapp.desc.win32.console_utf8) { - _sapp.win32.orig_codepage = GetConsoleOutputCP(); - SetConsoleOutputCP(CP_UTF8); - } -} - -_SOKOL_PRIVATE void _sapp_win32_restore_console(void) { - if (_sapp.desc.win32.console_utf8) { - SetConsoleOutputCP(_sapp.win32.orig_codepage); - } -} - -_SOKOL_PRIVATE void _sapp_win32_init_dpi(void) { - - DECLARE_HANDLE(DPI_AWARENESS_CONTEXT_T); - typedef BOOL(WINAPI * SETPROCESSDPIAWARE_T)(void); - typedef bool (WINAPI * SETPROCESSDPIAWARENESSCONTEXT_T)(DPI_AWARENESS_CONTEXT_T); // since Windows 10, version 1703 - typedef HRESULT(WINAPI * SETPROCESSDPIAWARENESS_T)(PROCESS_DPI_AWARENESS); - typedef HRESULT(WINAPI * GETDPIFORMONITOR_T)(HMONITOR, MONITOR_DPI_TYPE, UINT*, UINT*); - - SETPROCESSDPIAWARE_T fn_setprocessdpiaware = 0; - SETPROCESSDPIAWARENESS_T fn_setprocessdpiawareness = 0; - GETDPIFORMONITOR_T fn_getdpiformonitor = 0; - SETPROCESSDPIAWARENESSCONTEXT_T fn_setprocessdpiawarenesscontext =0; - - HINSTANCE user32 = LoadLibraryA("user32.dll"); - if (user32) { - fn_setprocessdpiaware = (SETPROCESSDPIAWARE_T)(void*) GetProcAddress(user32, "SetProcessDPIAware"); - fn_setprocessdpiawarenesscontext = (SETPROCESSDPIAWARENESSCONTEXT_T)(void*) GetProcAddress(user32, "SetProcessDpiAwarenessContext"); - } - HINSTANCE shcore = LoadLibraryA("shcore.dll"); - if (shcore) { - fn_setprocessdpiawareness = (SETPROCESSDPIAWARENESS_T)(void*) GetProcAddress(shcore, "SetProcessDpiAwareness"); - fn_getdpiformonitor = (GETDPIFORMONITOR_T)(void*) GetProcAddress(shcore, "GetDpiForMonitor"); - } - /* - NOTE on SetProcessDpiAware() vs SetProcessDpiAwareness() vs SetProcessDpiAwarenessContext(): - - These are different attempts to get DPI handling on Windows right, from oldest - to newest. SetProcessDpiAwarenessContext() is required for the new - DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 method. - */ - bool init_dpi_awareness = true; - #if !defined(SOKOL_D3D11) - // special case for GL and Vulkan: if no high-dpi is requested, need to set the - // process to dpi-unaware, so that Windows takes care of upscaling - if (!_sapp.desc.high_dpi) { - _sapp.win32.dpi.aware = false; - fn_setprocessdpiawareness(PROCESS_DPI_UNAWARE); - init_dpi_awareness = false; - } - #endif - if (init_dpi_awareness) { - if (fn_setprocessdpiawareness) { - // first try the Win10 Creator Update per-monitor-dpi awareness, if that fails, fall back to system-dpi-awareness - // NOTE: if DPI awareness had already been set otherwise (e.g. via manifest.xml) both calls will fail - _sapp.win32.dpi.aware = true; - DPI_AWARENESS_CONTEXT_T per_monitor_aware_v2 = (DPI_AWARENESS_CONTEXT_T)-4; - if (!(fn_setprocessdpiawarenesscontext && fn_setprocessdpiawarenesscontext(per_monitor_aware_v2))) { - // fallback to system-dpi-aware - fn_setprocessdpiawareness(PROCESS_SYSTEM_DPI_AWARE); - } - } else if (fn_setprocessdpiaware) { - // fallback for Windows 7 - _sapp.win32.dpi.aware = true; - fn_setprocessdpiaware(); - } - } - // get dpi scale factor for main monitor - if (fn_getdpiformonitor && _sapp.win32.dpi.aware) { - POINT pt = { 1, 1 }; - HMONITOR hm = MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST); - UINT dpix, dpiy; - HRESULT hr = fn_getdpiformonitor(hm, MDT_EFFECTIVE_DPI, &dpix, &dpiy); - _SOKOL_UNUSED(hr); - SOKOL_ASSERT(SUCCEEDED(hr)); - // clamp window scale to an integer factor - _sapp.win32.dpi.window_scale = (float)dpix / 96.0f; - } else { - _sapp.win32.dpi.window_scale = 1.0f; - } - if (_sapp.desc.high_dpi) { - _sapp.win32.dpi.content_scale = _sapp.win32.dpi.window_scale; - _sapp.win32.dpi.mouse_scale = 1.0f; - } else { - _sapp.win32.dpi.content_scale = 1.0f; - _sapp.win32.dpi.mouse_scale = 1.0f / _sapp.win32.dpi.window_scale; - } - _sapp.dpi_scale = _sapp.win32.dpi.content_scale; - if (user32) { - FreeLibrary(user32); - } - if (shcore) { - FreeLibrary(shcore); - } -} - -_SOKOL_PRIVATE bool _sapp_win32_set_clipboard_string(const char* str) { - SOKOL_ASSERT(str); - SOKOL_ASSERT(_sapp.win32.hwnd); - SOKOL_ASSERT(_sapp.clipboard.enabled && (_sapp.clipboard.buf_size > 0)); - - if (!OpenClipboard(_sapp.win32.hwnd)) { - return false; - } - - HANDLE object = 0; - wchar_t* wchar_buf = 0; - - const SIZE_T wchar_buf_size = (SIZE_T)_sapp.clipboard.buf_size * sizeof(wchar_t); - object = GlobalAlloc(GMEM_MOVEABLE, wchar_buf_size); - if (NULL == object) { - goto error; - } - wchar_buf = (wchar_t*) GlobalLock(object); - if (NULL == wchar_buf) { - goto error; - } - if (!_sapp_win32_utf8_to_wide(str, wchar_buf, (int)wchar_buf_size)) { - goto error; - } - GlobalUnlock(object); - wchar_buf = 0; - EmptyClipboard(); - // NOTE: when successful, SetClipboardData() takes ownership of memory object! - if (NULL == SetClipboardData(CF_UNICODETEXT, object)) { - goto error; - } - CloseClipboard(); - return true; - -error: - if (wchar_buf) { - GlobalUnlock(object); - } - if (object) { - GlobalFree(object); - } - CloseClipboard(); - return false; -} - -_SOKOL_PRIVATE const char* _sapp_win32_get_clipboard_string(void) { - SOKOL_ASSERT(_sapp.clipboard.enabled && _sapp.clipboard.buffer); - SOKOL_ASSERT(_sapp.win32.hwnd); - if (!OpenClipboard(_sapp.win32.hwnd)) { - /* silently ignore any errors and just return the current - content of the local clipboard buffer - */ - return _sapp.clipboard.buffer; - } - HANDLE object = GetClipboardData(CF_UNICODETEXT); - if (!object) { - CloseClipboard(); - return _sapp.clipboard.buffer; - } - const wchar_t* wchar_buf = (const wchar_t*) GlobalLock(object); - if (!wchar_buf) { - CloseClipboard(); - return _sapp.clipboard.buffer; - } - if (!_sapp_win32_wide_to_utf8(wchar_buf, _sapp.clipboard.buffer, _sapp.clipboard.buf_size)) { - _SAPP_ERROR(CLIPBOARD_STRING_TOO_BIG); - } - GlobalUnlock(object); - CloseClipboard(); - return _sapp.clipboard.buffer; -} - -_SOKOL_PRIVATE void _sapp_win32_update_window_title(void) { - _sapp_win32_utf8_to_wide(_sapp.window_title, _sapp.window_title_wide, sizeof(_sapp.window_title_wide)); - SetWindowTextW(_sapp.win32.hwnd, _sapp.window_title_wide); -} - -_SOKOL_PRIVATE HICON _sapp_win32_create_icon_from_image(const sapp_image_desc* desc, bool is_cursor) { - _SAPP_STRUCT(BITMAPV5HEADER, bi); - bi.bV5Size = sizeof(bi); - bi.bV5Width = desc->width; - bi.bV5Height = -desc->height; // NOTE the '-' here to indicate that origin is top-left - bi.bV5Planes = 1; - bi.bV5BitCount = 32; - bi.bV5Compression = BI_BITFIELDS; - bi.bV5RedMask = 0x00FF0000; - bi.bV5GreenMask = 0x0000FF00; - bi.bV5BlueMask = 0x000000FF; - bi.bV5AlphaMask = 0xFF000000; - - uint8_t* target = 0; - const uint8_t* source = (const uint8_t*)desc->pixels.ptr; - - HDC dc = GetDC(NULL); - HBITMAP color = CreateDIBSection(dc, (BITMAPINFO*)&bi, DIB_RGB_COLORS, (void**)&target, NULL, (DWORD)0); - ReleaseDC(NULL, dc); - if (0 == color) { - return NULL; - } - SOKOL_ASSERT(target); - - HBITMAP mask = CreateBitmap(desc->width, desc->height, 1, 1, NULL); - if (0 == mask) { - DeleteObject(color); - return NULL; - } - - for (int i = 0; i < (desc->width*desc->height); i++) { - target[0] = source[2]; - target[1] = source[1]; - target[2] = source[0]; - target[3] = source[3]; - target += 4; - source += 4; - } - - _SAPP_STRUCT(ICONINFO, icon_info); - icon_info.fIcon = !is_cursor; - icon_info.xHotspot = (DWORD) (is_cursor ? desc->cursor_hotspot_x : 0); - icon_info.yHotspot = (DWORD) (is_cursor ? desc->cursor_hotspot_y : 0); - icon_info.hbmMask = mask; - icon_info.hbmColor = color; - HICON icon_handle = CreateIconIndirect(&icon_info); - DeleteObject(color); - DeleteObject(mask); - - return icon_handle; -} - -_SOKOL_PRIVATE void _sapp_win32_set_icon(const sapp_icon_desc* icon_desc, int num_images) { - SOKOL_ASSERT((num_images > 0) && (num_images <= SAPP_MAX_ICONIMAGES)); - - int big_img_index = _sapp_image_bestmatch(icon_desc->images, num_images, GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON)); - int sml_img_index = _sapp_image_bestmatch(icon_desc->images, num_images, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON)); - HICON big_icon = _sapp_win32_create_icon_from_image(&icon_desc->images[big_img_index], false); - HICON sml_icon = _sapp_win32_create_icon_from_image(&icon_desc->images[sml_img_index], false); - - // if icon creation or lookup has failed for some reason, leave the currently set icon untouched - if (0 != big_icon) { - SendMessage(_sapp.win32.hwnd, WM_SETICON, ICON_BIG, (LPARAM) big_icon); - if (0 != _sapp.win32.big_icon) { - DestroyIcon(_sapp.win32.big_icon); - } - _sapp.win32.big_icon = big_icon; - } - if (0 != sml_icon) { - SendMessage(_sapp.win32.hwnd, WM_SETICON, ICON_SMALL, (LPARAM) sml_icon); - if (0 != _sapp.win32.small_icon) { - DestroyIcon(_sapp.win32.small_icon); - } - _sapp.win32.small_icon = sml_icon; - } -} - -/* don't laugh, but this seems to be the easiest and most robust - way to check if we're running on Win10 - - From: https://github.com/videolan/vlc/blob/232fb13b0d6110c4d1b683cde24cf9a7f2c5c2ea/modules/video_output/win32/d3d11_swapchain.c#L263 -*/ -_SOKOL_PRIVATE bool _sapp_win32_is_win10_or_greater(void) { - HMODULE h = GetModuleHandleW(L"kernel32.dll"); - if (NULL != h) { - return (NULL != GetProcAddress(h, "GetSystemCpuSetInformation")); - } else { - return false; - } -} - -_SOKOL_PRIVATE void _sapp_win32_run(const sapp_desc* desc) { - _sapp_init_state(desc); - _sapp_win32_init_console(); - _sapp.win32.is_win10_or_greater = _sapp_win32_is_win10_or_greater(); - _sapp_win32_init_keytable(); - _sapp_win32_utf8_to_wide(_sapp.window_title, _sapp.window_title_wide, sizeof(_sapp.window_title_wide)); - _sapp_win32_init_dpi(); - _sapp_win32_init_cursors(); - _sapp_win32_create_window(); - sapp_set_icon(&desc->icon); - #if defined(SOKOL_D3D11) - _sapp_d3d11_create_device_and_swapchain(); - _sapp_d3d11_create_default_render_target(); - #elif defined(SOKOL_GLCORE) - _sapp_wgl_init(); - _sapp_wgl_load_extensions(); - _sapp_wgl_create_context(); - #elif defined(SOKOL_WGPU) - _sapp_wgpu_init(); - #elif defined(SOKOL_VULKAN) - _sapp_vk_init(); - #endif - _sapp.valid = true; - - bool done = false; - while (!(done || _sapp.quit_ordered)) { - _sapp_timing_update(&_sapp.timing, 0.0); - MSG msg; - while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE)) { - if (WM_QUIT == msg.message) { - done = true; - continue; - } else { - TranslateMessage(&msg); - DispatchMessageW(&msg); - } - } - _sapp_win32_frame(false); - // check for window resized, this cannot happen in WM_SIZE as it explodes memory usage - // NOTE: when Vulkan is active, _sapp_win32_update_dimensions() will never return true, - // instead the resize-event is fixed by the swapchain management code - if (_sapp_win32_update_dimensions()) { - #if defined(SOKOL_D3D11) - _sapp_d3d11_resize_default_render_target(); - #elif defined(SOKOL_WGPU) - _sapp_wgpu_swapchain_size_changed(); - #endif - _sapp_win32_app_event(SAPP_EVENTTYPE_RESIZED); - } - if (_sapp.quit_requested) { - PostMessage(_sapp.win32.hwnd, WM_CLOSE, 0, 0); - } - // update mouse-lock state - _sapp_win32_update_mouse_lock(); - } - _sapp_call_cleanup(); - - #if defined(SOKOL_D3D11) - _sapp_d3d11_destroy_default_render_target(); - _sapp_d3d11_destroy_device_and_swapchain(); - #elif defined(SOKOL_GLCORE) - _sapp_wgl_destroy_context(); - _sapp_wgl_shutdown(); - #elif defined(SOKOL_WGPU) - _sapp_wgpu_discard(); - #elif defined(SOKOL_VULKAN) - _sapp_vk_discard(); - #endif - _sapp_win32_destroy_window(); - _sapp_win32_destroy_icons(); - _sapp_win32_restore_console(); - _sapp_win32_free_raw_input_data(); - _sapp_discard_state(); -} - -_SOKOL_PRIVATE char** _sapp_win32_command_line_to_utf8_argv(LPWSTR w_command_line, int* o_argc) { - int argc = 0; - char** argv = 0; - char* args; - - LPWSTR* w_argv = CommandLineToArgvW(w_command_line, &argc); - if (w_argv == NULL) { - // FIXME: chicken egg problem, can't report errors before sokol_main() is called! - } else { - size_t size = wcslen(w_command_line) * 4; - argv = (char**) _sapp_malloc_clear(((size_t)argc + 1) * sizeof(char*) + size); - SOKOL_ASSERT(argv); - args = (char*) &argv[argc + 1]; - int n; - for (int i = 0; i < argc; ++i) { - n = WideCharToMultiByte(CP_UTF8, 0, w_argv[i], -1, args, (int)size, NULL, NULL); - if (n == 0) { - // FIXME: chicken egg problem, can't report errors before sokol_main() is called! - break; - } - argv[i] = args; - size -= (size_t)n; - args += n; - } - LocalFree(w_argv); - } - *o_argc = argc; - return argv; -} - -_SOKOL_PRIVATE bool _sapp_win32_make_custom_mouse_cursor(sapp_mouse_cursor cursor, const sapp_image_desc* desc) { - SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); - SOKOL_ASSERT(0 == _sapp.win32.custom_cursors[cursor]); - const HCURSOR win32_cursor = _sapp_win32_create_icon_from_image(desc, true); - _sapp.win32.custom_cursors[cursor] = win32_cursor; - return win32_cursor != 0; -} - -_SOKOL_PRIVATE void _sapp_win32_destroy_custom_mouse_cursor(sapp_mouse_cursor cursor) { - SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); - HCURSOR win32_cursor = _sapp.win32.custom_cursors[cursor]; - SOKOL_ASSERT(win32_cursor); - _sapp.win32.custom_cursors[cursor] = 0; - // NOTE: DestroyIcon() may return zero (failure) if the cursor is currently in - // use. Normally that shouldn't happen since when attempting to unbind the - // current cursor it will be hidden first, but since there might be other edge - // cases we just log a warning but don't fail hard - BOOL res = DestroyIcon(win32_cursor); - if (!res) { - _SAPP_WARN(WIN32_DESTROYICON_FOR_CURSOR_FAILED); - } -} - -#if !defined(SOKOL_NO_ENTRY) -#if defined(SOKOL_WIN32_FORCE_MAIN) -int main(int argc, char* argv[]) { - sapp_desc desc = sokol_main(argc, argv); - _sapp_win32_run(&desc); - return 0; -} -#endif /* SOKOL_WIN32_FORCE_MAIN */ -#if defined(SOKOL_WIN32_FORCE_WINMAIN) || !defined(SOKOL_WIN32_FORCE_MAIN) -int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nCmdShow) { - _SOKOL_UNUSED(hInstance); - _SOKOL_UNUSED(hPrevInstance); - _SOKOL_UNUSED(lpCmdLine); - _SOKOL_UNUSED(nCmdShow); - int argc_utf8 = 0; - char** argv_utf8 = _sapp_win32_command_line_to_utf8_argv(GetCommandLineW(), &argc_utf8); - sapp_desc desc = sokol_main(argc_utf8, argv_utf8); - _sapp_win32_run(&desc); - _sapp_free(argv_utf8); - return 0; -} -#endif /* SOKOL_WIN32_FORCE_WINMAIN */ -#endif /* SOKOL_NO_ENTRY */ - -#ifdef _MSC_VER - #pragma warning(pop) -#endif - -#endif /* _SAPP_WIN32 */ - -// █████ ███ ██ ██████ ██████ ██████ ██ ██████ -// ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ███████ ██ ██ ██ ██ ██ ██████ ██ ██ ██ ██ ██ -// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ██ ██ ██ ████ ██████ ██ ██ ██████ ██ ██████ -// -// >>android -#if defined(_SAPP_ANDROID) - -/* android loop thread */ -_SOKOL_PRIVATE bool _sapp_android_init_egl(void) { - SOKOL_ASSERT(_sapp.android.display == EGL_NO_DISPLAY); - SOKOL_ASSERT(_sapp.android.context == EGL_NO_CONTEXT); - - EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); - if (display == EGL_NO_DISPLAY) { - return false; - } - if (eglInitialize(display, NULL, NULL) == EGL_FALSE) { - return false; - } - EGLint alpha_size = _sapp.desc.alpha ? 8 : 0; - const EGLint cfg_attributes[] = { - EGL_SURFACE_TYPE, EGL_WINDOW_BIT, - EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, - EGL_RED_SIZE, 8, - EGL_GREEN_SIZE, 8, - EGL_BLUE_SIZE, 8, - EGL_ALPHA_SIZE, alpha_size, - EGL_DEPTH_SIZE, 16, - EGL_STENCIL_SIZE, 0, - EGL_NONE, - }; - EGLConfig available_cfgs[32]; - EGLint cfg_count; - eglChooseConfig(display, cfg_attributes, available_cfgs, 32, &cfg_count); - SOKOL_ASSERT(cfg_count > 0); - SOKOL_ASSERT(cfg_count <= 32); - - /* find config with 8-bit rgb buffer if available, ndk sample does not trust egl spec */ - EGLConfig config; - bool exact_cfg_found = false; - for (int i = 0; i < cfg_count; ++i) { - EGLConfig c = available_cfgs[i]; - EGLint r, g, b, a, d; - if (eglGetConfigAttrib(display, c, EGL_RED_SIZE, &r) == EGL_TRUE && - eglGetConfigAttrib(display, c, EGL_GREEN_SIZE, &g) == EGL_TRUE && - eglGetConfigAttrib(display, c, EGL_BLUE_SIZE, &b) == EGL_TRUE && - eglGetConfigAttrib(display, c, EGL_ALPHA_SIZE, &a) == EGL_TRUE && - eglGetConfigAttrib(display, c, EGL_DEPTH_SIZE, &d) == EGL_TRUE && - r == 8 && g == 8 && b == 8 && (alpha_size == 0 || a == alpha_size) && d == 16) { - exact_cfg_found = true; - config = c; - break; - } - } - if (!exact_cfg_found) { - config = available_cfgs[0]; - } - - EGLint ctx_attributes[] = { - EGL_CONTEXT_MAJOR_VERSION, _sapp.desc.gl.major_version, - EGL_CONTEXT_MINOR_VERSION, _sapp.desc.gl.minor_version, - EGL_NONE, - }; - EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, ctx_attributes); - if (context == EGL_NO_CONTEXT) { - return false; - } - - _sapp.android.config = config; - _sapp.android.display = display; - _sapp.android.context = context; - return true; -} - -_SOKOL_PRIVATE void _sapp_android_cleanup_egl(void) { - if (_sapp.android.display != EGL_NO_DISPLAY) { - eglMakeCurrent(_sapp.android.display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); - if (_sapp.android.surface != EGL_NO_SURFACE) { - eglDestroySurface(_sapp.android.display, _sapp.android.surface); - _sapp.android.surface = EGL_NO_SURFACE; - } - if (_sapp.android.context != EGL_NO_CONTEXT) { - eglDestroyContext(_sapp.android.display, _sapp.android.context); - _sapp.android.context = EGL_NO_CONTEXT; - } - eglTerminate(_sapp.android.display); - _sapp.android.display = EGL_NO_DISPLAY; - } -} - -_SOKOL_PRIVATE bool _sapp_android_init_egl_surface(ANativeWindow* window) { - SOKOL_ASSERT(_sapp.android.display != EGL_NO_DISPLAY); - SOKOL_ASSERT(_sapp.android.context != EGL_NO_CONTEXT); - SOKOL_ASSERT(_sapp.android.surface == EGL_NO_SURFACE); - SOKOL_ASSERT(window); - - /* TODO: set window flags */ - /* ANativeActivity_setWindowFlags(activity, AWINDOW_FLAG_KEEP_SCREEN_ON, 0); */ - - /* create egl surface and make it current */ - EGLSurface surface = eglCreateWindowSurface(_sapp.android.display, _sapp.android.config, window, NULL); - if (surface == EGL_NO_SURFACE) { - return false; - } - if (eglMakeCurrent(_sapp.android.display, surface, surface, _sapp.android.context) == EGL_FALSE) { - return false; - } - _sapp.android.surface = surface; - glGetIntegerv(GL_FRAMEBUFFER_BINDING, (GLint*)&_sapp.gl.framebuffer); - return true; -} - -_SOKOL_PRIVATE void _sapp_android_cleanup_egl_surface(void) { - if (_sapp.android.display == EGL_NO_DISPLAY) { - return; - } - eglMakeCurrent(_sapp.android.display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); - if (_sapp.android.surface != EGL_NO_SURFACE) { - eglDestroySurface(_sapp.android.display, _sapp.android.surface); - _sapp.android.surface = EGL_NO_SURFACE; - } -} - -_SOKOL_PRIVATE void _sapp_android_app_event(sapp_event_type type) { - if (_sapp_events_enabled()) { - _sapp_init_event(type); - _sapp_call_event(&_sapp.event); - } -} - -_SOKOL_PRIVATE void _sapp_android_update_dimensions(ANativeWindow* window, bool force_update) { - SOKOL_ASSERT(_sapp.android.display != EGL_NO_DISPLAY); - SOKOL_ASSERT(_sapp.android.context != EGL_NO_CONTEXT); - SOKOL_ASSERT(_sapp.android.surface != EGL_NO_SURFACE); - SOKOL_ASSERT(window); - - const int32_t win_w = ANativeWindow_getWidth(window); - const int32_t win_h = ANativeWindow_getHeight(window); - SOKOL_ASSERT(win_w >= 0 && win_h >= 0); - const bool win_changed = (win_w != _sapp.window_width) || (win_h != _sapp.window_height); - _sapp.window_width = win_w; - _sapp.window_height = win_h; - if (win_changed || force_update) { - if (!_sapp.desc.high_dpi) { - const int32_t buf_w = win_w / 2; - const int32_t buf_h = win_h / 2; - EGLint format; - EGLBoolean egl_result = eglGetConfigAttrib(_sapp.android.display, _sapp.android.config, EGL_NATIVE_VISUAL_ID, &format); - SOKOL_ASSERT(egl_result == EGL_TRUE); _SOKOL_UNUSED(egl_result); - /* NOTE: calling ANativeWindow_setBuffersGeometry() with the same dimensions - as the ANativeWindow size results in weird display artefacts, that's - why it's only called when the buffer geometry is different from - the window size - */ - int32_t result = ANativeWindow_setBuffersGeometry(window, buf_w, buf_h, format); - SOKOL_ASSERT(result == 0); _SOKOL_UNUSED(result); - } - } - - /* query surface size */ - EGLint fb_w, fb_h; - EGLBoolean egl_result_w = eglQuerySurface(_sapp.android.display, _sapp.android.surface, EGL_WIDTH, &fb_w); - EGLBoolean egl_result_h = eglQuerySurface(_sapp.android.display, _sapp.android.surface, EGL_HEIGHT, &fb_h); - SOKOL_ASSERT(egl_result_w == EGL_TRUE); _SOKOL_UNUSED(egl_result_w); - SOKOL_ASSERT(egl_result_h == EGL_TRUE); _SOKOL_UNUSED(egl_result_h); - const bool fb_changed = (fb_w != _sapp.framebuffer_width) || (fb_h != _sapp.framebuffer_height); - _sapp.framebuffer_width = fb_w; - _sapp.framebuffer_height = fb_h; - /* [TrussC] Use actual display density for dpi_scale instead of fb/win ratio. - Android baseline is 160dpi (mdpi), so dpi_scale = density / 160. - This makes sapp_dpi_scale() behave consistently with macOS/Windows. */ - if (_sapp.android.activity) { - AConfiguration* config = AConfiguration_new(); - AConfiguration_fromAssetManager(config, _sapp.android.activity->assetManager); - int32_t density = AConfiguration_getDensity(config); - AConfiguration_delete(config); - if (density > 0) { - _sapp.dpi_scale = (float)density / 160.0f; - } else { - _sapp.dpi_scale = (float)fb_w / (float)win_w; - } - } else { - _sapp.dpi_scale = (float)fb_w / (float)win_w; - } - if (win_changed || fb_changed || force_update) { - if (!_sapp.first_frame) { - _sapp_android_app_event(SAPP_EVENTTYPE_RESIZED); - } - } -} - -_SOKOL_PRIVATE void _sapp_android_cleanup(void) { - if (_sapp.android.surface != EGL_NO_SURFACE) { - /* egl context is bound, cleanup gracefully */ - if (_sapp.init_called && !_sapp.cleanup_called) { - _sapp_call_cleanup(); - } - } - /* always try to cleanup by destroying egl context */ - _sapp_android_cleanup_egl(); -} - -_SOKOL_PRIVATE void _sapp_android_shutdown(void) { - /* try to cleanup while we still have a surface and can call cleanup_cb() */ - _sapp_android_cleanup(); - /* request exit */ - ANativeActivity_finish(_sapp.android.activity); -} - -_SOKOL_PRIVATE void _sapp_android_frame(double external_now) { - SOKOL_ASSERT(_sapp.android.display != EGL_NO_DISPLAY); - SOKOL_ASSERT(_sapp.android.context != EGL_NO_CONTEXT); - SOKOL_ASSERT(_sapp.android.surface != EGL_NO_SURFACE); - _sapp_timing_update(&_sapp.timing, external_now); - _sapp_android_update_dimensions(_sapp.android.current.window, false); - _sapp_frame(); - // Modified by tettou771 for TrussC: skip present support - if (_sapp.skip_present) { _sapp.skip_present = false; return; } - eglSwapBuffers(_sapp.android.display, _sapp.android.surface); -} - -_SOKOL_PRIVATE bool _sapp_android_touch_event(const AInputEvent* e) { - if (AInputEvent_getType(e) != AINPUT_EVENT_TYPE_MOTION) { - return false; - } - if (!_sapp_events_enabled()) { - return false; - } - int32_t action_idx = AMotionEvent_getAction(e); - int32_t action = action_idx & AMOTION_EVENT_ACTION_MASK; - sapp_event_type type = SAPP_EVENTTYPE_INVALID; - switch (action) { - case AMOTION_EVENT_ACTION_DOWN: - case AMOTION_EVENT_ACTION_POINTER_DOWN: - type = SAPP_EVENTTYPE_TOUCHES_BEGAN; - break; - case AMOTION_EVENT_ACTION_MOVE: - type = SAPP_EVENTTYPE_TOUCHES_MOVED; - break; - case AMOTION_EVENT_ACTION_UP: - case AMOTION_EVENT_ACTION_POINTER_UP: - type = SAPP_EVENTTYPE_TOUCHES_ENDED; - break; - case AMOTION_EVENT_ACTION_CANCEL: - type = SAPP_EVENTTYPE_TOUCHES_CANCELLED; - break; - default: - break; - } - if (type == SAPP_EVENTTYPE_INVALID) { - return false; - } - int32_t idx = action_idx >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT; - _sapp_init_event(type); - _sapp.event.num_touches = (int)AMotionEvent_getPointerCount(e); - if (_sapp.event.num_touches > SAPP_MAX_TOUCHPOINTS) { - _sapp.event.num_touches = SAPP_MAX_TOUCHPOINTS; - } - for (int32_t i = 0; i < _sapp.event.num_touches; i++) { - sapp_touchpoint* dst = &_sapp.event.touches[i]; - dst->identifier = (uintptr_t)AMotionEvent_getPointerId(e, (size_t)i); - dst->pos_x = (AMotionEvent_getX(e, (size_t)i) / _sapp.window_width) * _sapp.framebuffer_width; - dst->pos_y = (AMotionEvent_getY(e, (size_t)i) / _sapp.window_height) * _sapp.framebuffer_height; - dst->android_tooltype = (sapp_android_tooltype) AMotionEvent_getToolType(e, (size_t)i); - if (action == AMOTION_EVENT_ACTION_POINTER_DOWN || - action == AMOTION_EVENT_ACTION_POINTER_UP) { - dst->changed = (i == idx); - } else { - dst->changed = true; - } - } - _sapp_call_event(&_sapp.event); - return true; -} - -_SOKOL_PRIVATE bool _sapp_android_key_event(const AInputEvent* e) { - if (AInputEvent_getType(e) != AINPUT_EVENT_TYPE_KEY) { - return false; - } - if (AKeyEvent_getKeyCode(e) == AKEYCODE_BACK) { - /* [TrussC tettou771] Patched: BACK key no longer triggers shutdown. - Upstream calls _sapp_android_shutdown() here, which destroys the - EGL surface and invokes cleanup_cb. Under screen pinning, the OS - blocks ANativeActivity_finish(), so the process keeps running but - the EGL context is gone — the app appears frozen. Consume the - event without shutdown. Long-term fix: implement - SAPP_EVENTTYPE_QUIT_REQUESTED for Android (already supported on - macOS/Windows/Linux). */ - return true; - } - return false; -} - -_SOKOL_PRIVATE int _sapp_android_input_cb(int fd, int events, void* data) { - _SOKOL_UNUSED(fd); - _SOKOL_UNUSED(data); - if ((events & ALOOPER_EVENT_INPUT) == 0) { - _SAPP_ERROR(ANDROID_UNSUPPORTED_INPUT_EVENT_INPUT_CB); - return 1; - } - SOKOL_ASSERT(_sapp.android.current.input); - AInputEvent* event = NULL; - while (AInputQueue_getEvent(_sapp.android.current.input, &event) >= 0) { - if (AInputQueue_preDispatchEvent(_sapp.android.current.input, event) != 0) { - continue; - } - int32_t handled = 0; - if (_sapp_android_touch_event(event) || _sapp_android_key_event(event)) { - handled = 1; - } - AInputQueue_finishEvent(_sapp.android.current.input, event, handled); - } - return 1; -} - -_SOKOL_PRIVATE int _sapp_android_main_cb(int fd, int events, void* data) { - _SOKOL_UNUSED(data); - if ((events & ALOOPER_EVENT_INPUT) == 0) { - _SAPP_ERROR(ANDROID_UNSUPPORTED_INPUT_EVENT_MAIN_CB); - return 1; - } - - _sapp_android_msg_t msg; - if (read(fd, &msg, sizeof(msg)) != sizeof(msg)) { - _SAPP_ERROR(ANDROID_READ_MSG_FAILED); - return 1; - } - - pthread_mutex_lock(&_sapp.android.pt.mutex); - switch (msg) { - case _SOKOL_ANDROID_MSG_CREATE: - { - _SAPP_INFO(ANDROID_MSG_CREATE); - SOKOL_ASSERT(!_sapp.valid); - bool result = _sapp_android_init_egl(); - SOKOL_ASSERT(result); _SOKOL_UNUSED(result); - _sapp.valid = true; - _sapp.android.has_created = true; - } - break; - case _SOKOL_ANDROID_MSG_RESUME: - _SAPP_INFO(ANDROID_MSG_RESUME); - _sapp.android.has_resumed = true; - _sapp_android_app_event(SAPP_EVENTTYPE_RESUMED); - break; - case _SOKOL_ANDROID_MSG_PAUSE: - _SAPP_INFO(ANDROID_MSG_PAUSE); - _sapp.android.has_resumed = false; - _sapp_android_app_event(SAPP_EVENTTYPE_SUSPENDED); - break; - case _SOKOL_ANDROID_MSG_FOCUS: - _SAPP_INFO(ANDROID_MSG_FOCUS); - _sapp.android.has_focus = true; - break; - case _SOKOL_ANDROID_MSG_NO_FOCUS: - _SAPP_INFO(ANDROID_MSG_NO_FOCUS); - _sapp.android.has_focus = false; - break; - case _SOKOL_ANDROID_MSG_SET_NATIVE_WINDOW: - _SAPP_INFO(ANDROID_MSG_SET_NATIVE_WINDOW); - if (_sapp.android.current.window != _sapp.android.pending.window) { - if (_sapp.android.current.window != NULL) { - _sapp_android_cleanup_egl_surface(); - } - if (_sapp.android.pending.window != NULL) { - if (_sapp_android_init_egl_surface(_sapp.android.pending.window)) { - _sapp_android_update_dimensions(_sapp.android.pending.window, true); - } else { - _sapp_android_shutdown(); - } - } - } - _sapp.android.current.window = _sapp.android.pending.window; - break; - case _SOKOL_ANDROID_MSG_SET_INPUT_QUEUE: - _SAPP_INFO(ANDROID_MSG_SET_INPUT_QUEUE); - if (_sapp.android.current.input != _sapp.android.pending.input) { - if (_sapp.android.current.input != NULL) { - AInputQueue_detachLooper(_sapp.android.current.input); - } - if (_sapp.android.pending.input != NULL) { - AInputQueue_attachLooper( - _sapp.android.pending.input, - _sapp.android.looper, - ALOOPER_POLL_CALLBACK, - _sapp_android_input_cb, - NULL); /* data */ - } - } - _sapp.android.current.input = _sapp.android.pending.input; - break; - case _SOKOL_ANDROID_MSG_DESTROY: - _SAPP_INFO(ANDROID_MSG_DESTROY); - _sapp_android_cleanup(); - _sapp.valid = false; - _sapp.android.is_thread_stopping = true; - break; - default: - _SAPP_WARN(ANDROID_UNKNOWN_MSG); - break; - } - pthread_cond_broadcast(&_sapp.android.pt.cond); /* signal "received" */ - pthread_mutex_unlock(&_sapp.android.pt.mutex); - return 1; -} - -_SOKOL_PRIVATE bool _sapp_android_should_update(void) { - bool is_in_front = _sapp.android.has_resumed && _sapp.android.has_focus; - bool has_surface = _sapp.android.surface != EGL_NO_SURFACE; - return is_in_front && has_surface; -} - -#if __ANDROID_API__ >= 29 -_SOKOL_PRIVATE void _sapp_android_frame_callback(int64_t frame_time_nanos, void* data) { - _SOKOL_UNUSED(data); - _sapp.android.frame_callback_in_flight = false; - if (_sapp.android.is_thread_stopping) { - return; - } - if (_sapp_android_should_update()) { - // Post the next frame callback. We do this here rather than later so the runnable can be - // queued early in the looper. - AChoreographer_postFrameCallback64(_sapp.android.choreographer, _sapp_android_frame_callback, NULL); - _sapp.android.frame_callback_in_flight = true; - _sapp_android_frame((double)frame_time_nanos / 1.0e9); - } -} -#endif - -_SOKOL_PRIVATE void _sapp_android_show_keyboard(bool shown) { - SOKOL_ASSERT(_sapp.valid); - /* This seems to be broken in the NDK, but there is (a very cumbersome) workaround... */ - if (shown) { - ANativeActivity_showSoftInput(_sapp.android.activity, ANATIVEACTIVITY_SHOW_SOFT_INPUT_FORCED); - } else { - ANativeActivity_hideSoftInput(_sapp.android.activity, ANATIVEACTIVITY_HIDE_SOFT_INPUT_NOT_ALWAYS); - } -} - -_SOKOL_PRIVATE void* _sapp_android_loop(void* arg) { - _SOKOL_UNUSED(arg); - _SAPP_INFO(ANDROID_LOOP_THREAD_STARTED); - - _sapp.android.looper = ALooper_prepare(0 /* or ALOOPER_PREPARE_ALLOW_NON_CALLBACKS*/); - ALooper_addFd(_sapp.android.looper, - _sapp.android.pt.read_from_main_fd, - ALOOPER_POLL_CALLBACK, - ALOOPER_EVENT_INPUT, - _sapp_android_main_cb, - NULL); /* data */ - - #if __ANDROID_API__ >= 29 - _sapp.android.choreographer = AChoreographer_getInstance(); - if (_sapp.android.choreographer != NULL) { - _SAPP_INFO(ANDROID_CHOREOGRAPHER_ENABLED); - } else { - _SAPP_INFO(ANDROID_CHOREOGRAPHER_UNAVAILABLE); - } - #else - _SAPP_INFO(ANDROID_CHOREOGRAPHER_UNAVAILABLE); - #endif - - /* signal start to main thread */ - pthread_mutex_lock(&_sapp.android.pt.mutex); - _sapp.android.is_thread_started = true; - pthread_cond_broadcast(&_sapp.android.pt.cond); - pthread_mutex_unlock(&_sapp.android.pt.mutex); - - /* main loop */ - while (!_sapp.android.is_thread_stopping) { - #if __ANDROID_API__ >= 29 - if (_sapp.android.choreographer != NULL) { - // Posts _sapp_android_frame_callback with the choreographer to start our frame - // loop (for example, on first run or when resuming). When we have a choreographer, - // we'll get frame callbacks via _sapp_android_frame_callback. - if (!_sapp.android.frame_callback_in_flight && _sapp_android_should_update()) { - AChoreographer_postFrameCallback64(_sapp.android.choreographer, _sapp_android_frame_callback, NULL); - _sapp.android.frame_callback_in_flight = true; - } - // Blocks until the next event. We don't need a while loop here because we're - // already being driven by the outer while loop. - ALooper_pollOnce(-1, NULL, NULL, NULL); - continue; - } - #endif - // sokol frame -- fallback if not updating frames from choreographer callbacks - if (_sapp_android_should_update()) { - _sapp_android_frame(0.0); - } - - /* process all events (or stop early if app is requested to quit) */ - bool process_events = true; - while (process_events && !_sapp.android.is_thread_stopping) { - bool block_until_event = !_sapp.android.is_thread_stopping && !_sapp_android_should_update(); - process_events = ALooper_pollOnce(block_until_event ? -1 : 0, NULL, NULL, NULL) == ALOOPER_POLL_CALLBACK; - } - } - - /* cleanup thread */ - if (_sapp.android.current.input != NULL) { - AInputQueue_detachLooper(_sapp.android.current.input); - } - - /* the following causes heap corruption on exit, why?? - ALooper_removeFd(_sapp.android.looper, _sapp.android.pt.read_from_main_fd); - ALooper_release(_sapp.android.looper);*/ - - /* signal "destroyed" */ - pthread_mutex_lock(&_sapp.android.pt.mutex); - _sapp.android.is_thread_stopped = true; - pthread_cond_broadcast(&_sapp.android.pt.cond); - pthread_mutex_unlock(&_sapp.android.pt.mutex); - - _SAPP_INFO(ANDROID_LOOP_THREAD_DONE); - return NULL; -} - -/* android main/ui thread */ -_SOKOL_PRIVATE void _sapp_android_msg(_sapp_android_msg_t msg) { - if (write(_sapp.android.pt.write_from_main_fd, &msg, sizeof(msg)) != sizeof(msg)) { - _SAPP_ERROR(ANDROID_WRITE_MSG_FAILED); - } -} - -_SOKOL_PRIVATE void _sapp_android_on_start(ANativeActivity* activity) { - _SOKOL_UNUSED(activity); - _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONSTART); -} - -_SOKOL_PRIVATE void _sapp_android_on_resume(ANativeActivity* activity) { - _SOKOL_UNUSED(activity); - _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONRESUME); - _sapp_android_msg(_SOKOL_ANDROID_MSG_RESUME); -} - -_SOKOL_PRIVATE void* _sapp_android_on_save_instance_state(ANativeActivity* activity, size_t* out_size) { - _SOKOL_UNUSED(activity); - _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONSAVEINSTANCESTATE); - *out_size = 0; - return NULL; -} - -_SOKOL_PRIVATE void _sapp_android_on_window_focus_changed(ANativeActivity* activity, int has_focus) { - _SOKOL_UNUSED(activity); - _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONWINDOWFOCUSCHANGED); - if (has_focus) { - _sapp_android_msg(_SOKOL_ANDROID_MSG_FOCUS); - } else { - _sapp_android_msg(_SOKOL_ANDROID_MSG_NO_FOCUS); - } -} - -_SOKOL_PRIVATE void _sapp_android_on_pause(ANativeActivity* activity) { - _SOKOL_UNUSED(activity); - _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONPAUSE); - _sapp_android_msg(_SOKOL_ANDROID_MSG_PAUSE); -} - -_SOKOL_PRIVATE void _sapp_android_on_stop(ANativeActivity* activity) { - _SOKOL_UNUSED(activity); - _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONSTOP); -} - -_SOKOL_PRIVATE void _sapp_android_msg_set_native_window(ANativeWindow* window) { - pthread_mutex_lock(&_sapp.android.pt.mutex); - _sapp.android.pending.window = window; - _sapp_android_msg(_SOKOL_ANDROID_MSG_SET_NATIVE_WINDOW); - while (_sapp.android.current.window != window) { - pthread_cond_wait(&_sapp.android.pt.cond, &_sapp.android.pt.mutex); - } - pthread_mutex_unlock(&_sapp.android.pt.mutex); -} - -_SOKOL_PRIVATE void _sapp_android_on_native_window_created(ANativeActivity* activity, ANativeWindow* window) { - _SOKOL_UNUSED(activity); - _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONNATIVEWINDOWCREATED); - _sapp_android_msg_set_native_window(window); -} - -_SOKOL_PRIVATE void _sapp_android_on_native_window_destroyed(ANativeActivity* activity, ANativeWindow* window) { - _SOKOL_UNUSED(activity); - _SOKOL_UNUSED(window); - _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONNATIVEWINDOWDESTROYED); - _sapp_android_msg_set_native_window(NULL); -} - -_SOKOL_PRIVATE void _sapp_android_msg_set_input_queue(AInputQueue* input) { - pthread_mutex_lock(&_sapp.android.pt.mutex); - _sapp.android.pending.input = input; - _sapp_android_msg(_SOKOL_ANDROID_MSG_SET_INPUT_QUEUE); - while (_sapp.android.current.input != input) { - pthread_cond_wait(&_sapp.android.pt.cond, &_sapp.android.pt.mutex); - } - pthread_mutex_unlock(&_sapp.android.pt.mutex); -} - -_SOKOL_PRIVATE void _sapp_android_on_input_queue_created(ANativeActivity* activity, AInputQueue* queue) { - _SOKOL_UNUSED(activity); - _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONINPUTQUEUECREATED); - _sapp_android_msg_set_input_queue(queue); -} - -_SOKOL_PRIVATE void _sapp_android_on_input_queue_destroyed(ANativeActivity* activity, AInputQueue* queue) { - _SOKOL_UNUSED(activity); - _SOKOL_UNUSED(queue); - _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONINPUTQUEUEDESTROYED); - _sapp_android_msg_set_input_queue(NULL); -} - -_SOKOL_PRIVATE void _sapp_android_on_config_changed(ANativeActivity* activity) { - _SOKOL_UNUSED(activity); - _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONCONFIGURATIONCHANGED); - /* see android:configChanges in manifest */ -} - -_SOKOL_PRIVATE void _sapp_android_on_low_memory(ANativeActivity* activity) { - _SOKOL_UNUSED(activity); - _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONLOWMEMORY); -} - -_SOKOL_PRIVATE void _sapp_android_on_destroy(ANativeActivity* activity) { - /* - * For some reason even an empty app using nativeactivity.h will crash (WIN DEATH) - * on my device (Moto X 2nd gen) when the app is removed from the task view - * (TaskStackView: onTaskViewDismissed). - * - * However, if ANativeActivity_finish() is explicitly called from for example - * _sapp_android_on_stop(), the crash disappears. Is this a bug in NativeActivity? - */ - _SOKOL_UNUSED(activity); - _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONDESTROY); - - /* send destroy msg */ - pthread_mutex_lock(&_sapp.android.pt.mutex); - _sapp_android_msg(_SOKOL_ANDROID_MSG_DESTROY); - while (!_sapp.android.is_thread_stopped) { - pthread_cond_wait(&_sapp.android.pt.cond, &_sapp.android.pt.mutex); - } - pthread_mutex_unlock(&_sapp.android.pt.mutex); - - /* clean up main thread */ - pthread_cond_destroy(&_sapp.android.pt.cond); - pthread_mutex_destroy(&_sapp.android.pt.mutex); - - close(_sapp.android.pt.read_from_main_fd); - close(_sapp.android.pt.write_from_main_fd); - - _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_DONE); - - /* this is a bit naughty, but causes a clean restart of the app (static globals are reset) */ - exit(0); -} - -JNIEXPORT -void ANativeActivity_onCreate(ANativeActivity* activity, void* saved_state, size_t saved_state_size) { - _SOKOL_UNUSED(saved_state); - _SOKOL_UNUSED(saved_state_size); - _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONCREATE); - - // the NativeActity pointer needs to be available inside sokol_main() - // (see https://github.com/floooh/sokol/issues/708), however _sapp_init_state() - // will clear the global _sapp_t struct, so we need to initialize the native - // activity pointer twice, once before sokol_main() and once after _sapp_init_state() - _sapp_clear(&_sapp, sizeof(_sapp)); - _sapp.android.activity = activity; - sapp_desc desc = sokol_main(0, NULL); - _sapp_init_state(&desc); - _sapp.android.activity = activity; - - int pipe_fd[2]; - if (pipe(pipe_fd) != 0) { - _SAPP_ERROR(ANDROID_CREATE_THREAD_PIPE_FAILED); - return; - } - _sapp.android.pt.read_from_main_fd = pipe_fd[0]; - _sapp.android.pt.write_from_main_fd = pipe_fd[1]; - - pthread_mutex_init(&_sapp.android.pt.mutex, NULL); - pthread_cond_init(&_sapp.android.pt.cond, NULL); - - pthread_attr_t attr; - pthread_attr_init(&attr); - pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); - pthread_create(&_sapp.android.pt.thread, &attr, _sapp_android_loop, 0); - pthread_attr_destroy(&attr); - - /* wait until main loop has started */ - pthread_mutex_lock(&_sapp.android.pt.mutex); - while (!_sapp.android.is_thread_started) { - pthread_cond_wait(&_sapp.android.pt.cond, &_sapp.android.pt.mutex); - } - pthread_mutex_unlock(&_sapp.android.pt.mutex); - - /* send create msg */ - pthread_mutex_lock(&_sapp.android.pt.mutex); - _sapp_android_msg(_SOKOL_ANDROID_MSG_CREATE); - while (!_sapp.android.has_created) { - pthread_cond_wait(&_sapp.android.pt.cond, &_sapp.android.pt.mutex); - } - pthread_mutex_unlock(&_sapp.android.pt.mutex); - - /* register for callbacks */ - activity->callbacks->onStart = _sapp_android_on_start; - activity->callbacks->onResume = _sapp_android_on_resume; - activity->callbacks->onSaveInstanceState = _sapp_android_on_save_instance_state; - activity->callbacks->onWindowFocusChanged = _sapp_android_on_window_focus_changed; - activity->callbacks->onPause = _sapp_android_on_pause; - activity->callbacks->onStop = _sapp_android_on_stop; - activity->callbacks->onDestroy = _sapp_android_on_destroy; - activity->callbacks->onNativeWindowCreated = _sapp_android_on_native_window_created; - /* activity->callbacks->onNativeWindowResized = _sapp_android_on_native_window_resized; */ - /* activity->callbacks->onNativeWindowRedrawNeeded = _sapp_android_on_native_window_redraw_needed; */ - activity->callbacks->onNativeWindowDestroyed = _sapp_android_on_native_window_destroyed; - activity->callbacks->onInputQueueCreated = _sapp_android_on_input_queue_created; - activity->callbacks->onInputQueueDestroyed = _sapp_android_on_input_queue_destroyed; - /* activity->callbacks->onContentRectChanged = _sapp_android_on_content_rect_changed; */ - /* activity->callbacks->onConfigurationChanged = _sapp_android_on_config_changed; */ - activity->callbacks->onLowMemory = _sapp_android_on_low_memory; - - _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_CREATE_SUCCESS); - - /* NOT A BUG: do NOT call sapp_discard_state() */ -} - -#endif /* _SAPP_ANDROID */ - -// ██ ██ ███ ██ ██ ██ ██ ██ -// ██ ██ ████ ██ ██ ██ ██ ██ -// ██ ██ ██ ██ ██ ██ ██ ███ -// ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ███████ ██ ██ ████ ██████ ██ ██ -// -// >>linux -#if defined(_SAPP_LINUX) - -/* see GLFW's xkb_unicode.c */ -static const struct _sapp_x11_codepair { - uint16_t keysym; - uint16_t ucs; -} _sapp_x11_keysymtab[] = { - { 0x01a1, 0x0104 }, - { 0x01a2, 0x02d8 }, - { 0x01a3, 0x0141 }, - { 0x01a5, 0x013d }, - { 0x01a6, 0x015a }, - { 0x01a9, 0x0160 }, - { 0x01aa, 0x015e }, - { 0x01ab, 0x0164 }, - { 0x01ac, 0x0179 }, - { 0x01ae, 0x017d }, - { 0x01af, 0x017b }, - { 0x01b1, 0x0105 }, - { 0x01b2, 0x02db }, - { 0x01b3, 0x0142 }, - { 0x01b5, 0x013e }, - { 0x01b6, 0x015b }, - { 0x01b7, 0x02c7 }, - { 0x01b9, 0x0161 }, - { 0x01ba, 0x015f }, - { 0x01bb, 0x0165 }, - { 0x01bc, 0x017a }, - { 0x01bd, 0x02dd }, - { 0x01be, 0x017e }, - { 0x01bf, 0x017c }, - { 0x01c0, 0x0154 }, - { 0x01c3, 0x0102 }, - { 0x01c5, 0x0139 }, - { 0x01c6, 0x0106 }, - { 0x01c8, 0x010c }, - { 0x01ca, 0x0118 }, - { 0x01cc, 0x011a }, - { 0x01cf, 0x010e }, - { 0x01d0, 0x0110 }, - { 0x01d1, 0x0143 }, - { 0x01d2, 0x0147 }, - { 0x01d5, 0x0150 }, - { 0x01d8, 0x0158 }, - { 0x01d9, 0x016e }, - { 0x01db, 0x0170 }, - { 0x01de, 0x0162 }, - { 0x01e0, 0x0155 }, - { 0x01e3, 0x0103 }, - { 0x01e5, 0x013a }, - { 0x01e6, 0x0107 }, - { 0x01e8, 0x010d }, - { 0x01ea, 0x0119 }, - { 0x01ec, 0x011b }, - { 0x01ef, 0x010f }, - { 0x01f0, 0x0111 }, - { 0x01f1, 0x0144 }, - { 0x01f2, 0x0148 }, - { 0x01f5, 0x0151 }, - { 0x01f8, 0x0159 }, - { 0x01f9, 0x016f }, - { 0x01fb, 0x0171 }, - { 0x01fe, 0x0163 }, - { 0x01ff, 0x02d9 }, - { 0x02a1, 0x0126 }, - { 0x02a6, 0x0124 }, - { 0x02a9, 0x0130 }, - { 0x02ab, 0x011e }, - { 0x02ac, 0x0134 }, - { 0x02b1, 0x0127 }, - { 0x02b6, 0x0125 }, - { 0x02b9, 0x0131 }, - { 0x02bb, 0x011f }, - { 0x02bc, 0x0135 }, - { 0x02c5, 0x010a }, - { 0x02c6, 0x0108 }, - { 0x02d5, 0x0120 }, - { 0x02d8, 0x011c }, - { 0x02dd, 0x016c }, - { 0x02de, 0x015c }, - { 0x02e5, 0x010b }, - { 0x02e6, 0x0109 }, - { 0x02f5, 0x0121 }, - { 0x02f8, 0x011d }, - { 0x02fd, 0x016d }, - { 0x02fe, 0x015d }, - { 0x03a2, 0x0138 }, - { 0x03a3, 0x0156 }, - { 0x03a5, 0x0128 }, - { 0x03a6, 0x013b }, - { 0x03aa, 0x0112 }, - { 0x03ab, 0x0122 }, - { 0x03ac, 0x0166 }, - { 0x03b3, 0x0157 }, - { 0x03b5, 0x0129 }, - { 0x03b6, 0x013c }, - { 0x03ba, 0x0113 }, - { 0x03bb, 0x0123 }, - { 0x03bc, 0x0167 }, - { 0x03bd, 0x014a }, - { 0x03bf, 0x014b }, - { 0x03c0, 0x0100 }, - { 0x03c7, 0x012e }, - { 0x03cc, 0x0116 }, - { 0x03cf, 0x012a }, - { 0x03d1, 0x0145 }, - { 0x03d2, 0x014c }, - { 0x03d3, 0x0136 }, - { 0x03d9, 0x0172 }, - { 0x03dd, 0x0168 }, - { 0x03de, 0x016a }, - { 0x03e0, 0x0101 }, - { 0x03e7, 0x012f }, - { 0x03ec, 0x0117 }, - { 0x03ef, 0x012b }, - { 0x03f1, 0x0146 }, - { 0x03f2, 0x014d }, - { 0x03f3, 0x0137 }, - { 0x03f9, 0x0173 }, - { 0x03fd, 0x0169 }, - { 0x03fe, 0x016b }, - { 0x047e, 0x203e }, - { 0x04a1, 0x3002 }, - { 0x04a2, 0x300c }, - { 0x04a3, 0x300d }, - { 0x04a4, 0x3001 }, - { 0x04a5, 0x30fb }, - { 0x04a6, 0x30f2 }, - { 0x04a7, 0x30a1 }, - { 0x04a8, 0x30a3 }, - { 0x04a9, 0x30a5 }, - { 0x04aa, 0x30a7 }, - { 0x04ab, 0x30a9 }, - { 0x04ac, 0x30e3 }, - { 0x04ad, 0x30e5 }, - { 0x04ae, 0x30e7 }, - { 0x04af, 0x30c3 }, - { 0x04b0, 0x30fc }, - { 0x04b1, 0x30a2 }, - { 0x04b2, 0x30a4 }, - { 0x04b3, 0x30a6 }, - { 0x04b4, 0x30a8 }, - { 0x04b5, 0x30aa }, - { 0x04b6, 0x30ab }, - { 0x04b7, 0x30ad }, - { 0x04b8, 0x30af }, - { 0x04b9, 0x30b1 }, - { 0x04ba, 0x30b3 }, - { 0x04bb, 0x30b5 }, - { 0x04bc, 0x30b7 }, - { 0x04bd, 0x30b9 }, - { 0x04be, 0x30bb }, - { 0x04bf, 0x30bd }, - { 0x04c0, 0x30bf }, - { 0x04c1, 0x30c1 }, - { 0x04c2, 0x30c4 }, - { 0x04c3, 0x30c6 }, - { 0x04c4, 0x30c8 }, - { 0x04c5, 0x30ca }, - { 0x04c6, 0x30cb }, - { 0x04c7, 0x30cc }, - { 0x04c8, 0x30cd }, - { 0x04c9, 0x30ce }, - { 0x04ca, 0x30cf }, - { 0x04cb, 0x30d2 }, - { 0x04cc, 0x30d5 }, - { 0x04cd, 0x30d8 }, - { 0x04ce, 0x30db }, - { 0x04cf, 0x30de }, - { 0x04d0, 0x30df }, - { 0x04d1, 0x30e0 }, - { 0x04d2, 0x30e1 }, - { 0x04d3, 0x30e2 }, - { 0x04d4, 0x30e4 }, - { 0x04d5, 0x30e6 }, - { 0x04d6, 0x30e8 }, - { 0x04d7, 0x30e9 }, - { 0x04d8, 0x30ea }, - { 0x04d9, 0x30eb }, - { 0x04da, 0x30ec }, - { 0x04db, 0x30ed }, - { 0x04dc, 0x30ef }, - { 0x04dd, 0x30f3 }, - { 0x04de, 0x309b }, - { 0x04df, 0x309c }, - { 0x05ac, 0x060c }, - { 0x05bb, 0x061b }, - { 0x05bf, 0x061f }, - { 0x05c1, 0x0621 }, - { 0x05c2, 0x0622 }, - { 0x05c3, 0x0623 }, - { 0x05c4, 0x0624 }, - { 0x05c5, 0x0625 }, - { 0x05c6, 0x0626 }, - { 0x05c7, 0x0627 }, - { 0x05c8, 0x0628 }, - { 0x05c9, 0x0629 }, - { 0x05ca, 0x062a }, - { 0x05cb, 0x062b }, - { 0x05cc, 0x062c }, - { 0x05cd, 0x062d }, - { 0x05ce, 0x062e }, - { 0x05cf, 0x062f }, - { 0x05d0, 0x0630 }, - { 0x05d1, 0x0631 }, - { 0x05d2, 0x0632 }, - { 0x05d3, 0x0633 }, - { 0x05d4, 0x0634 }, - { 0x05d5, 0x0635 }, - { 0x05d6, 0x0636 }, - { 0x05d7, 0x0637 }, - { 0x05d8, 0x0638 }, - { 0x05d9, 0x0639 }, - { 0x05da, 0x063a }, - { 0x05e0, 0x0640 }, - { 0x05e1, 0x0641 }, - { 0x05e2, 0x0642 }, - { 0x05e3, 0x0643 }, - { 0x05e4, 0x0644 }, - { 0x05e5, 0x0645 }, - { 0x05e6, 0x0646 }, - { 0x05e7, 0x0647 }, - { 0x05e8, 0x0648 }, - { 0x05e9, 0x0649 }, - { 0x05ea, 0x064a }, - { 0x05eb, 0x064b }, - { 0x05ec, 0x064c }, - { 0x05ed, 0x064d }, - { 0x05ee, 0x064e }, - { 0x05ef, 0x064f }, - { 0x05f0, 0x0650 }, - { 0x05f1, 0x0651 }, - { 0x05f2, 0x0652 }, - { 0x06a1, 0x0452 }, - { 0x06a2, 0x0453 }, - { 0x06a3, 0x0451 }, - { 0x06a4, 0x0454 }, - { 0x06a5, 0x0455 }, - { 0x06a6, 0x0456 }, - { 0x06a7, 0x0457 }, - { 0x06a8, 0x0458 }, - { 0x06a9, 0x0459 }, - { 0x06aa, 0x045a }, - { 0x06ab, 0x045b }, - { 0x06ac, 0x045c }, - { 0x06ae, 0x045e }, - { 0x06af, 0x045f }, - { 0x06b0, 0x2116 }, - { 0x06b1, 0x0402 }, - { 0x06b2, 0x0403 }, - { 0x06b3, 0x0401 }, - { 0x06b4, 0x0404 }, - { 0x06b5, 0x0405 }, - { 0x06b6, 0x0406 }, - { 0x06b7, 0x0407 }, - { 0x06b8, 0x0408 }, - { 0x06b9, 0x0409 }, - { 0x06ba, 0x040a }, - { 0x06bb, 0x040b }, - { 0x06bc, 0x040c }, - { 0x06be, 0x040e }, - { 0x06bf, 0x040f }, - { 0x06c0, 0x044e }, - { 0x06c1, 0x0430 }, - { 0x06c2, 0x0431 }, - { 0x06c3, 0x0446 }, - { 0x06c4, 0x0434 }, - { 0x06c5, 0x0435 }, - { 0x06c6, 0x0444 }, - { 0x06c7, 0x0433 }, - { 0x06c8, 0x0445 }, - { 0x06c9, 0x0438 }, - { 0x06ca, 0x0439 }, - { 0x06cb, 0x043a }, - { 0x06cc, 0x043b }, - { 0x06cd, 0x043c }, - { 0x06ce, 0x043d }, - { 0x06cf, 0x043e }, - { 0x06d0, 0x043f }, - { 0x06d1, 0x044f }, - { 0x06d2, 0x0440 }, - { 0x06d3, 0x0441 }, - { 0x06d4, 0x0442 }, - { 0x06d5, 0x0443 }, - { 0x06d6, 0x0436 }, - { 0x06d7, 0x0432 }, - { 0x06d8, 0x044c }, - { 0x06d9, 0x044b }, - { 0x06da, 0x0437 }, - { 0x06db, 0x0448 }, - { 0x06dc, 0x044d }, - { 0x06dd, 0x0449 }, - { 0x06de, 0x0447 }, - { 0x06df, 0x044a }, - { 0x06e0, 0x042e }, - { 0x06e1, 0x0410 }, - { 0x06e2, 0x0411 }, - { 0x06e3, 0x0426 }, - { 0x06e4, 0x0414 }, - { 0x06e5, 0x0415 }, - { 0x06e6, 0x0424 }, - { 0x06e7, 0x0413 }, - { 0x06e8, 0x0425 }, - { 0x06e9, 0x0418 }, - { 0x06ea, 0x0419 }, - { 0x06eb, 0x041a }, - { 0x06ec, 0x041b }, - { 0x06ed, 0x041c }, - { 0x06ee, 0x041d }, - { 0x06ef, 0x041e }, - { 0x06f0, 0x041f }, - { 0x06f1, 0x042f }, - { 0x06f2, 0x0420 }, - { 0x06f3, 0x0421 }, - { 0x06f4, 0x0422 }, - { 0x06f5, 0x0423 }, - { 0x06f6, 0x0416 }, - { 0x06f7, 0x0412 }, - { 0x06f8, 0x042c }, - { 0x06f9, 0x042b }, - { 0x06fa, 0x0417 }, - { 0x06fb, 0x0428 }, - { 0x06fc, 0x042d }, - { 0x06fd, 0x0429 }, - { 0x06fe, 0x0427 }, - { 0x06ff, 0x042a }, - { 0x07a1, 0x0386 }, - { 0x07a2, 0x0388 }, - { 0x07a3, 0x0389 }, - { 0x07a4, 0x038a }, - { 0x07a5, 0x03aa }, - { 0x07a7, 0x038c }, - { 0x07a8, 0x038e }, - { 0x07a9, 0x03ab }, - { 0x07ab, 0x038f }, - { 0x07ae, 0x0385 }, - { 0x07af, 0x2015 }, - { 0x07b1, 0x03ac }, - { 0x07b2, 0x03ad }, - { 0x07b3, 0x03ae }, - { 0x07b4, 0x03af }, - { 0x07b5, 0x03ca }, - { 0x07b6, 0x0390 }, - { 0x07b7, 0x03cc }, - { 0x07b8, 0x03cd }, - { 0x07b9, 0x03cb }, - { 0x07ba, 0x03b0 }, - { 0x07bb, 0x03ce }, - { 0x07c1, 0x0391 }, - { 0x07c2, 0x0392 }, - { 0x07c3, 0x0393 }, - { 0x07c4, 0x0394 }, - { 0x07c5, 0x0395 }, - { 0x07c6, 0x0396 }, - { 0x07c7, 0x0397 }, - { 0x07c8, 0x0398 }, - { 0x07c9, 0x0399 }, - { 0x07ca, 0x039a }, - { 0x07cb, 0x039b }, - { 0x07cc, 0x039c }, - { 0x07cd, 0x039d }, - { 0x07ce, 0x039e }, - { 0x07cf, 0x039f }, - { 0x07d0, 0x03a0 }, - { 0x07d1, 0x03a1 }, - { 0x07d2, 0x03a3 }, - { 0x07d4, 0x03a4 }, - { 0x07d5, 0x03a5 }, - { 0x07d6, 0x03a6 }, - { 0x07d7, 0x03a7 }, - { 0x07d8, 0x03a8 }, - { 0x07d9, 0x03a9 }, - { 0x07e1, 0x03b1 }, - { 0x07e2, 0x03b2 }, - { 0x07e3, 0x03b3 }, - { 0x07e4, 0x03b4 }, - { 0x07e5, 0x03b5 }, - { 0x07e6, 0x03b6 }, - { 0x07e7, 0x03b7 }, - { 0x07e8, 0x03b8 }, - { 0x07e9, 0x03b9 }, - { 0x07ea, 0x03ba }, - { 0x07eb, 0x03bb }, - { 0x07ec, 0x03bc }, - { 0x07ed, 0x03bd }, - { 0x07ee, 0x03be }, - { 0x07ef, 0x03bf }, - { 0x07f0, 0x03c0 }, - { 0x07f1, 0x03c1 }, - { 0x07f2, 0x03c3 }, - { 0x07f3, 0x03c2 }, - { 0x07f4, 0x03c4 }, - { 0x07f5, 0x03c5 }, - { 0x07f6, 0x03c6 }, - { 0x07f7, 0x03c7 }, - { 0x07f8, 0x03c8 }, - { 0x07f9, 0x03c9 }, - { 0x08a1, 0x23b7 }, - { 0x08a2, 0x250c }, - { 0x08a3, 0x2500 }, - { 0x08a4, 0x2320 }, - { 0x08a5, 0x2321 }, - { 0x08a6, 0x2502 }, - { 0x08a7, 0x23a1 }, - { 0x08a8, 0x23a3 }, - { 0x08a9, 0x23a4 }, - { 0x08aa, 0x23a6 }, - { 0x08ab, 0x239b }, - { 0x08ac, 0x239d }, - { 0x08ad, 0x239e }, - { 0x08ae, 0x23a0 }, - { 0x08af, 0x23a8 }, - { 0x08b0, 0x23ac }, - { 0x08bc, 0x2264 }, - { 0x08bd, 0x2260 }, - { 0x08be, 0x2265 }, - { 0x08bf, 0x222b }, - { 0x08c0, 0x2234 }, - { 0x08c1, 0x221d }, - { 0x08c2, 0x221e }, - { 0x08c5, 0x2207 }, - { 0x08c8, 0x223c }, - { 0x08c9, 0x2243 }, - { 0x08cd, 0x21d4 }, - { 0x08ce, 0x21d2 }, - { 0x08cf, 0x2261 }, - { 0x08d6, 0x221a }, - { 0x08da, 0x2282 }, - { 0x08db, 0x2283 }, - { 0x08dc, 0x2229 }, - { 0x08dd, 0x222a }, - { 0x08de, 0x2227 }, - { 0x08df, 0x2228 }, - { 0x08ef, 0x2202 }, - { 0x08f6, 0x0192 }, - { 0x08fb, 0x2190 }, - { 0x08fc, 0x2191 }, - { 0x08fd, 0x2192 }, - { 0x08fe, 0x2193 }, - { 0x09e0, 0x25c6 }, - { 0x09e1, 0x2592 }, - { 0x09e2, 0x2409 }, - { 0x09e3, 0x240c }, - { 0x09e4, 0x240d }, - { 0x09e5, 0x240a }, - { 0x09e8, 0x2424 }, - { 0x09e9, 0x240b }, - { 0x09ea, 0x2518 }, - { 0x09eb, 0x2510 }, - { 0x09ec, 0x250c }, - { 0x09ed, 0x2514 }, - { 0x09ee, 0x253c }, - { 0x09ef, 0x23ba }, - { 0x09f0, 0x23bb }, - { 0x09f1, 0x2500 }, - { 0x09f2, 0x23bc }, - { 0x09f3, 0x23bd }, - { 0x09f4, 0x251c }, - { 0x09f5, 0x2524 }, - { 0x09f6, 0x2534 }, - { 0x09f7, 0x252c }, - { 0x09f8, 0x2502 }, - { 0x0aa1, 0x2003 }, - { 0x0aa2, 0x2002 }, - { 0x0aa3, 0x2004 }, - { 0x0aa4, 0x2005 }, - { 0x0aa5, 0x2007 }, - { 0x0aa6, 0x2008 }, - { 0x0aa7, 0x2009 }, - { 0x0aa8, 0x200a }, - { 0x0aa9, 0x2014 }, - { 0x0aaa, 0x2013 }, - { 0x0aae, 0x2026 }, - { 0x0aaf, 0x2025 }, - { 0x0ab0, 0x2153 }, - { 0x0ab1, 0x2154 }, - { 0x0ab2, 0x2155 }, - { 0x0ab3, 0x2156 }, - { 0x0ab4, 0x2157 }, - { 0x0ab5, 0x2158 }, - { 0x0ab6, 0x2159 }, - { 0x0ab7, 0x215a }, - { 0x0ab8, 0x2105 }, - { 0x0abb, 0x2012 }, - { 0x0abc, 0x2329 }, - { 0x0abe, 0x232a }, - { 0x0ac3, 0x215b }, - { 0x0ac4, 0x215c }, - { 0x0ac5, 0x215d }, - { 0x0ac6, 0x215e }, - { 0x0ac9, 0x2122 }, - { 0x0aca, 0x2613 }, - { 0x0acc, 0x25c1 }, - { 0x0acd, 0x25b7 }, - { 0x0ace, 0x25cb }, - { 0x0acf, 0x25af }, - { 0x0ad0, 0x2018 }, - { 0x0ad1, 0x2019 }, - { 0x0ad2, 0x201c }, - { 0x0ad3, 0x201d }, - { 0x0ad4, 0x211e }, - { 0x0ad6, 0x2032 }, - { 0x0ad7, 0x2033 }, - { 0x0ad9, 0x271d }, - { 0x0adb, 0x25ac }, - { 0x0adc, 0x25c0 }, - { 0x0add, 0x25b6 }, - { 0x0ade, 0x25cf }, - { 0x0adf, 0x25ae }, - { 0x0ae0, 0x25e6 }, - { 0x0ae1, 0x25ab }, - { 0x0ae2, 0x25ad }, - { 0x0ae3, 0x25b3 }, - { 0x0ae4, 0x25bd }, - { 0x0ae5, 0x2606 }, - { 0x0ae6, 0x2022 }, - { 0x0ae7, 0x25aa }, - { 0x0ae8, 0x25b2 }, - { 0x0ae9, 0x25bc }, - { 0x0aea, 0x261c }, - { 0x0aeb, 0x261e }, - { 0x0aec, 0x2663 }, - { 0x0aed, 0x2666 }, - { 0x0aee, 0x2665 }, - { 0x0af0, 0x2720 }, - { 0x0af1, 0x2020 }, - { 0x0af2, 0x2021 }, - { 0x0af3, 0x2713 }, - { 0x0af4, 0x2717 }, - { 0x0af5, 0x266f }, - { 0x0af6, 0x266d }, - { 0x0af7, 0x2642 }, - { 0x0af8, 0x2640 }, - { 0x0af9, 0x260e }, - { 0x0afa, 0x2315 }, - { 0x0afb, 0x2117 }, - { 0x0afc, 0x2038 }, - { 0x0afd, 0x201a }, - { 0x0afe, 0x201e }, - { 0x0ba3, 0x003c }, - { 0x0ba6, 0x003e }, - { 0x0ba8, 0x2228 }, - { 0x0ba9, 0x2227 }, - { 0x0bc0, 0x00af }, - { 0x0bc2, 0x22a5 }, - { 0x0bc3, 0x2229 }, - { 0x0bc4, 0x230a }, - { 0x0bc6, 0x005f }, - { 0x0bca, 0x2218 }, - { 0x0bcc, 0x2395 }, - { 0x0bce, 0x22a4 }, - { 0x0bcf, 0x25cb }, - { 0x0bd3, 0x2308 }, - { 0x0bd6, 0x222a }, - { 0x0bd8, 0x2283 }, - { 0x0bda, 0x2282 }, - { 0x0bdc, 0x22a2 }, - { 0x0bfc, 0x22a3 }, - { 0x0cdf, 0x2017 }, - { 0x0ce0, 0x05d0 }, - { 0x0ce1, 0x05d1 }, - { 0x0ce2, 0x05d2 }, - { 0x0ce3, 0x05d3 }, - { 0x0ce4, 0x05d4 }, - { 0x0ce5, 0x05d5 }, - { 0x0ce6, 0x05d6 }, - { 0x0ce7, 0x05d7 }, - { 0x0ce8, 0x05d8 }, - { 0x0ce9, 0x05d9 }, - { 0x0cea, 0x05da }, - { 0x0ceb, 0x05db }, - { 0x0cec, 0x05dc }, - { 0x0ced, 0x05dd }, - { 0x0cee, 0x05de }, - { 0x0cef, 0x05df }, - { 0x0cf0, 0x05e0 }, - { 0x0cf1, 0x05e1 }, - { 0x0cf2, 0x05e2 }, - { 0x0cf3, 0x05e3 }, - { 0x0cf4, 0x05e4 }, - { 0x0cf5, 0x05e5 }, - { 0x0cf6, 0x05e6 }, - { 0x0cf7, 0x05e7 }, - { 0x0cf8, 0x05e8 }, - { 0x0cf9, 0x05e9 }, - { 0x0cfa, 0x05ea }, - { 0x0da1, 0x0e01 }, - { 0x0da2, 0x0e02 }, - { 0x0da3, 0x0e03 }, - { 0x0da4, 0x0e04 }, - { 0x0da5, 0x0e05 }, - { 0x0da6, 0x0e06 }, - { 0x0da7, 0x0e07 }, - { 0x0da8, 0x0e08 }, - { 0x0da9, 0x0e09 }, - { 0x0daa, 0x0e0a }, - { 0x0dab, 0x0e0b }, - { 0x0dac, 0x0e0c }, - { 0x0dad, 0x0e0d }, - { 0x0dae, 0x0e0e }, - { 0x0daf, 0x0e0f }, - { 0x0db0, 0x0e10 }, - { 0x0db1, 0x0e11 }, - { 0x0db2, 0x0e12 }, - { 0x0db3, 0x0e13 }, - { 0x0db4, 0x0e14 }, - { 0x0db5, 0x0e15 }, - { 0x0db6, 0x0e16 }, - { 0x0db7, 0x0e17 }, - { 0x0db8, 0x0e18 }, - { 0x0db9, 0x0e19 }, - { 0x0dba, 0x0e1a }, - { 0x0dbb, 0x0e1b }, - { 0x0dbc, 0x0e1c }, - { 0x0dbd, 0x0e1d }, - { 0x0dbe, 0x0e1e }, - { 0x0dbf, 0x0e1f }, - { 0x0dc0, 0x0e20 }, - { 0x0dc1, 0x0e21 }, - { 0x0dc2, 0x0e22 }, - { 0x0dc3, 0x0e23 }, - { 0x0dc4, 0x0e24 }, - { 0x0dc5, 0x0e25 }, - { 0x0dc6, 0x0e26 }, - { 0x0dc7, 0x0e27 }, - { 0x0dc8, 0x0e28 }, - { 0x0dc9, 0x0e29 }, - { 0x0dca, 0x0e2a }, - { 0x0dcb, 0x0e2b }, - { 0x0dcc, 0x0e2c }, - { 0x0dcd, 0x0e2d }, - { 0x0dce, 0x0e2e }, - { 0x0dcf, 0x0e2f }, - { 0x0dd0, 0x0e30 }, - { 0x0dd1, 0x0e31 }, - { 0x0dd2, 0x0e32 }, - { 0x0dd3, 0x0e33 }, - { 0x0dd4, 0x0e34 }, - { 0x0dd5, 0x0e35 }, - { 0x0dd6, 0x0e36 }, - { 0x0dd7, 0x0e37 }, - { 0x0dd8, 0x0e38 }, - { 0x0dd9, 0x0e39 }, - { 0x0dda, 0x0e3a }, - { 0x0ddf, 0x0e3f }, - { 0x0de0, 0x0e40 }, - { 0x0de1, 0x0e41 }, - { 0x0de2, 0x0e42 }, - { 0x0de3, 0x0e43 }, - { 0x0de4, 0x0e44 }, - { 0x0de5, 0x0e45 }, - { 0x0de6, 0x0e46 }, - { 0x0de7, 0x0e47 }, - { 0x0de8, 0x0e48 }, - { 0x0de9, 0x0e49 }, - { 0x0dea, 0x0e4a }, - { 0x0deb, 0x0e4b }, - { 0x0dec, 0x0e4c }, - { 0x0ded, 0x0e4d }, - { 0x0df0, 0x0e50 }, - { 0x0df1, 0x0e51 }, - { 0x0df2, 0x0e52 }, - { 0x0df3, 0x0e53 }, - { 0x0df4, 0x0e54 }, - { 0x0df5, 0x0e55 }, - { 0x0df6, 0x0e56 }, - { 0x0df7, 0x0e57 }, - { 0x0df8, 0x0e58 }, - { 0x0df9, 0x0e59 }, - { 0x0ea1, 0x3131 }, - { 0x0ea2, 0x3132 }, - { 0x0ea3, 0x3133 }, - { 0x0ea4, 0x3134 }, - { 0x0ea5, 0x3135 }, - { 0x0ea6, 0x3136 }, - { 0x0ea7, 0x3137 }, - { 0x0ea8, 0x3138 }, - { 0x0ea9, 0x3139 }, - { 0x0eaa, 0x313a }, - { 0x0eab, 0x313b }, - { 0x0eac, 0x313c }, - { 0x0ead, 0x313d }, - { 0x0eae, 0x313e }, - { 0x0eaf, 0x313f }, - { 0x0eb0, 0x3140 }, - { 0x0eb1, 0x3141 }, - { 0x0eb2, 0x3142 }, - { 0x0eb3, 0x3143 }, - { 0x0eb4, 0x3144 }, - { 0x0eb5, 0x3145 }, - { 0x0eb6, 0x3146 }, - { 0x0eb7, 0x3147 }, - { 0x0eb8, 0x3148 }, - { 0x0eb9, 0x3149 }, - { 0x0eba, 0x314a }, - { 0x0ebb, 0x314b }, - { 0x0ebc, 0x314c }, - { 0x0ebd, 0x314d }, - { 0x0ebe, 0x314e }, - { 0x0ebf, 0x314f }, - { 0x0ec0, 0x3150 }, - { 0x0ec1, 0x3151 }, - { 0x0ec2, 0x3152 }, - { 0x0ec3, 0x3153 }, - { 0x0ec4, 0x3154 }, - { 0x0ec5, 0x3155 }, - { 0x0ec6, 0x3156 }, - { 0x0ec7, 0x3157 }, - { 0x0ec8, 0x3158 }, - { 0x0ec9, 0x3159 }, - { 0x0eca, 0x315a }, - { 0x0ecb, 0x315b }, - { 0x0ecc, 0x315c }, - { 0x0ecd, 0x315d }, - { 0x0ece, 0x315e }, - { 0x0ecf, 0x315f }, - { 0x0ed0, 0x3160 }, - { 0x0ed1, 0x3161 }, - { 0x0ed2, 0x3162 }, - { 0x0ed3, 0x3163 }, - { 0x0ed4, 0x11a8 }, - { 0x0ed5, 0x11a9 }, - { 0x0ed6, 0x11aa }, - { 0x0ed7, 0x11ab }, - { 0x0ed8, 0x11ac }, - { 0x0ed9, 0x11ad }, - { 0x0eda, 0x11ae }, - { 0x0edb, 0x11af }, - { 0x0edc, 0x11b0 }, - { 0x0edd, 0x11b1 }, - { 0x0ede, 0x11b2 }, - { 0x0edf, 0x11b3 }, - { 0x0ee0, 0x11b4 }, - { 0x0ee1, 0x11b5 }, - { 0x0ee2, 0x11b6 }, - { 0x0ee3, 0x11b7 }, - { 0x0ee4, 0x11b8 }, - { 0x0ee5, 0x11b9 }, - { 0x0ee6, 0x11ba }, - { 0x0ee7, 0x11bb }, - { 0x0ee8, 0x11bc }, - { 0x0ee9, 0x11bd }, - { 0x0eea, 0x11be }, - { 0x0eeb, 0x11bf }, - { 0x0eec, 0x11c0 }, - { 0x0eed, 0x11c1 }, - { 0x0eee, 0x11c2 }, - { 0x0eef, 0x316d }, - { 0x0ef0, 0x3171 }, - { 0x0ef1, 0x3178 }, - { 0x0ef2, 0x317f }, - { 0x0ef3, 0x3181 }, - { 0x0ef4, 0x3184 }, - { 0x0ef5, 0x3186 }, - { 0x0ef6, 0x318d }, - { 0x0ef7, 0x318e }, - { 0x0ef8, 0x11eb }, - { 0x0ef9, 0x11f0 }, - { 0x0efa, 0x11f9 }, - { 0x0eff, 0x20a9 }, - { 0x13a4, 0x20ac }, - { 0x13bc, 0x0152 }, - { 0x13bd, 0x0153 }, - { 0x13be, 0x0178 }, - { 0x20ac, 0x20ac }, - { 0xfe50, '`' }, - { 0xfe51, 0x00b4 }, - { 0xfe52, '^' }, - { 0xfe53, '~' }, - { 0xfe54, 0x00af }, - { 0xfe55, 0x02d8 }, - { 0xfe56, 0x02d9 }, - { 0xfe57, 0x00a8 }, - { 0xfe58, 0x02da }, - { 0xfe59, 0x02dd }, - { 0xfe5a, 0x02c7 }, - { 0xfe5b, 0x00b8 }, - { 0xfe5c, 0x02db }, - { 0xfe5d, 0x037a }, - { 0xfe5e, 0x309b }, - { 0xfe5f, 0x309c }, - { 0xfe63, '/' }, - { 0xfe64, 0x02bc }, - { 0xfe65, 0x02bd }, - { 0xfe66, 0x02f5 }, - { 0xfe67, 0x02f3 }, - { 0xfe68, 0x02cd }, - { 0xfe69, 0xa788 }, - { 0xfe6a, 0x02f7 }, - { 0xfe6e, ',' }, - { 0xfe6f, 0x00a4 }, - { 0xfe80, 'a' }, /* XK_dead_a */ - { 0xfe81, 'A' }, /* XK_dead_A */ - { 0xfe82, 'e' }, /* XK_dead_e */ - { 0xfe83, 'E' }, /* XK_dead_E */ - { 0xfe84, 'i' }, /* XK_dead_i */ - { 0xfe85, 'I' }, /* XK_dead_I */ - { 0xfe86, 'o' }, /* XK_dead_o */ - { 0xfe87, 'O' }, /* XK_dead_O */ - { 0xfe88, 'u' }, /* XK_dead_u */ - { 0xfe89, 'U' }, /* XK_dead_U */ - { 0xfe8a, 0x0259 }, - { 0xfe8b, 0x018f }, - { 0xfe8c, 0x00b5 }, - { 0xfe90, '_' }, - { 0xfe91, 0x02c8 }, - { 0xfe92, 0x02cc }, - { 0xff80 /*XKB_KEY_KP_Space*/, ' ' }, - { 0xff95 /*XKB_KEY_KP_7*/, 0x0037 }, - { 0xff96 /*XKB_KEY_KP_4*/, 0x0034 }, - { 0xff97 /*XKB_KEY_KP_8*/, 0x0038 }, - { 0xff98 /*XKB_KEY_KP_6*/, 0x0036 }, - { 0xff99 /*XKB_KEY_KP_2*/, 0x0032 }, - { 0xff9a /*XKB_KEY_KP_9*/, 0x0039 }, - { 0xff9b /*XKB_KEY_KP_3*/, 0x0033 }, - { 0xff9c /*XKB_KEY_KP_1*/, 0x0031 }, - { 0xff9d /*XKB_KEY_KP_5*/, 0x0035 }, - { 0xff9e /*XKB_KEY_KP_0*/, 0x0030 }, - { 0xffaa /*XKB_KEY_KP_Multiply*/, '*' }, - { 0xffab /*XKB_KEY_KP_Add*/, '+' }, - { 0xffac /*XKB_KEY_KP_Separator*/, ',' }, - { 0xffad /*XKB_KEY_KP_Subtract*/, '-' }, - { 0xffae /*XKB_KEY_KP_Decimal*/, '.' }, - { 0xffaf /*XKB_KEY_KP_Divide*/, '/' }, - { 0xffb0 /*XKB_KEY_KP_0*/, 0x0030 }, - { 0xffb1 /*XKB_KEY_KP_1*/, 0x0031 }, - { 0xffb2 /*XKB_KEY_KP_2*/, 0x0032 }, - { 0xffb3 /*XKB_KEY_KP_3*/, 0x0033 }, - { 0xffb4 /*XKB_KEY_KP_4*/, 0x0034 }, - { 0xffb5 /*XKB_KEY_KP_5*/, 0x0035 }, - { 0xffb6 /*XKB_KEY_KP_6*/, 0x0036 }, - { 0xffb7 /*XKB_KEY_KP_7*/, 0x0037 }, - { 0xffb8 /*XKB_KEY_KP_8*/, 0x0038 }, - { 0xffb9 /*XKB_KEY_KP_9*/, 0x0039 }, - { 0xffbd /*XKB_KEY_KP_Equal*/, '=' } -}; - -_SOKOL_PRIVATE int _sapp_x11_error_handler(Display* display, XErrorEvent* event) { - _SOKOL_UNUSED(display); - _sapp.x11.error_code = event->error_code; - return 0; -} - -_SOKOL_PRIVATE void _sapp_x11_grab_error_handler(void) { - _sapp.x11.error_code = Success; - XSetErrorHandler(_sapp_x11_error_handler); -} - -_SOKOL_PRIVATE void _sapp_x11_release_error_handler(void) { - XSync(_sapp.x11.display, False); - XSetErrorHandler(NULL); -} - -_SOKOL_PRIVATE void _sapp_x11_init_extensions(void) { - _sapp.x11.UTF8_STRING = XInternAtom(_sapp.x11.display, "UTF8_STRING", False); - _sapp.x11.WM_PROTOCOLS = XInternAtom(_sapp.x11.display, "WM_PROTOCOLS", False); - _sapp.x11.WM_DELETE_WINDOW = XInternAtom(_sapp.x11.display, "WM_DELETE_WINDOW", False); - _sapp.x11.WM_STATE = XInternAtom(_sapp.x11.display, "WM_STATE", False); - _sapp.x11.NET_WM_NAME = XInternAtom(_sapp.x11.display, "_NET_WM_NAME", False); - _sapp.x11.NET_WM_ICON_NAME = XInternAtom(_sapp.x11.display, "_NET_WM_ICON_NAME", False); - _sapp.x11.NET_WM_ICON = XInternAtom(_sapp.x11.display, "_NET_WM_ICON", False); - _sapp.x11.NET_WM_STATE = XInternAtom(_sapp.x11.display, "_NET_WM_STATE", False); - _sapp.x11.NET_WM_STATE_FULLSCREEN = XInternAtom(_sapp.x11.display, "_NET_WM_STATE_FULLSCREEN", False); - _sapp.x11.CLIPBOARD = XInternAtom(_sapp.x11.display, "CLIPBOARD", False); - _sapp.x11.TARGETS = XInternAtom(_sapp.x11.display, "TARGETS", False); - if (_sapp.drop.enabled) { - _sapp.x11.xdnd.XdndAware = XInternAtom(_sapp.x11.display, "XdndAware", False); - _sapp.x11.xdnd.XdndEnter = XInternAtom(_sapp.x11.display, "XdndEnter", False); - _sapp.x11.xdnd.XdndPosition = XInternAtom(_sapp.x11.display, "XdndPosition", False); - _sapp.x11.xdnd.XdndStatus = XInternAtom(_sapp.x11.display, "XdndStatus", False); - _sapp.x11.xdnd.XdndActionCopy = XInternAtom(_sapp.x11.display, "XdndActionCopy", False); - _sapp.x11.xdnd.XdndDrop = XInternAtom(_sapp.x11.display, "XdndDrop", False); - _sapp.x11.xdnd.XdndFinished = XInternAtom(_sapp.x11.display, "XdndFinished", False); - _sapp.x11.xdnd.XdndSelection = XInternAtom(_sapp.x11.display, "XdndSelection", False); - _sapp.x11.xdnd.XdndTypeList = XInternAtom(_sapp.x11.display, "XdndTypeList", False); - _sapp.x11.xdnd.text_uri_list = XInternAtom(_sapp.x11.display, "text/uri-list", False); - } - - /* check Xi extension for raw mouse input */ - if (XQueryExtension(_sapp.x11.display, "XInputExtension", &_sapp.x11.xi.major_opcode, &_sapp.x11.xi.event_base, &_sapp.x11.xi.error_base)) { - _sapp.x11.xi.major = 2; - _sapp.x11.xi.minor = 0; - if (XIQueryVersion(_sapp.x11.display, &_sapp.x11.xi.major, &_sapp.x11.xi.minor) == Success) { - _sapp.x11.xi.available = true; - } - } -} - -// translate the X11 KeySyms for a key to sokol-app key code -// NOTE: this is only used as a fallback, in case the XBK method fails -// it is layout-dependent and will fail partially on most non-US layouts. -// -_SOKOL_PRIVATE sapp_keycode _sapp_x11_translate_keysyms(const KeySym* keysyms, int width) { - if (width > 1) { - switch (keysyms[1]) { - case XK_KP_0: return SAPP_KEYCODE_KP_0; - case XK_KP_1: return SAPP_KEYCODE_KP_1; - case XK_KP_2: return SAPP_KEYCODE_KP_2; - case XK_KP_3: return SAPP_KEYCODE_KP_3; - case XK_KP_4: return SAPP_KEYCODE_KP_4; - case XK_KP_5: return SAPP_KEYCODE_KP_5; - case XK_KP_6: return SAPP_KEYCODE_KP_6; - case XK_KP_7: return SAPP_KEYCODE_KP_7; - case XK_KP_8: return SAPP_KEYCODE_KP_8; - case XK_KP_9: return SAPP_KEYCODE_KP_9; - case XK_KP_Separator: - case XK_KP_Decimal: return SAPP_KEYCODE_KP_DECIMAL; - case XK_KP_Equal: return SAPP_KEYCODE_KP_EQUAL; - case XK_KP_Enter: return SAPP_KEYCODE_KP_ENTER; - default: break; - } - } - - switch (keysyms[0]) { - case XK_Escape: return SAPP_KEYCODE_ESCAPE; - case XK_Tab: return SAPP_KEYCODE_TAB; - case XK_Shift_L: return SAPP_KEYCODE_LEFT_SHIFT; - case XK_Shift_R: return SAPP_KEYCODE_RIGHT_SHIFT; - case XK_Control_L: return SAPP_KEYCODE_LEFT_CONTROL; - case XK_Control_R: return SAPP_KEYCODE_RIGHT_CONTROL; - case XK_Meta_L: - case XK_Alt_L: return SAPP_KEYCODE_LEFT_ALT; - case XK_Mode_switch: // Mapped to Alt_R on many keyboards - case XK_ISO_Level3_Shift: // AltGr on at least some machines - case XK_Meta_R: - case XK_Alt_R: return SAPP_KEYCODE_RIGHT_ALT; - case XK_Super_L: return SAPP_KEYCODE_LEFT_SUPER; - case XK_Super_R: return SAPP_KEYCODE_RIGHT_SUPER; - case XK_Menu: return SAPP_KEYCODE_MENU; - case XK_Num_Lock: return SAPP_KEYCODE_NUM_LOCK; - case XK_Caps_Lock: return SAPP_KEYCODE_CAPS_LOCK; - case XK_Print: return SAPP_KEYCODE_PRINT_SCREEN; - case XK_Scroll_Lock: return SAPP_KEYCODE_SCROLL_LOCK; - case XK_Pause: return SAPP_KEYCODE_PAUSE; - case XK_Delete: return SAPP_KEYCODE_DELETE; - case XK_BackSpace: return SAPP_KEYCODE_BACKSPACE; - case XK_Return: return SAPP_KEYCODE_ENTER; - case XK_Home: return SAPP_KEYCODE_HOME; - case XK_End: return SAPP_KEYCODE_END; - case XK_Page_Up: return SAPP_KEYCODE_PAGE_UP; - case XK_Page_Down: return SAPP_KEYCODE_PAGE_DOWN; - case XK_Insert: return SAPP_KEYCODE_INSERT; - case XK_Left: return SAPP_KEYCODE_LEFT; - case XK_Right: return SAPP_KEYCODE_RIGHT; - case XK_Down: return SAPP_KEYCODE_DOWN; - case XK_Up: return SAPP_KEYCODE_UP; - case XK_F1: return SAPP_KEYCODE_F1; - case XK_F2: return SAPP_KEYCODE_F2; - case XK_F3: return SAPP_KEYCODE_F3; - case XK_F4: return SAPP_KEYCODE_F4; - case XK_F5: return SAPP_KEYCODE_F5; - case XK_F6: return SAPP_KEYCODE_F6; - case XK_F7: return SAPP_KEYCODE_F7; - case XK_F8: return SAPP_KEYCODE_F8; - case XK_F9: return SAPP_KEYCODE_F9; - case XK_F10: return SAPP_KEYCODE_F10; - case XK_F11: return SAPP_KEYCODE_F11; - case XK_F12: return SAPP_KEYCODE_F12; - case XK_F13: return SAPP_KEYCODE_F13; - case XK_F14: return SAPP_KEYCODE_F14; - case XK_F15: return SAPP_KEYCODE_F15; - case XK_F16: return SAPP_KEYCODE_F16; - case XK_F17: return SAPP_KEYCODE_F17; - case XK_F18: return SAPP_KEYCODE_F18; - case XK_F19: return SAPP_KEYCODE_F19; - case XK_F20: return SAPP_KEYCODE_F20; - case XK_F21: return SAPP_KEYCODE_F21; - case XK_F22: return SAPP_KEYCODE_F22; - case XK_F23: return SAPP_KEYCODE_F23; - case XK_F24: return SAPP_KEYCODE_F24; - case XK_F25: return SAPP_KEYCODE_F25; - - // numeric keypad - case XK_KP_Divide: return SAPP_KEYCODE_KP_DIVIDE; - case XK_KP_Multiply: return SAPP_KEYCODE_KP_MULTIPLY; - case XK_KP_Subtract: return SAPP_KEYCODE_KP_SUBTRACT; - case XK_KP_Add: return SAPP_KEYCODE_KP_ADD; - - // these should have been detected in secondary keysym test above! - case XK_KP_Insert: return SAPP_KEYCODE_KP_0; - case XK_KP_End: return SAPP_KEYCODE_KP_1; - case XK_KP_Down: return SAPP_KEYCODE_KP_2; - case XK_KP_Page_Down: return SAPP_KEYCODE_KP_3; - case XK_KP_Left: return SAPP_KEYCODE_KP_4; - case XK_KP_Right: return SAPP_KEYCODE_KP_6; - case XK_KP_Home: return SAPP_KEYCODE_KP_7; - case XK_KP_Up: return SAPP_KEYCODE_KP_8; - case XK_KP_Page_Up: return SAPP_KEYCODE_KP_9; - case XK_KP_Delete: return SAPP_KEYCODE_KP_DECIMAL; - case XK_KP_Equal: return SAPP_KEYCODE_KP_EQUAL; - case XK_KP_Enter: return SAPP_KEYCODE_KP_ENTER; - - // last resort: Check for printable keys (should not happen if the XKB - // extension is available). This will give a layout dependent mapping - // (which is wrong, and we may miss some keys, especially on non-US - // keyboards), but it's better than nothing... - case XK_a: return SAPP_KEYCODE_A; - case XK_b: return SAPP_KEYCODE_B; - case XK_c: return SAPP_KEYCODE_C; - case XK_d: return SAPP_KEYCODE_D; - case XK_e: return SAPP_KEYCODE_E; - case XK_f: return SAPP_KEYCODE_F; - case XK_g: return SAPP_KEYCODE_G; - case XK_h: return SAPP_KEYCODE_H; - case XK_i: return SAPP_KEYCODE_I; - case XK_j: return SAPP_KEYCODE_J; - case XK_k: return SAPP_KEYCODE_K; - case XK_l: return SAPP_KEYCODE_L; - case XK_m: return SAPP_KEYCODE_M; - case XK_n: return SAPP_KEYCODE_N; - case XK_o: return SAPP_KEYCODE_O; - case XK_p: return SAPP_KEYCODE_P; - case XK_q: return SAPP_KEYCODE_Q; - case XK_r: return SAPP_KEYCODE_R; - case XK_s: return SAPP_KEYCODE_S; - case XK_t: return SAPP_KEYCODE_T; - case XK_u: return SAPP_KEYCODE_U; - case XK_v: return SAPP_KEYCODE_V; - case XK_w: return SAPP_KEYCODE_W; - case XK_x: return SAPP_KEYCODE_X; - case XK_y: return SAPP_KEYCODE_Y; - case XK_z: return SAPP_KEYCODE_Z; - case XK_1: return SAPP_KEYCODE_1; - case XK_2: return SAPP_KEYCODE_2; - case XK_3: return SAPP_KEYCODE_3; - case XK_4: return SAPP_KEYCODE_4; - case XK_5: return SAPP_KEYCODE_5; - case XK_6: return SAPP_KEYCODE_6; - case XK_7: return SAPP_KEYCODE_7; - case XK_8: return SAPP_KEYCODE_8; - case XK_9: return SAPP_KEYCODE_9; - case XK_0: return SAPP_KEYCODE_0; - case XK_space: return SAPP_KEYCODE_SPACE; - case XK_minus: return SAPP_KEYCODE_MINUS; - case XK_equal: return SAPP_KEYCODE_EQUAL; - case XK_bracketleft: return SAPP_KEYCODE_LEFT_BRACKET; - case XK_bracketright: return SAPP_KEYCODE_RIGHT_BRACKET; - case XK_backslash: return SAPP_KEYCODE_BACKSLASH; - case XK_semicolon: return SAPP_KEYCODE_SEMICOLON; - case XK_apostrophe: return SAPP_KEYCODE_APOSTROPHE; - case XK_grave: return SAPP_KEYCODE_GRAVE_ACCENT; - case XK_comma: return SAPP_KEYCODE_COMMA; - case XK_period: return SAPP_KEYCODE_PERIOD; - case XK_slash: return SAPP_KEYCODE_SLASH; - case XK_less: return SAPP_KEYCODE_WORLD_1; // At least in some layouts... - default: break; - } - - // no matching translation was found - return SAPP_KEYCODE_INVALID; -} - - -// setup dynamic keycode/scancode mapping tables, this is required -// for getting layout-independent keycodes on X11. -// -// see GLFW x11_init.c/createKeyTables() -_SOKOL_PRIVATE void _sapp_x11_init_keytable(void) { - for (int i = 0; i < SAPP_MAX_KEYCODES; i++) { - _sapp.keycodes[i] = SAPP_KEYCODE_INVALID; - } - // use XKB to determine physical key locations independently of the current keyboard layout - XkbDescPtr desc = XkbGetMap(_sapp.x11.display, 0, XkbUseCoreKbd); - SOKOL_ASSERT(desc); - XkbGetNames(_sapp.x11.display, XkbKeyNamesMask | XkbKeyAliasesMask, desc); - - const int scancode_min = desc->min_key_code; - const int scancode_max = desc->max_key_code; - - const struct { sapp_keycode key; const char* name; } keymap[] = { - { SAPP_KEYCODE_GRAVE_ACCENT, "TLDE" }, - { SAPP_KEYCODE_1, "AE01" }, - { SAPP_KEYCODE_2, "AE02" }, - { SAPP_KEYCODE_3, "AE03" }, - { SAPP_KEYCODE_4, "AE04" }, - { SAPP_KEYCODE_5, "AE05" }, - { SAPP_KEYCODE_6, "AE06" }, - { SAPP_KEYCODE_7, "AE07" }, - { SAPP_KEYCODE_8, "AE08" }, - { SAPP_KEYCODE_9, "AE09" }, - { SAPP_KEYCODE_0, "AE10" }, - { SAPP_KEYCODE_MINUS, "AE11" }, - { SAPP_KEYCODE_EQUAL, "AE12" }, - { SAPP_KEYCODE_Q, "AD01" }, - { SAPP_KEYCODE_W, "AD02" }, - { SAPP_KEYCODE_E, "AD03" }, - { SAPP_KEYCODE_R, "AD04" }, - { SAPP_KEYCODE_T, "AD05" }, - { SAPP_KEYCODE_Y, "AD06" }, - { SAPP_KEYCODE_U, "AD07" }, - { SAPP_KEYCODE_I, "AD08" }, - { SAPP_KEYCODE_O, "AD09" }, - { SAPP_KEYCODE_P, "AD10" }, - { SAPP_KEYCODE_LEFT_BRACKET, "AD11" }, - { SAPP_KEYCODE_RIGHT_BRACKET, "AD12" }, - { SAPP_KEYCODE_A, "AC01" }, - { SAPP_KEYCODE_S, "AC02" }, - { SAPP_KEYCODE_D, "AC03" }, - { SAPP_KEYCODE_F, "AC04" }, - { SAPP_KEYCODE_G, "AC05" }, - { SAPP_KEYCODE_H, "AC06" }, - { SAPP_KEYCODE_J, "AC07" }, - { SAPP_KEYCODE_K, "AC08" }, - { SAPP_KEYCODE_L, "AC09" }, - { SAPP_KEYCODE_SEMICOLON, "AC10" }, - { SAPP_KEYCODE_APOSTROPHE, "AC11" }, - { SAPP_KEYCODE_Z, "AB01" }, - { SAPP_KEYCODE_X, "AB02" }, - { SAPP_KEYCODE_C, "AB03" }, - { SAPP_KEYCODE_V, "AB04" }, - { SAPP_KEYCODE_B, "AB05" }, - { SAPP_KEYCODE_N, "AB06" }, - { SAPP_KEYCODE_M, "AB07" }, - { SAPP_KEYCODE_COMMA, "AB08" }, - { SAPP_KEYCODE_PERIOD, "AB09" }, - { SAPP_KEYCODE_SLASH, "AB10" }, - { SAPP_KEYCODE_BACKSLASH, "BKSL" }, - { SAPP_KEYCODE_WORLD_1, "LSGT" }, - { SAPP_KEYCODE_SPACE, "SPCE" }, - { SAPP_KEYCODE_ESCAPE, "ESC" }, - { SAPP_KEYCODE_ENTER, "RTRN" }, - { SAPP_KEYCODE_TAB, "TAB" }, - { SAPP_KEYCODE_BACKSPACE, "BKSP" }, - { SAPP_KEYCODE_INSERT, "INS" }, - { SAPP_KEYCODE_DELETE, "DELE" }, - { SAPP_KEYCODE_RIGHT, "RGHT" }, - { SAPP_KEYCODE_LEFT, "LEFT" }, - { SAPP_KEYCODE_DOWN, "DOWN" }, - { SAPP_KEYCODE_UP, "UP" }, - { SAPP_KEYCODE_PAGE_UP, "PGUP" }, - { SAPP_KEYCODE_PAGE_DOWN, "PGDN" }, - { SAPP_KEYCODE_HOME, "HOME" }, - { SAPP_KEYCODE_END, "END" }, - { SAPP_KEYCODE_CAPS_LOCK, "CAPS" }, - { SAPP_KEYCODE_SCROLL_LOCK, "SCLK" }, - { SAPP_KEYCODE_NUM_LOCK, "NMLK" }, - { SAPP_KEYCODE_PRINT_SCREEN, "PRSC" }, - { SAPP_KEYCODE_PAUSE, "PAUS" }, - { SAPP_KEYCODE_F1, "FK01" }, - { SAPP_KEYCODE_F2, "FK02" }, - { SAPP_KEYCODE_F3, "FK03" }, - { SAPP_KEYCODE_F4, "FK04" }, - { SAPP_KEYCODE_F5, "FK05" }, - { SAPP_KEYCODE_F6, "FK06" }, - { SAPP_KEYCODE_F7, "FK07" }, - { SAPP_KEYCODE_F8, "FK08" }, - { SAPP_KEYCODE_F9, "FK09" }, - { SAPP_KEYCODE_F10, "FK10" }, - { SAPP_KEYCODE_F11, "FK11" }, - { SAPP_KEYCODE_F12, "FK12" }, - { SAPP_KEYCODE_F13, "FK13" }, - { SAPP_KEYCODE_F14, "FK14" }, - { SAPP_KEYCODE_F15, "FK15" }, - { SAPP_KEYCODE_F16, "FK16" }, - { SAPP_KEYCODE_F17, "FK17" }, - { SAPP_KEYCODE_F18, "FK18" }, - { SAPP_KEYCODE_F19, "FK19" }, - { SAPP_KEYCODE_F20, "FK20" }, - { SAPP_KEYCODE_F21, "FK21" }, - { SAPP_KEYCODE_F22, "FK22" }, - { SAPP_KEYCODE_F23, "FK23" }, - { SAPP_KEYCODE_F24, "FK24" }, - { SAPP_KEYCODE_F25, "FK25" }, - { SAPP_KEYCODE_KP_0, "KP0" }, - { SAPP_KEYCODE_KP_1, "KP1" }, - { SAPP_KEYCODE_KP_2, "KP2" }, - { SAPP_KEYCODE_KP_3, "KP3" }, - { SAPP_KEYCODE_KP_4, "KP4" }, - { SAPP_KEYCODE_KP_5, "KP5" }, - { SAPP_KEYCODE_KP_6, "KP6" }, - { SAPP_KEYCODE_KP_7, "KP7" }, - { SAPP_KEYCODE_KP_8, "KP8" }, - { SAPP_KEYCODE_KP_9, "KP9" }, - { SAPP_KEYCODE_KP_DECIMAL, "KPDL" }, - { SAPP_KEYCODE_KP_DIVIDE, "KPDV" }, - { SAPP_KEYCODE_KP_MULTIPLY, "KPMU" }, - { SAPP_KEYCODE_KP_SUBTRACT, "KPSU" }, - { SAPP_KEYCODE_KP_ADD, "KPAD" }, - { SAPP_KEYCODE_KP_ENTER, "KPEN" }, - { SAPP_KEYCODE_KP_EQUAL, "KPEQ" }, - { SAPP_KEYCODE_LEFT_SHIFT, "LFSH" }, - { SAPP_KEYCODE_LEFT_CONTROL, "LCTL" }, - { SAPP_KEYCODE_LEFT_ALT, "LALT" }, - { SAPP_KEYCODE_LEFT_SUPER, "LWIN" }, - { SAPP_KEYCODE_RIGHT_SHIFT, "RTSH" }, - { SAPP_KEYCODE_RIGHT_CONTROL, "RCTL" }, - { SAPP_KEYCODE_RIGHT_ALT, "RALT" }, - { SAPP_KEYCODE_RIGHT_ALT, "LVL3" }, - { SAPP_KEYCODE_RIGHT_ALT, "MDSW" }, - { SAPP_KEYCODE_RIGHT_SUPER, "RWIN" }, - { SAPP_KEYCODE_MENU, "MENU" } - }; - const int num_keymap_items = (int)(sizeof(keymap) / sizeof(keymap[0])); - - // find X11 keycode to sokol-app key code mapping - for (int scancode = scancode_min; scancode <= scancode_max; scancode++) { - sapp_keycode key = SAPP_KEYCODE_INVALID; - for (int i = 0; i < num_keymap_items; i++) { - if (strncmp(desc->names->keys[scancode].name, keymap[i].name, XkbKeyNameLength) == 0) { - key = keymap[i].key; - break; - } - } - - // fall back to key aliases in case the key name did not match - for (int i = 0; i < desc->names->num_key_aliases; i++) { - if (key != SAPP_KEYCODE_INVALID) { - break; - } - if (strncmp(desc->names->key_aliases[i].real, desc->names->keys[scancode].name, XkbKeyNameLength) != 0) { - continue; - } - for (int j = 0; j < num_keymap_items; j++) { - if (strncmp(desc->names->key_aliases[i].alias, keymap[j].name, XkbKeyNameLength) == 0) { - key = keymap[j].key; - break; - } - } - } - _sapp.keycodes[scancode] = key; - } - XkbFreeNames(desc, XkbKeyNamesMask, True); - XkbFreeKeyboard(desc, 0, True); - - int width = 0; - KeySym* keysyms = XGetKeyboardMapping(_sapp.x11.display, scancode_min, scancode_max - scancode_min + 1, &width); - for (int scancode = scancode_min; scancode <= scancode_max; scancode++) { - // translate untranslated key codes using the traditional X11 KeySym lookups - if (_sapp.keycodes[scancode] == SAPP_KEYCODE_INVALID) { - const size_t base = (size_t)((scancode - scancode_min) * width); - _sapp.keycodes[scancode] = _sapp_x11_translate_keysyms(&keysyms[base], width); - } - } - XFree(keysyms); -} - -_SOKOL_PRIVATE void _sapp_x11_query_system_dpi(void) { - /* from GLFW: - - NOTE: Default to the display-wide DPI as we don't currently have a policy - for which monitor a window is considered to be on - - _sapp.x11.dpi = DisplayWidth(_sapp.x11.display, _sapp.x11.screen) * - 25.4f / DisplayWidthMM(_sapp.x11.display, _sapp.x11.screen); - - NOTE: Basing the scale on Xft.dpi where available should provide the most - consistent user experience (matches Qt, Gtk, etc), although not - always the most accurate one - */ - bool dpi_ok = false; - char* rms = XResourceManagerString(_sapp.x11.display); - if (rms) { - XrmDatabase db = XrmGetStringDatabase(rms); - if (db) { - XrmValue value; - char* type = NULL; - if (XrmGetResource(db, "Xft.dpi", "Xft.Dpi", &type, &value)) { - if (type && strcmp(type, "String") == 0) { - _sapp.x11.dpi = atof(value.addr); - dpi_ok = true; - } - } - XrmDestroyDatabase(db); - } - } - // fallback if querying DPI had failed: assume the standard DPI 96.0f - if (!dpi_ok) { - _sapp.x11.dpi = 96.0f; - _SAPP_WARN(LINUX_X11_QUERY_SYSTEM_DPI_FAILED); - } -} - -#if defined(_SAPP_GLX) - -_SOKOL_PRIVATE bool _sapp_glx_has_ext(const char* ext, const char* extensions) { - SOKOL_ASSERT(ext); - const char* start = extensions; - while (true) { - const char* where = strstr(start, ext); - if (!where) { - return false; - } - const char* terminator = where + strlen(ext); - if ((where == start) || (*(where - 1) == ' ')) { - if (*terminator == ' ' || *terminator == '\0') { - break; - } - } - start = terminator; - } - return true; -} - -_SOKOL_PRIVATE bool _sapp_glx_extsupported(const char* ext, const char* extensions) { - if (extensions) { - return _sapp_glx_has_ext(ext, extensions); - } else { - return false; - } -} - -_SOKOL_PRIVATE void* _sapp_glx_getprocaddr(const char* procname) -{ - if (_sapp.glx.GetProcAddress) { - return (void*) _sapp.glx.GetProcAddress(procname); - } else if (_sapp.glx.GetProcAddressARB) { - return (void*) _sapp.glx.GetProcAddressARB(procname); - } else { - return dlsym(_sapp.glx.libgl, procname); - } -} - -_SOKOL_PRIVATE void _sapp_glx_init(void) { - const char* sonames[] = { "libGL.so.1", "libGL.so", 0 }; - for (int i = 0; sonames[i]; i++) { - _sapp.glx.libgl = dlopen(sonames[i], RTLD_LAZY|RTLD_GLOBAL); - if (_sapp.glx.libgl) { - break; - } - } - if (!_sapp.glx.libgl) { - _SAPP_PANIC(LINUX_GLX_LOAD_LIBGL_FAILED); - } - _sapp.glx.GetFBConfigs = (PFNGLXGETFBCONFIGSPROC) dlsym(_sapp.glx.libgl, "glXGetFBConfigs"); - _sapp.glx.GetFBConfigAttrib = (PFNGLXGETFBCONFIGATTRIBPROC) dlsym(_sapp.glx.libgl, "glXGetFBConfigAttrib"); - _sapp.glx.GetClientString = (PFNGLXGETCLIENTSTRINGPROC) dlsym(_sapp.glx.libgl, "glXGetClientString"); - _sapp.glx.QueryExtension = (PFNGLXQUERYEXTENSIONPROC) dlsym(_sapp.glx.libgl, "glXQueryExtension"); - _sapp.glx.QueryVersion = (PFNGLXQUERYVERSIONPROC) dlsym(_sapp.glx.libgl, "glXQueryVersion"); - _sapp.glx.DestroyContext = (PFNGLXDESTROYCONTEXTPROC) dlsym(_sapp.glx.libgl, "glXDestroyContext"); - _sapp.glx.MakeCurrent = (PFNGLXMAKECURRENTPROC) dlsym(_sapp.glx.libgl, "glXMakeCurrent"); - _sapp.glx.SwapBuffers = (PFNGLXSWAPBUFFERSPROC) dlsym(_sapp.glx.libgl, "glXSwapBuffers"); - _sapp.glx.QueryExtensionsString = (PFNGLXQUERYEXTENSIONSSTRINGPROC) dlsym(_sapp.glx.libgl, "glXQueryExtensionsString"); - _sapp.glx.CreateWindow = (PFNGLXCREATEWINDOWPROC) dlsym(_sapp.glx.libgl, "glXCreateWindow"); - _sapp.glx.DestroyWindow = (PFNGLXDESTROYWINDOWPROC) dlsym(_sapp.glx.libgl, "glXDestroyWindow"); - _sapp.glx.GetProcAddress = (PFNGLXGETPROCADDRESSPROC) dlsym(_sapp.glx.libgl, "glXGetProcAddress"); - _sapp.glx.GetProcAddressARB = (PFNGLXGETPROCADDRESSPROC) dlsym(_sapp.glx.libgl, "glXGetProcAddressARB"); - _sapp.glx.GetVisualFromFBConfig = (PFNGLXGETVISUALFROMFBCONFIGPROC) dlsym(_sapp.glx.libgl, "glXGetVisualFromFBConfig"); - if (!_sapp.glx.GetFBConfigs || - !_sapp.glx.GetFBConfigAttrib || - !_sapp.glx.GetClientString || - !_sapp.glx.QueryExtension || - !_sapp.glx.QueryVersion || - !_sapp.glx.DestroyContext || - !_sapp.glx.MakeCurrent || - !_sapp.glx.SwapBuffers || - !_sapp.glx.QueryExtensionsString || - !_sapp.glx.CreateWindow || - !_sapp.glx.DestroyWindow || - !_sapp.glx.GetProcAddress || - !_sapp.glx.GetProcAddressARB || - !_sapp.glx.GetVisualFromFBConfig) - { - _SAPP_PANIC(LINUX_GLX_LOAD_ENTRY_POINTS_FAILED); - } - - if (!_sapp.glx.QueryExtension(_sapp.x11.display, &_sapp.glx.error_base, &_sapp.glx.event_base)) { - _SAPP_PANIC(LINUX_GLX_EXTENSION_NOT_FOUND); - } - if (!_sapp.glx.QueryVersion(_sapp.x11.display, &_sapp.glx.major, &_sapp.glx.minor)) { - _SAPP_PANIC(LINUX_GLX_QUERY_VERSION_FAILED); - } - if (_sapp.glx.major == 1 && _sapp.glx.minor < 3) { - _SAPP_PANIC(LINUX_GLX_VERSION_TOO_LOW); - } - const char* exts = _sapp.glx.QueryExtensionsString(_sapp.x11.display, _sapp.x11.screen); - if (_sapp_glx_extsupported("GLX_EXT_swap_control", exts)) { - _sapp.glx.SwapIntervalEXT = (PFNGLXSWAPINTERVALEXTPROC) _sapp_glx_getprocaddr("glXSwapIntervalEXT"); - _sapp.glx.EXT_swap_control = 0 != _sapp.glx.SwapIntervalEXT; - } - if (_sapp_glx_extsupported("GLX_MESA_swap_control", exts)) { - _sapp.glx.SwapIntervalMESA = (PFNGLXSWAPINTERVALMESAPROC) _sapp_glx_getprocaddr("glXSwapIntervalMESA"); - _sapp.glx.MESA_swap_control = 0 != _sapp.glx.SwapIntervalMESA; - } - _sapp.glx.ARB_multisample = _sapp_glx_extsupported("GLX_ARB_multisample", exts); - if (_sapp_glx_extsupported("GLX_ARB_create_context", exts)) { - _sapp.glx.CreateContextAttribsARB = (PFNGLXCREATECONTEXTATTRIBSARBPROC) _sapp_glx_getprocaddr("glXCreateContextAttribsARB"); - _sapp.glx.ARB_create_context = 0 != _sapp.glx.CreateContextAttribsARB; - } - _sapp.glx.ARB_create_context_profile = _sapp_glx_extsupported("GLX_ARB_create_context_profile", exts); -} - -_SOKOL_PRIVATE int _sapp_glx_attrib(GLXFBConfig fbconfig, int attrib) { - int value; - _sapp.glx.GetFBConfigAttrib(_sapp.x11.display, fbconfig, attrib, &value); - return value; -} - -_SOKOL_PRIVATE GLXFBConfig _sapp_glx_choosefbconfig(void) { - GLXFBConfig* native_configs; - _sapp_gl_fbconfig* usable_configs; - const _sapp_gl_fbconfig* closest; - int i, native_count, usable_count; - const char* vendor; - bool trust_window_bit = true; - - /* HACK: This is a (hopefully temporary) workaround for Chromium - (VirtualBox GL) not setting the window bit on any GLXFBConfigs - */ - vendor = _sapp.glx.GetClientString(_sapp.x11.display, GLX_VENDOR); - if (vendor && strcmp(vendor, "Chromium") == 0) { - trust_window_bit = false; - } - - native_configs = _sapp.glx.GetFBConfigs(_sapp.x11.display, _sapp.x11.screen, &native_count); - if (!native_configs || !native_count) { - _SAPP_PANIC(LINUX_GLX_NO_GLXFBCONFIGS); - } - - usable_configs = (_sapp_gl_fbconfig*) _sapp_malloc_clear((size_t)native_count * sizeof(_sapp_gl_fbconfig)); - usable_count = 0; - for (i = 0; i < native_count; i++) { - const GLXFBConfig n = native_configs[i]; - _sapp_gl_fbconfig* u = usable_configs + usable_count; - _sapp_gl_init_fbconfig(u); - - /* Only consider RGBA GLXFBConfigs */ - if (0 == (_sapp_glx_attrib(n, GLX_RENDER_TYPE) & GLX_RGBA_BIT)) { - continue; - } - /* Only consider window GLXFBConfigs */ - if (0 == (_sapp_glx_attrib(n, GLX_DRAWABLE_TYPE) & GLX_WINDOW_BIT)) { - if (trust_window_bit) { - continue; - } - } - u->red_bits = _sapp_glx_attrib(n, GLX_RED_SIZE); - u->green_bits = _sapp_glx_attrib(n, GLX_GREEN_SIZE); - u->blue_bits = _sapp_glx_attrib(n, GLX_BLUE_SIZE); - u->alpha_bits = _sapp_glx_attrib(n, GLX_ALPHA_SIZE); - u->depth_bits = _sapp_glx_attrib(n, GLX_DEPTH_SIZE); - u->stencil_bits = _sapp_glx_attrib(n, GLX_STENCIL_SIZE); - if (_sapp_glx_attrib(n, GLX_DOUBLEBUFFER)) { - u->doublebuffer = true; - } - if (_sapp.glx.ARB_multisample) { - u->samples = _sapp_glx_attrib(n, GLX_SAMPLES); - } - u->handle = (uintptr_t) n; - usable_count++; - } - _sapp_gl_fbconfig desired; - _sapp_gl_init_fbconfig(&desired); - desired.red_bits = 8; - desired.green_bits = 8; - desired.blue_bits = 8; - desired.alpha_bits = 8; - desired.depth_bits = 24; - desired.stencil_bits = 8; - desired.doublebuffer = true; - desired.samples = _sapp.sample_count > 1 ? _sapp.sample_count : 0; - closest = _sapp_gl_choose_fbconfig(&desired, usable_configs, usable_count); - GLXFBConfig result = 0; - if (closest) { - result = (GLXFBConfig) closest->handle; - } - XFree(native_configs); - _sapp_free(usable_configs); - return result; -} - -_SOKOL_PRIVATE void _sapp_glx_choose_visual(Visual** visual, int* depth) { - GLXFBConfig native = _sapp_glx_choosefbconfig(); - if (0 == native) { - _SAPP_PANIC(LINUX_GLX_NO_SUITABLE_GLXFBCONFIG); - } - XVisualInfo* result = _sapp.glx.GetVisualFromFBConfig(_sapp.x11.display, native); - if (!result) { - _SAPP_PANIC(LINUX_GLX_GET_VISUAL_FROM_FBCONFIG_FAILED); - } - *visual = result->visual; - *depth = result->depth; - XFree(result); -} - -_SOKOL_PRIVATE void _sapp_glx_make_current(void) { - _sapp.glx.MakeCurrent(_sapp.x11.display, _sapp.glx.window, _sapp.glx.ctx); - glGetIntegerv(GL_FRAMEBUFFER_BINDING, (GLint*)&_sapp.gl.framebuffer); -} - -_SOKOL_PRIVATE void _sapp_glx_create_context(void) { - GLXFBConfig native = _sapp_glx_choosefbconfig(); - if (0 == native){ - _SAPP_PANIC(LINUX_GLX_NO_SUITABLE_GLXFBCONFIG); - } - if (!(_sapp.glx.ARB_create_context && _sapp.glx.ARB_create_context_profile)) { - _SAPP_PANIC(LINUX_GLX_REQUIRED_EXTENSIONS_MISSING); - } - _sapp_x11_grab_error_handler(); - const int attribs[] = { - GLX_CONTEXT_MAJOR_VERSION_ARB, _sapp.desc.gl.major_version, - GLX_CONTEXT_MINOR_VERSION_ARB, _sapp.desc.gl.minor_version, - GLX_CONTEXT_PROFILE_MASK_ARB, GLX_CONTEXT_CORE_PROFILE_BIT_ARB, - GLX_CONTEXT_FLAGS_ARB, GLX_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB, - 0, 0 - }; - _sapp.glx.ctx = _sapp.glx.CreateContextAttribsARB(_sapp.x11.display, native, NULL, True, attribs); - if (!_sapp.glx.ctx) { - _SAPP_PANIC(LINUX_GLX_CREATE_CONTEXT_FAILED); - } - _sapp_x11_release_error_handler(); - _sapp.glx.window = _sapp.glx.CreateWindow(_sapp.x11.display, native, _sapp.x11.window, NULL); - if (!_sapp.glx.window) { - _SAPP_PANIC(LINUX_GLX_CREATE_WINDOW_FAILED); - } - _sapp_glx_make_current(); -} - -_SOKOL_PRIVATE void _sapp_glx_destroy_context(void) { - if (_sapp.glx.window) { - _sapp.glx.DestroyWindow(_sapp.x11.display, _sapp.glx.window); - _sapp.glx.window = 0; - } - if (_sapp.glx.ctx) { - _sapp.glx.DestroyContext(_sapp.x11.display, _sapp.glx.ctx); - _sapp.glx.ctx = 0; - } -} - -_SOKOL_PRIVATE void _sapp_glx_swap_buffers(void) { - // Modified by tettou771 for TrussC: skip present support - if (_sapp.skip_present) { _sapp.skip_present = false; return; } - _sapp.glx.SwapBuffers(_sapp.x11.display, _sapp.glx.window); -} - -_SOKOL_PRIVATE void _sapp_glx_swapinterval(int interval) { - if (_sapp.glx.EXT_swap_control) { - _sapp.glx.SwapIntervalEXT(_sapp.x11.display, _sapp.glx.window, interval); - } else if (_sapp.glx.MESA_swap_control) { - _sapp.glx.SwapIntervalMESA(interval); - } -} - -#endif // _SAPP_GLX - -_SOKOL_PRIVATE void _sapp_x11_send_event(Atom type, int a, int b, int c, int d, int e) { - _SAPP_STRUCT(XEvent, event); - event.type = ClientMessage; - event.xclient.window = _sapp.x11.window; - event.xclient.format = 32; - event.xclient.message_type = type; - event.xclient.data.l[0] = a; - event.xclient.data.l[1] = b; - event.xclient.data.l[2] = c; - event.xclient.data.l[3] = d; - event.xclient.data.l[4] = e; - - XSendEvent(_sapp.x11.display, _sapp.x11.root, - False, - SubstructureNotifyMask | SubstructureRedirectMask, - &event); -} - -_SOKOL_PRIVATE bool _sapp_x11_wait_for_event(int event_type, double timeout_sec, XEvent* out_event) { - _sapp_timestamp_t ts; - _sapp_timestamp_init(&ts); - while (!XCheckTypedWindowEvent(_sapp.x11.display, _sapp.x11.window, event_type, out_event)) { - struct pollfd fd = { ConnectionNumber(_sapp.x11.display), POLLIN, 0 }; - poll(&fd, 1, timeout_sec * 1000); - if (_sapp_timestamp_now(&ts) > timeout_sec) { - return false; - } - } - return true; -} - -_SOKOL_PRIVATE void _sapp_x11_app_event(sapp_event_type type) { - if (_sapp_events_enabled()) { - _sapp_init_event(type); - _sapp_call_event(&_sapp.event); - } -} - -_SOKOL_PRIVATE void _sapp_x11_update_dimensions(int x11_window_width, int x11_window_height) { - // NOTE: do *NOT* use _sapp.dpi_scale for the window scale - const float window_scale = _sapp.x11.dpi / 96.0f; - _sapp.window_width = _sapp_roundf_gzero(x11_window_width / window_scale); - _sapp.window_height = _sapp_roundf_gzero(x11_window_height / window_scale); - // NOTE: on Vulkan, updating the framebuffer dimensions is entirely handled - // by the swapchain management code - #if !defined(SOKOL_VULKAN) - int cur_fb_width = _sapp.framebuffer_width; - int cur_fb_height = _sapp.framebuffer_height; - _sapp.framebuffer_width = _sapp_roundf_gzero(_sapp.window_width * _sapp.dpi_scale); - _sapp.framebuffer_height = _sapp_roundf_gzero(_sapp.window_height * _sapp.dpi_scale); - bool dim_changed = (_sapp.framebuffer_width != cur_fb_width) || (_sapp.framebuffer_height != cur_fb_height); - if (dim_changed) { - #if defined(SOKOL_WGPU) - _sapp_wgpu_swapchain_size_changed(); - #endif - if (!_sapp.first_frame) { - _sapp_x11_app_event(SAPP_EVENTTYPE_RESIZED); - } - } - #endif -} - -_SOKOL_PRIVATE void _sapp_x11_update_dimensions_from_window_size(void) { - XWindowAttributes attribs; - XGetWindowAttributes(_sapp.x11.display, _sapp.x11.window, &attribs); - _sapp_x11_update_dimensions(attribs.width, attribs.height); -} - -_SOKOL_PRIVATE void _sapp_x11_set_fullscreen(bool enable) { - /* NOTE: this function must be called after XMapWindow (which happens in _sapp_x11_show_window()) */ - if (_sapp.x11.NET_WM_STATE && _sapp.x11.NET_WM_STATE_FULLSCREEN) { - if (enable) { - const int _NET_WM_STATE_ADD = 1; - _sapp_x11_send_event(_sapp.x11.NET_WM_STATE, - _NET_WM_STATE_ADD, - _sapp.x11.NET_WM_STATE_FULLSCREEN, - 0, 1, 0); - } else { - const int _NET_WM_STATE_REMOVE = 0; - _sapp_x11_send_event(_sapp.x11.NET_WM_STATE, - _NET_WM_STATE_REMOVE, - _sapp.x11.NET_WM_STATE_FULLSCREEN, - 0, 1, 0); - } - } - XFlush(_sapp.x11.display); -} - -_SOKOL_PRIVATE void _sapp_x11_create_hidden_cursor(void) { - SOKOL_ASSERT(0 == _sapp.x11.hidden_cursor); - const int w = 16; - const int h = 16; - XcursorImage* img = XcursorImageCreate(w, h); - SOKOL_ASSERT(img && (img->width == 16) && (img->height == 16) && img->pixels); - img->xhot = 0; - img->yhot = 0; - const size_t num_bytes = (size_t)(w * h) * sizeof(XcursorPixel); - _sapp_clear(img->pixels, num_bytes); - _sapp.x11.hidden_cursor = XcursorImageLoadCursor(_sapp.x11.display, img); - XcursorImageDestroy(img); -} - - _SOKOL_PRIVATE void _sapp_x11_create_standard_cursor(sapp_mouse_cursor cursor, const char* name, const char* theme, int size, uint32_t fallback_native) { - SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); - SOKOL_ASSERT(_sapp.x11.display); - if (theme) { - XcursorImage* img = XcursorLibraryLoadImage(name, theme, size); - if (img) { - _sapp.x11.standard_cursors[cursor] = XcursorImageLoadCursor(_sapp.x11.display, img); - XcursorImageDestroy(img); - } - } - if (0 == _sapp.x11.standard_cursors[cursor]) { - _sapp.x11.standard_cursors[cursor] = XCreateFontCursor(_sapp.x11.display, fallback_native); - } -} - -_SOKOL_PRIVATE void _sapp_x11_create_standard_cursors(void) { - SOKOL_ASSERT(_sapp.x11.display); - const char* cursor_theme = XcursorGetTheme(_sapp.x11.display); - const int size = XcursorGetDefaultSize(_sapp.x11.display); - _sapp_x11_create_standard_cursor(SAPP_MOUSECURSOR_ARROW, "default", cursor_theme, size, XC_left_ptr); - _sapp_x11_create_standard_cursor(SAPP_MOUSECURSOR_IBEAM, "text", cursor_theme, size, XC_xterm); - _sapp_x11_create_standard_cursor(SAPP_MOUSECURSOR_CROSSHAIR, "crosshair", cursor_theme, size, XC_crosshair); - _sapp_x11_create_standard_cursor(SAPP_MOUSECURSOR_POINTING_HAND, "pointer", cursor_theme, size, XC_hand2); - _sapp_x11_create_standard_cursor(SAPP_MOUSECURSOR_RESIZE_EW, "ew-resize", cursor_theme, size, XC_sb_h_double_arrow); - _sapp_x11_create_standard_cursor(SAPP_MOUSECURSOR_RESIZE_NS, "ns-resize", cursor_theme, size, XC_sb_v_double_arrow); - _sapp_x11_create_standard_cursor(SAPP_MOUSECURSOR_RESIZE_NWSE, "nwse-resize", cursor_theme, size, 0); - _sapp_x11_create_standard_cursor(SAPP_MOUSECURSOR_RESIZE_NESW, "nesw-resize", cursor_theme, size, 0); - _sapp_x11_create_standard_cursor(SAPP_MOUSECURSOR_RESIZE_ALL, "all-scroll", cursor_theme, size, XC_fleur); - _sapp_x11_create_standard_cursor(SAPP_MOUSECURSOR_NOT_ALLOWED, "not-allowed", cursor_theme, size, 0); - _sapp_x11_create_hidden_cursor(); -} - -_SOKOL_PRIVATE void _sapp_x11_destroy_standard_cursors(void) { - SOKOL_ASSERT(_sapp.x11.display); - if (_sapp.x11.hidden_cursor) { - XFreeCursor(_sapp.x11.display, _sapp.x11.hidden_cursor); - _sapp.x11.hidden_cursor = 0; - } - for (int i = 0; i < _SAPP_MOUSECURSOR_NUM; i++) { - if (_sapp.x11.standard_cursors[i]) { - XFreeCursor(_sapp.x11.display, _sapp.x11.standard_cursors[i]); - _sapp.x11.standard_cursors[i] = 0; - } - } -} - -_SOKOL_PRIVATE bool _sapp_x11_make_custom_mouse_cursor(sapp_mouse_cursor cursor, const sapp_image_desc* desc) { - SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); - SOKOL_ASSERT(0 == _sapp.x11.custom_cursors[cursor]); - XcursorImage* img = XcursorImageCreate(desc->width, desc->height); - SOKOL_ASSERT(img && ((int) img->width == desc->width) && ((int) img->height == desc->height) && img->pixels); - img->xhot = (XcursorDim) desc->cursor_hotspot_x; - img->yhot = (XcursorDim) desc->cursor_hotspot_y; - const size_t dest_num_bytes = (size_t)(img->width * img->height) * sizeof(XcursorPixel); - SOKOL_ASSERT(dest_num_bytes == desc->pixels.size); - // Copy RGBA -> BGRA - for (size_t i = 0; i < dest_num_bytes; i += 4) { - ((uint8_t*) img->pixels)[i+0] = ((uint8_t*) desc->pixels.ptr)[i+2]; - ((uint8_t*) img->pixels)[i+1] = ((uint8_t*) desc->pixels.ptr)[i+1]; - ((uint8_t*) img->pixels)[i+2] = ((uint8_t*) desc->pixels.ptr)[i+0]; - ((uint8_t*) img->pixels)[i+3] = ((uint8_t*) desc->pixels.ptr)[i+3]; - } - _sapp.x11.custom_cursors[cursor] = XcursorImageLoadCursor(_sapp.x11.display, img); - XcursorImageDestroy(img); - return 0 != _sapp.x11.custom_cursors[cursor]; -} - -_SOKOL_PRIVATE void _sapp_x11_destroy_custom_mouse_cursor(sapp_mouse_cursor cursor) { - SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); - Cursor xcursor = _sapp.x11.custom_cursors[cursor]; - _sapp.x11.custom_cursors[cursor] = 0; - SOKOL_ASSERT(xcursor); - XFreeCursor(_sapp.x11.display, xcursor); -} - -_SOKOL_PRIVATE void _sapp_x11_toggle_fullscreen(void) { - _sapp.fullscreen = !_sapp.fullscreen; - _sapp_x11_set_fullscreen(_sapp.fullscreen); - _sapp_x11_update_dimensions_from_window_size(); -} - -_SOKOL_PRIVATE void _sapp_x11_update_cursor(sapp_mouse_cursor cursor, bool shown) { - SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); - if (shown) { - if (_sapp.custom_cursor_bound[cursor]) { - Cursor xcursor = _sapp.x11.custom_cursors[cursor]; - SOKOL_ASSERT(0 != xcursor); - XDefineCursor(_sapp.x11.display, _sapp.x11.window, xcursor); - } else if (_sapp.x11.standard_cursors[cursor]) { - XDefineCursor(_sapp.x11.display, _sapp.x11.window, _sapp.x11.standard_cursors[cursor]); - } else { - XUndefineCursor(_sapp.x11.display, _sapp.x11.window); - } - } else { - XDefineCursor(_sapp.x11.display, _sapp.x11.window, _sapp.x11.hidden_cursor); - } - XFlush(_sapp.x11.display); -} - -_SOKOL_PRIVATE void _sapp_x11_lock_mouse(bool lock) { - if (lock == _sapp.mouse.locked) { - return; - } - _sapp.mouse.dx = 0.0f; - _sapp.mouse.dy = 0.0f; - _sapp.mouse.locked = lock; - if (_sapp.mouse.locked) { - if (_sapp.x11.xi.available) { - XIEventMask em; - unsigned char mask[XIMaskLen(XI_RawMotion)] = { 0 }; // XIMaskLen is a macro - em.deviceid = XIAllMasterDevices; - em.mask_len = sizeof(mask); - em.mask = mask; - XISetMask(mask, XI_RawMotion); - XISelectEvents(_sapp.x11.display, _sapp.x11.root, &em, 1); - } - XGrabPointer(_sapp.x11.display, // display - _sapp.x11.window, // grab_window - True, // owner_events - ButtonPressMask | ButtonReleaseMask | PointerMotionMask, // event_mask - GrabModeAsync, // pointer_mode - GrabModeAsync, // keyboard_mode - _sapp.x11.window, // confine_to - _sapp.x11.hidden_cursor, // cursor - CurrentTime); // time - } else { - if (_sapp.x11.xi.available) { - XIEventMask em; - unsigned char mask[] = { 0 }; - em.deviceid = XIAllMasterDevices; - em.mask_len = sizeof(mask); - em.mask = mask; - XISelectEvents(_sapp.x11.display, _sapp.x11.root, &em, 1); - } - XWarpPointer(_sapp.x11.display, None, _sapp.x11.window, 0, 0, 0, 0, (int) _sapp.mouse.x, _sapp.mouse.y); - XUngrabPointer(_sapp.x11.display, CurrentTime); - } - XFlush(_sapp.x11.display); -} - -_SOKOL_PRIVATE void _sapp_x11_set_clipboard_string(const char* str) { - SOKOL_ASSERT(_sapp.clipboard.enabled && _sapp.clipboard.buffer); - if (strlen(str) >= (size_t)_sapp.clipboard.buf_size) { - _SAPP_ERROR(CLIPBOARD_STRING_TOO_BIG); - } - XSetSelectionOwner(_sapp.x11.display, _sapp.x11.CLIPBOARD, _sapp.x11.window, CurrentTime); - if (XGetSelectionOwner(_sapp.x11.display, _sapp.x11.CLIPBOARD) != _sapp.x11.window) { - _SAPP_ERROR(LINUX_X11_FAILED_TO_BECOME_OWNER_OF_CLIPBOARD); - } -} - -_SOKOL_PRIVATE const char* _sapp_x11_get_clipboard_string(void) { - SOKOL_ASSERT(_sapp.clipboard.enabled && _sapp.clipboard.buffer); - Atom none = XInternAtom(_sapp.x11.display, "SAPP_SELECTION", False); - Atom incremental = XInternAtom(_sapp.x11.display, "INCR", False); - if (XGetSelectionOwner(_sapp.x11.display, _sapp.x11.CLIPBOARD) == _sapp.x11.window) { - // Instead of doing a large number of X round-trips just to put this - // string into a window property and then read it back, just return it - return _sapp.clipboard.buffer; - } - XConvertSelection(_sapp.x11.display, - _sapp.x11.CLIPBOARD, - _sapp.x11.UTF8_STRING, - none, - _sapp.x11.window, - CurrentTime); - XEvent event; - if (!_sapp_x11_wait_for_event(SelectionNotify, 0.1, &event)) { - return NULL; - } - if (event.xselection.property == None) { - return NULL; - } - char* data = NULL; - Atom actualType; - int actualFormat; - unsigned long itemCount, bytesAfter; - const bool ret = XGetWindowProperty(_sapp.x11.display, - event.xselection.requestor, - event.xselection.property, - 0, - LONG_MAX, - True, - _sapp.x11.UTF8_STRING, - &actualType, - &actualFormat, - &itemCount, - &bytesAfter, - (unsigned char**) &data); - if (ret != Success || data == NULL) { - if (data != NULL) { - XFree(data); - } - return NULL; - } - if ((actualType == incremental) || (itemCount >= (size_t)_sapp.clipboard.buf_size)) { - _SAPP_ERROR(CLIPBOARD_STRING_TOO_BIG); - XFree(data); - return NULL; - } - _sapp_strcpy(data, _sapp.clipboard.buffer, (size_t)_sapp.clipboard.buf_size); - XFree(data); - return _sapp.clipboard.buffer; -} - -_SOKOL_PRIVATE void _sapp_x11_update_window_title(void) { - Xutf8SetWMProperties(_sapp.x11.display, - _sapp.x11.window, - _sapp.window_title, _sapp.window_title, - NULL, 0, NULL, NULL, NULL); - XChangeProperty(_sapp.x11.display, _sapp.x11.window, - _sapp.x11.NET_WM_NAME, _sapp.x11.UTF8_STRING, 8, - PropModeReplace, - (unsigned char*)_sapp.window_title, - strlen(_sapp.window_title)); - XChangeProperty(_sapp.x11.display, _sapp.x11.window, - _sapp.x11.NET_WM_ICON_NAME, _sapp.x11.UTF8_STRING, 8, - PropModeReplace, - (unsigned char*)_sapp.window_title, - strlen(_sapp.window_title)); - XFlush(_sapp.x11.display); -} - -_SOKOL_PRIVATE void _sapp_x11_set_icon(const sapp_icon_desc* icon_desc, int num_images) { - SOKOL_ASSERT((num_images > 0) && (num_images <= SAPP_MAX_ICONIMAGES)); - int long_count = 0; - for (int i = 0; i < num_images; i++) { - const sapp_image_desc* img_desc = &icon_desc->images[i]; - long_count += 2 + (img_desc->width * img_desc->height); - } - long* icon_data = (long*) _sapp_malloc_clear((size_t)long_count * sizeof(long)); - SOKOL_ASSERT(icon_data); - long* dst = icon_data; - for (int img_index = 0; img_index < num_images; img_index++) { - const sapp_image_desc* img_desc = &icon_desc->images[img_index]; - const uint8_t* src = (const uint8_t*) img_desc->pixels.ptr; - *dst++ = img_desc->width; - *dst++ = img_desc->height; - const int num_pixels = img_desc->width * img_desc->height; - for (int pixel_index = 0; pixel_index < num_pixels; pixel_index++) { - *dst++ = ((long)(src[pixel_index * 4 + 0]) << 16) | - ((long)(src[pixel_index * 4 + 1]) << 8) | - ((long)(src[pixel_index * 4 + 2]) << 0) | - ((long)(src[pixel_index * 4 + 3]) << 24); - } - } - XChangeProperty(_sapp.x11.display, _sapp.x11.window, - _sapp.x11.NET_WM_ICON, - XA_CARDINAL, 32, - PropModeReplace, - (unsigned char*)icon_data, - long_count); - _sapp_free(icon_data); - XFlush(_sapp.x11.display); -} - -_SOKOL_PRIVATE void _sapp_x11_create_window(Visual* visual_or_null, int depth) { - Visual* visual = visual_or_null; - if (0 == visual_or_null) { - visual = DefaultVisual(_sapp.x11.display, _sapp.x11.screen); - depth = DefaultDepth(_sapp.x11.display, _sapp.x11.screen); - } - _sapp.x11.colormap = XCreateColormap(_sapp.x11.display, _sapp.x11.root, visual, AllocNone); - _SAPP_STRUCT(XSetWindowAttributes, wa); - const uint32_t wamask = CWBorderPixel | CWColormap | CWEventMask; - wa.colormap = _sapp.x11.colormap; - wa.border_pixel = 0; - wa.event_mask = StructureNotifyMask | KeyPressMask | KeyReleaseMask | - PointerMotionMask | ButtonPressMask | ButtonReleaseMask | - ExposureMask | FocusChangeMask | VisibilityChangeMask | - EnterWindowMask | LeaveWindowMask | PropertyChangeMask; - - int display_width = DisplayWidth(_sapp.x11.display, _sapp.x11.screen); - int display_height = DisplayHeight(_sapp.x11.display, _sapp.x11.screen); - // NOTE: do *NOT* use _sapp.dpi_scale for the size multiplicator! - const float window_scale = _sapp.x11.dpi / 96.0f; - int x11_window_width = _sapp_roundf_gzero(_sapp.window_width * window_scale); - int x11_window_height = _sapp_roundf_gzero(_sapp.window_height * window_scale); - if (0 == _sapp.window_width) { - x11_window_width = (display_width * 4) / 5; - } - if (0 == _sapp.window_height) { - x11_window_height = (display_height * 4) / 5; - } - _sapp_x11_grab_error_handler(); - _sapp.x11.window = XCreateWindow(_sapp.x11.display, - _sapp.x11.root, - 0, 0, - (uint32_t)x11_window_width, - (uint32_t)x11_window_height, - 0, /* border width */ - depth, /* color depth */ - InputOutput, - visual, - wamask, - &wa); - _sapp_x11_release_error_handler(); - if (!_sapp.x11.window) { - _SAPP_PANIC(LINUX_X11_CREATE_WINDOW_FAILED); - } - Atom protocols[] = { - _sapp.x11.WM_DELETE_WINDOW - }; - XSetWMProtocols(_sapp.x11.display, _sapp.x11.window, protocols, 1); - - // NOTE: PPosition and PSize are obsolete and ignored - XSizeHints* hints = XAllocSizeHints(); - hints->flags = PWinGravity; - hints->win_gravity = CenterGravity; - XSetWMNormalHints(_sapp.x11.display, _sapp.x11.window, hints); - XFree(hints); - - // announce support for drag'n'drop - if (_sapp.drop.enabled) { - const Atom version = _SAPP_X11_XDND_VERSION; - XChangeProperty(_sapp.x11.display, _sapp.x11.window, _sapp.x11.xdnd.XdndAware, XA_ATOM, 32, PropModeReplace, (unsigned char*) &version, 1); - } - _sapp_x11_update_window_title(); - _sapp_x11_update_dimensions_from_window_size(); -} - -_SOKOL_PRIVATE void _sapp_x11_destroy_window(void) { - if (_sapp.x11.window) { - XUnmapWindow(_sapp.x11.display, _sapp.x11.window); - XDestroyWindow(_sapp.x11.display, _sapp.x11.window); - _sapp.x11.window = 0; - } - if (_sapp.x11.colormap) { - XFreeColormap(_sapp.x11.display, _sapp.x11.colormap); - _sapp.x11.colormap = 0; - } - XFlush(_sapp.x11.display); -} - -_SOKOL_PRIVATE bool _sapp_x11_window_visible(void) { - XWindowAttributes wa; - XGetWindowAttributes(_sapp.x11.display, _sapp.x11.window, &wa); - return wa.map_state == IsViewable; -} - -_SOKOL_PRIVATE void _sapp_x11_show_window(void) { - if (!_sapp_x11_window_visible()) { - XMapWindow(_sapp.x11.display, _sapp.x11.window); - XEvent dummy; - _sapp_x11_wait_for_event(VisibilityNotify, 0.1, &dummy); - XRaiseWindow(_sapp.x11.display, _sapp.x11.window); - XFlush(_sapp.x11.display); - } -} - -_SOKOL_PRIVATE void _sapp_x11_hide_window(void) { - XUnmapWindow(_sapp.x11.display, _sapp.x11.window); - XFlush(_sapp.x11.display); -} - -_SOKOL_PRIVATE unsigned long _sapp_x11_get_window_property(Window window, Atom property, Atom type, unsigned char** value) { - Atom actualType; - int actualFormat; - unsigned long itemCount, bytesAfter; - XGetWindowProperty(_sapp.x11.display, - window, - property, - 0, - LONG_MAX, - False, - type, - &actualType, - &actualFormat, - &itemCount, - &bytesAfter, - value); - return itemCount; -} - -_SOKOL_PRIVATE int _sapp_x11_get_window_state(void) { - int result = WithdrawnState; - struct { - CARD32 state; - Window icon; - } *state = NULL; - - if (_sapp_x11_get_window_property(_sapp.x11.window, _sapp.x11.WM_STATE, _sapp.x11.WM_STATE, (unsigned char**)&state) >= 2) { - result = (int)state->state; - } - if (state) { - XFree(state); - } - return result; -} - -_SOKOL_PRIVATE uint32_t _sapp_x11_key_modifier_bit(sapp_keycode key) { - switch (key) { - case SAPP_KEYCODE_LEFT_SHIFT: - case SAPP_KEYCODE_RIGHT_SHIFT: - return SAPP_MODIFIER_SHIFT; - case SAPP_KEYCODE_LEFT_CONTROL: - case SAPP_KEYCODE_RIGHT_CONTROL: - return SAPP_MODIFIER_CTRL; - case SAPP_KEYCODE_LEFT_ALT: - case SAPP_KEYCODE_RIGHT_ALT: - return SAPP_MODIFIER_ALT; - case SAPP_KEYCODE_LEFT_SUPER: - case SAPP_KEYCODE_RIGHT_SUPER: - return SAPP_MODIFIER_SUPER; - default: - return 0; - } -} - -_SOKOL_PRIVATE uint32_t _sapp_x11_button_modifier_bit(sapp_mousebutton btn) { - switch (btn) { - case SAPP_MOUSEBUTTON_LEFT: return SAPP_MODIFIER_LMB; - case SAPP_MOUSEBUTTON_RIGHT: return SAPP_MODIFIER_RMB; - case SAPP_MOUSEBUTTON_MIDDLE: return SAPP_MODIFIER_MMB; - default: return 0; - } -} - -_SOKOL_PRIVATE uint32_t _sapp_x11_mods(uint32_t x11_mods) { - uint32_t mods = 0; - if (x11_mods & ShiftMask) { - mods |= SAPP_MODIFIER_SHIFT; - } - if (x11_mods & ControlMask) { - mods |= SAPP_MODIFIER_CTRL; - } - if (x11_mods & Mod1Mask) { - mods |= SAPP_MODIFIER_ALT; - } - if (x11_mods & Mod4Mask) { - mods |= SAPP_MODIFIER_SUPER; - } - if (x11_mods & Button1Mask) { - mods |= SAPP_MODIFIER_LMB; - } - if (x11_mods & Button2Mask) { - mods |= SAPP_MODIFIER_MMB; - } - if (x11_mods & Button3Mask) { - mods |= SAPP_MODIFIER_RMB; - } - return mods; -} - -_SOKOL_PRIVATE sapp_mousebutton _sapp_x11_translate_button(const XEvent* event) { - switch (event->xbutton.button) { - case Button1: return SAPP_MOUSEBUTTON_LEFT; - case Button2: return SAPP_MOUSEBUTTON_MIDDLE; - case Button3: return SAPP_MOUSEBUTTON_RIGHT; - default: return SAPP_MOUSEBUTTON_INVALID; - } -} - -_SOKOL_PRIVATE void _sapp_x11_mouse_update(int x, int y, bool clear_dxdy) { - if (!_sapp.mouse.locked) { - const float new_x = (float)x; - const float new_y = (float)y; - if (clear_dxdy) { - _sapp.mouse.dx = 0.0f; - _sapp.mouse.dy = 0.0f; - } else if (_sapp.mouse.pos_valid) { - _sapp.mouse.dx = new_x - _sapp.mouse.x; - _sapp.mouse.dy = new_y - _sapp.mouse.y; - } - _sapp.mouse.x = new_x; - _sapp.mouse.y = new_y; - _sapp.mouse.pos_valid = true; - } -} - -_SOKOL_PRIVATE void _sapp_x11_mouse_event(sapp_event_type type, sapp_mousebutton btn, uint32_t mods) { - if (_sapp_events_enabled()) { - _sapp_init_event(type); - _sapp.event.mouse_button = btn; - _sapp.event.modifiers = mods; - _sapp_call_event(&_sapp.event); - } -} - -_SOKOL_PRIVATE void _sapp_x11_scroll_event(float x, float y, uint32_t mods) { - if (_sapp_events_enabled()) { - _sapp_init_event(SAPP_EVENTTYPE_MOUSE_SCROLL); - _sapp.event.modifiers = mods; - _sapp.event.scroll_x = x; - _sapp.event.scroll_y = y; - _sapp_call_event(&_sapp.event); - } -} - -_SOKOL_PRIVATE void _sapp_x11_key_event(sapp_event_type type, sapp_keycode key, bool repeat, uint32_t mods) { - if (_sapp_events_enabled()) { - _sapp_init_event(type); - _sapp.event.key_code = key; - _sapp.event.key_repeat = repeat; - _sapp.event.modifiers = mods; - _sapp_call_event(&_sapp.event); - /* check if a CLIPBOARD_PASTED event must be sent too */ - if (_sapp.clipboard.enabled && - (type == SAPP_EVENTTYPE_KEY_DOWN) && - (_sapp.event.modifiers == SAPP_MODIFIER_CTRL) && - (_sapp.event.key_code == SAPP_KEYCODE_V)) - { - _sapp_init_event(SAPP_EVENTTYPE_CLIPBOARD_PASTED); - _sapp_call_event(&_sapp.event); - } - } -} - -_SOKOL_PRIVATE void _sapp_x11_char_event(uint32_t chr, bool repeat, uint32_t mods) { - if (_sapp_events_enabled()) { - _sapp_init_event(SAPP_EVENTTYPE_CHAR); - _sapp.event.char_code = chr; - _sapp.event.key_repeat = repeat; - _sapp.event.modifiers = mods; - _sapp_call_event(&_sapp.event); - } -} - -_SOKOL_PRIVATE sapp_keycode _sapp_x11_translate_key(int scancode) { - if ((scancode >= 0) && (scancode < _SAPP_X11_MAX_X11_KEYCODES)) { - return _sapp.keycodes[scancode]; - } else { - return SAPP_KEYCODE_INVALID; - } -} - -_SOKOL_PRIVATE int32_t _sapp_x11_keysym_to_unicode(KeySym keysym) { - int min = 0; - int max = sizeof(_sapp_x11_keysymtab) / sizeof(struct _sapp_x11_codepair) - 1; - int mid; - - /* First check for Latin-1 characters (1:1 mapping) */ - if ((keysym >= 0x0020 && keysym <= 0x007e) || - (keysym >= 0x00a0 && keysym <= 0x00ff)) - { - return keysym; - } - - /* Also check for directly encoded 24-bit UCS characters */ - if ((keysym & 0xff000000) == 0x01000000) { - return keysym & 0x00ffffff; - } - - /* Binary search in table */ - while (max >= min) { - mid = (min + max) / 2; - if (_sapp_x11_keysymtab[mid].keysym < keysym) { - min = mid + 1; - } else if (_sapp_x11_keysymtab[mid].keysym > keysym) { - max = mid - 1; - } else { - return _sapp_x11_keysymtab[mid].ucs; - } - } - - /* No matching Unicode value found */ - return -1; -} - -_SOKOL_PRIVATE bool _sapp_x11_keypress_repeat(int keycode) { - bool repeat = false; - if ((keycode >= 0) && (keycode < _SAPP_X11_MAX_X11_KEYCODES)) { - repeat = _sapp.x11.key_repeat[keycode]; - _sapp.x11.key_repeat[keycode] = true; - } - return repeat; -} - -_SOKOL_PRIVATE void _sapp_x11_keyrelease_repeat(int keycode) { - if ((keycode >= 0) && (keycode < _SAPP_X11_MAX_X11_KEYCODES)) { - _sapp.x11.key_repeat[keycode] = false; - } -} - -_SOKOL_PRIVATE bool _sapp_x11_parse_dropped_files_list(const char* src) { - SOKOL_ASSERT(src); - SOKOL_ASSERT(_sapp.drop.buffer); - - _sapp_clear_drop_buffer(); - _sapp.drop.num_files = 0; - - /* - src is (potentially percent-encoded) string made of one or multiple paths - separated by \r\n, each path starting with 'file://' - */ - bool err = false; - int src_count = 0; - char src_chr = 0; - char* dst_ptr = _sapp.drop.buffer; - const char* dst_end_ptr = dst_ptr + (_sapp.drop.max_path_length - 1); // room for terminating 0 - while (0 != (src_chr = *src++)) { - src_count++; - char dst_chr = 0; - /* check leading 'file://' */ - if (src_count <= 7) { - if (((src_count == 1) && (src_chr != 'f')) || - ((src_count == 2) && (src_chr != 'i')) || - ((src_count == 3) && (src_chr != 'l')) || - ((src_count == 4) && (src_chr != 'e')) || - ((src_count == 5) && (src_chr != ':')) || - ((src_count == 6) && (src_chr != '/')) || - ((src_count == 7) && (src_chr != '/'))) - { - _SAPP_ERROR(LINUX_X11_DROPPED_FILE_URI_WRONG_SCHEME); - err = true; - break; - } - } else if (src_chr == '\r') { - // skip - } else if (src_chr == '\n') { - src_count = 0; - _sapp.drop.num_files++; - // too many files is not an error - if (_sapp.drop.num_files >= _sapp.drop.max_files) { - break; - } - dst_ptr = _sapp.drop.buffer + _sapp.drop.num_files * _sapp.drop.max_path_length; - dst_end_ptr = dst_ptr + (_sapp.drop.max_path_length - 1); - } else if ((src_chr == '%') && src[0] && src[1]) { - // a percent-encoded byte (most likely UTF-8 multibyte sequence) - const char digits[3] = { src[0], src[1], 0 }; - src += 2; - dst_chr = (char) strtol(digits, 0, 16); - } else { - dst_chr = src_chr; - } - if (dst_chr) { - // dst_end_ptr already has adjustment for terminating zero - if (dst_ptr < dst_end_ptr) { - *dst_ptr++ = dst_chr; - } else { - _SAPP_ERROR(DROPPED_FILE_PATH_TOO_LONG); - err = true; - break; - } - } - } - if (err) { - _sapp_clear_drop_buffer(); - _sapp.drop.num_files = 0; - return false; - } else { - return true; - } -} - -_SOKOL_PRIVATE void _sapp_x11_on_genericevent(XEvent* event) { - if (_sapp.mouse.locked && _sapp.x11.xi.available) { - if (event->xcookie.extension == _sapp.x11.xi.major_opcode) { - if (XGetEventData(_sapp.x11.display, &event->xcookie)) { - if (event->xcookie.evtype == XI_RawMotion) { - XIRawEvent* re = (XIRawEvent*) event->xcookie.data; - if (re->valuators.mask_len) { - const double* values = re->raw_values; - if (XIMaskIsSet(re->valuators.mask, 0)) { - _sapp.mouse.dx = (float) *values; - values++; - } - if (XIMaskIsSet(re->valuators.mask, 1)) { - _sapp.mouse.dy = (float) *values; - } - _sapp_x11_mouse_event(SAPP_EVENTTYPE_MOUSE_MOVE, SAPP_MOUSEBUTTON_INVALID, _sapp_x11_mods(event->xmotion.state)); - } - } - XFreeEventData(_sapp.x11.display, &event->xcookie); - } - } - } -} - -_SOKOL_PRIVATE void _sapp_x11_on_focusin(XEvent* event) { - // NOTE: ignoring NotifyGrab and NotifyUngrab is same behaviour as GLFW - if ((event->xfocus.mode != NotifyGrab) && (event->xfocus.mode != NotifyUngrab)) { - _sapp_x11_app_event(SAPP_EVENTTYPE_FOCUSED); - } -} - -_SOKOL_PRIVATE void _sapp_x11_on_focusout(XEvent* event) { - // if focus is lost for any reason, and we're in mouse locked mode, disable mouse lock - if (_sapp.mouse.locked) { - _sapp_x11_lock_mouse(false); - } - // NOTE: ignoring NotifyGrab and NotifyUngrab is same behaviour as GLFW - if ((event->xfocus.mode != NotifyGrab) && (event->xfocus.mode != NotifyUngrab)) { - _sapp_x11_app_event(SAPP_EVENTTYPE_UNFOCUSED); - } -} - -_SOKOL_PRIVATE void _sapp_x11_on_keypress(XEvent* event) { - int keycode = (int)event->xkey.keycode; - - const sapp_keycode key = _sapp_x11_translate_key(keycode); - const bool repeat = _sapp_x11_keypress_repeat(keycode); - uint32_t mods = _sapp_x11_mods(event->xkey.state); - // X11 doesn't set modifier bit on key down, so emulate that - mods |= _sapp_x11_key_modifier_bit(key); - if (key != SAPP_KEYCODE_INVALID) { - _sapp_x11_key_event(SAPP_EVENTTYPE_KEY_DOWN, key, repeat, mods); - } - KeySym keysym; - XLookupString(&event->xkey, NULL, 0, &keysym, NULL); - int32_t chr = _sapp_x11_keysym_to_unicode(keysym); - if (chr > 0) { - _sapp_x11_char_event((uint32_t)chr, repeat, mods); - } -} - -_SOKOL_PRIVATE void _sapp_x11_on_keyrelease(XEvent* event) { - int keycode = (int)event->xkey.keycode; - const sapp_keycode key = _sapp_x11_translate_key(keycode); - _sapp_x11_keyrelease_repeat(keycode); - if (key != SAPP_KEYCODE_INVALID) { - uint32_t mods = _sapp_x11_mods(event->xkey.state); - // X11 doesn't clear modifier bit on key up, so emulate that - mods &= ~_sapp_x11_key_modifier_bit(key); - _sapp_x11_key_event(SAPP_EVENTTYPE_KEY_UP, key, false, mods); - } -} - -_SOKOL_PRIVATE void _sapp_x11_on_buttonpress(XEvent* event) { - _sapp_x11_mouse_update(event->xbutton.x, event->xbutton.y, false); - const sapp_mousebutton btn = _sapp_x11_translate_button(event); - uint32_t mods = _sapp_x11_mods(event->xbutton.state); - // X11 doesn't set modifier bit on button down, so emulate that - mods |= _sapp_x11_button_modifier_bit(btn); - if (btn != SAPP_MOUSEBUTTON_INVALID) { - _sapp_x11_mouse_event(SAPP_EVENTTYPE_MOUSE_DOWN, btn, mods); - _sapp.x11.mouse_buttons |= (1 << btn); - } else { - // might be a scroll event - switch (event->xbutton.button) { - case 4: _sapp_x11_scroll_event(0.0f, 1.0f, mods); break; - case 5: _sapp_x11_scroll_event(0.0f, -1.0f, mods); break; - case 6: _sapp_x11_scroll_event(1.0f, 0.0f, mods); break; - case 7: _sapp_x11_scroll_event(-1.0f, 0.0f, mods); break; - } - } -} - -_SOKOL_PRIVATE void _sapp_x11_on_buttonrelease(XEvent* event) { - _sapp_x11_mouse_update(event->xbutton.x, event->xbutton.y, false); - const sapp_mousebutton btn = _sapp_x11_translate_button(event); - if (btn != SAPP_MOUSEBUTTON_INVALID) { - uint32_t mods = _sapp_x11_mods(event->xbutton.state); - // X11 doesn't clear modifier bit on button up, so emulate that - mods &= ~_sapp_x11_button_modifier_bit(btn); - _sapp_x11_mouse_event(SAPP_EVENTTYPE_MOUSE_UP, btn, mods); - _sapp.x11.mouse_buttons &= ~(1 << btn); - } -} - -_SOKOL_PRIVATE void _sapp_x11_on_enternotify(XEvent* event) { - // don't send enter/leave events while mouse button held down - if (0 == _sapp.x11.mouse_buttons) { - _sapp_x11_mouse_update(event->xcrossing.x, event->xcrossing.y, true); - _sapp_x11_mouse_event(SAPP_EVENTTYPE_MOUSE_ENTER, SAPP_MOUSEBUTTON_INVALID, _sapp_x11_mods(event->xcrossing.state)); - } -} - -_SOKOL_PRIVATE void _sapp_x11_on_leavenotify(XEvent* event) { - if (0 == _sapp.x11.mouse_buttons) { - _sapp_x11_mouse_update(event->xcrossing.x, event->xcrossing.y, true); - _sapp_x11_mouse_event(SAPP_EVENTTYPE_MOUSE_LEAVE, SAPP_MOUSEBUTTON_INVALID, _sapp_x11_mods(event->xcrossing.state)); - } -} - -_SOKOL_PRIVATE void _sapp_x11_on_motionnotify(XEvent* event) { - if (!_sapp.mouse.locked) { - _sapp_x11_mouse_update(event->xmotion.x, event->xmotion.y, false); - _sapp_x11_mouse_event(SAPP_EVENTTYPE_MOUSE_MOVE, SAPP_MOUSEBUTTON_INVALID, _sapp_x11_mods(event->xmotion.state)); - } -} - -_SOKOL_PRIVATE void _sapp_x11_on_propertynotify(XEvent* event) { - if (event->xproperty.state == PropertyNewValue) { - if (event->xproperty.atom == _sapp.x11.WM_STATE) { - const int state = _sapp_x11_get_window_state(); - if (state != _sapp.x11.window_state) { - _sapp.x11.window_state = state; - if (state == IconicState) { - _sapp_x11_app_event(SAPP_EVENTTYPE_ICONIFIED); - } else if (state == NormalState) { - _sapp_x11_app_event(SAPP_EVENTTYPE_RESTORED); - } - } - } - } -} - -_SOKOL_PRIVATE void _sapp_x11_on_selectionnotify(XEvent* event) { - if (event->xselection.property == _sapp.x11.xdnd.XdndSelection) { - char* data = 0; - uint32_t result = _sapp_x11_get_window_property(event->xselection.requestor, - event->xselection.property, - event->xselection.target, - (unsigned char**) &data); - if (_sapp.drop.enabled && result) { - if (_sapp_x11_parse_dropped_files_list(data)) { - _sapp.mouse.dx = 0.0f; - _sapp.mouse.dy = 0.0f; - if (_sapp_events_enabled()) { - // FIXME: Figure out how to get modifier key state here. - // The XSelection event has no 'state' item, and - // XQueryKeymap() always returns a zeroed array. - _sapp_init_event(SAPP_EVENTTYPE_FILES_DROPPED); - _sapp_call_event(&_sapp.event); - } - } - } - if (_sapp.x11.xdnd.version >= 2) { - _SAPP_STRUCT(XEvent, reply); - reply.type = ClientMessage; - reply.xclient.window = _sapp.x11.xdnd.source; - reply.xclient.message_type = _sapp.x11.xdnd.XdndFinished; - reply.xclient.format = 32; - reply.xclient.data.l[0] = (long)_sapp.x11.window; - reply.xclient.data.l[1] = result; - reply.xclient.data.l[2] = (long)_sapp.x11.xdnd.XdndActionCopy; - XSendEvent(_sapp.x11.display, _sapp.x11.xdnd.source, False, NoEventMask, &reply); - XFlush(_sapp.x11.display); - } - if (data) { - XFree(data); - } - } -} - -_SOKOL_PRIVATE void _sapp_x11_on_clientmessage(XEvent* event) { - if (XFilterEvent(event, None)) { - return; - } - if (event->xclient.message_type == _sapp.x11.WM_PROTOCOLS) { - const Atom protocol = (Atom)event->xclient.data.l[0]; - if (protocol == _sapp.x11.WM_DELETE_WINDOW) { - _sapp.quit_requested = true; - } - } else if (event->xclient.message_type == _sapp.x11.xdnd.XdndEnter) { - const bool is_list = 0 != (event->xclient.data.l[1] & 1); - _sapp.x11.xdnd.source = (Window)event->xclient.data.l[0]; - _sapp.x11.xdnd.version = event->xclient.data.l[1] >> 24; - _sapp.x11.xdnd.format = None; - if (_sapp.x11.xdnd.version > _SAPP_X11_XDND_VERSION) { - return; - } - uint32_t count = 0; - Atom* formats = 0; - if (is_list) { - count = _sapp_x11_get_window_property(_sapp.x11.xdnd.source, _sapp.x11.xdnd.XdndTypeList, XA_ATOM, (unsigned char**)&formats); - } else { - count = 3; - formats = (Atom*) event->xclient.data.l + 2; - } - for (uint32_t i = 0; i < count; i++) { - if (formats[i] == _sapp.x11.xdnd.text_uri_list) { - _sapp.x11.xdnd.format = _sapp.x11.xdnd.text_uri_list; - break; - } - } - if (is_list && formats) { - XFree(formats); - } - } else if (event->xclient.message_type == _sapp.x11.xdnd.XdndDrop) { - if (_sapp.x11.xdnd.version > _SAPP_X11_XDND_VERSION) { - return; - } - Time time = CurrentTime; - if (_sapp.x11.xdnd.format) { - if (_sapp.x11.xdnd.version >= 1) { - time = (Time)event->xclient.data.l[2]; - } - XConvertSelection(_sapp.x11.display, - _sapp.x11.xdnd.XdndSelection, - _sapp.x11.xdnd.format, - _sapp.x11.xdnd.XdndSelection, - _sapp.x11.window, - time); - } else if (_sapp.x11.xdnd.version >= 2) { - _SAPP_STRUCT(XEvent, reply); - reply.type = ClientMessage; - reply.xclient.window = _sapp.x11.xdnd.source; - reply.xclient.message_type = _sapp.x11.xdnd.XdndFinished; - reply.xclient.format = 32; - reply.xclient.data.l[0] = (long)_sapp.x11.window; - reply.xclient.data.l[1] = 0; // drag was rejected - reply.xclient.data.l[2] = None; - XSendEvent(_sapp.x11.display, _sapp.x11.xdnd.source, False, NoEventMask, &reply); - XFlush(_sapp.x11.display); - } - } else if (event->xclient.message_type == _sapp.x11.xdnd.XdndPosition) { - // drag operation has moved over the window - // FIXME: we could track the mouse position here, but - // this isn't implemented on other platforms either so far - if (_sapp.x11.xdnd.version > _SAPP_X11_XDND_VERSION) { - return; - } - _SAPP_STRUCT(XEvent, reply); - reply.type = ClientMessage; - reply.xclient.window = _sapp.x11.xdnd.source; - reply.xclient.message_type = _sapp.x11.xdnd.XdndStatus; - reply.xclient.format = 32; - reply.xclient.data.l[0] = (long)_sapp.x11.window; - if (_sapp.x11.xdnd.format) { - /* reply that we are ready to copy the dragged data */ - reply.xclient.data.l[1] = 1; // accept with no rectangle - if (_sapp.x11.xdnd.version >= 2) { - reply.xclient.data.l[4] = (long)_sapp.x11.xdnd.XdndActionCopy; - } - } - XSendEvent(_sapp.x11.display, _sapp.x11.xdnd.source, False, NoEventMask, &reply); - XFlush(_sapp.x11.display); - } -} - -_SOKOL_PRIVATE void _sapp_x11_on_selectionrequest(XEvent* event) { - XSelectionRequestEvent* req = &event->xselectionrequest; - if (req->selection != _sapp.x11.CLIPBOARD) { - return; - } - if (!_sapp.clipboard.enabled) { - return; - } - SOKOL_ASSERT(_sapp.clipboard.buffer); - _SAPP_STRUCT(XSelectionEvent, reply); - reply.type = SelectionNotify; - reply.display = req->display; - reply.requestor = req->requestor; - reply.selection = req->selection; - reply.target = req->target; - reply.property = req->property; - reply.time = req->time; - if (req->target == _sapp.x11.UTF8_STRING) { - XChangeProperty(_sapp.x11.display, - req->requestor, - req->property, - _sapp.x11.UTF8_STRING, - 8, - PropModeReplace, - (unsigned char*) _sapp.clipboard.buffer, - strlen(_sapp.clipboard.buffer)); - } else if (req->target == _sapp.x11.TARGETS) { - XChangeProperty(_sapp.x11.display, - req->requestor, - req->property, - XA_ATOM, - 32, - PropModeReplace, - (unsigned char*) &_sapp.x11.UTF8_STRING, - 1); - } else { - reply.property = None; - } - XSendEvent(_sapp.x11.display, req->requestor, False, 0, (XEvent*) &reply); -} - -_SOKOL_PRIVATE void _sapp_x11_process_event(XEvent* event) { - switch (event->type) { - case GenericEvent: - _sapp_x11_on_genericevent(event); - break; - case FocusIn: - _sapp_x11_on_focusin(event); - break; - case FocusOut: - _sapp_x11_on_focusout(event); - break; - case KeyPress: - _sapp_x11_on_keypress(event); - break; - case KeyRelease: - _sapp_x11_on_keyrelease(event); - break; - case ButtonPress: - _sapp_x11_on_buttonpress(event); - break; - case ButtonRelease: - _sapp_x11_on_buttonrelease(event); - break; - case EnterNotify: - _sapp_x11_on_enternotify(event); - break; - case LeaveNotify: - _sapp_x11_on_leavenotify(event); - break; - case MotionNotify: - _sapp_x11_on_motionnotify(event); - break; - case PropertyNotify: - _sapp_x11_on_propertynotify(event); - break; - case SelectionNotify: - _sapp_x11_on_selectionnotify(event); - break; - case SelectionRequest: - _sapp_x11_on_selectionrequest(event); - break; - case DestroyNotify: - // not a bug - break; - case ClientMessage: - _sapp_x11_on_clientmessage(event); - break; - } -} - -#if defined(_SAPP_EGL) - -_SOKOL_PRIVATE void _sapp_egl_init(void) { - #if defined(SOKOL_GLCORE) - if (!eglBindAPI(EGL_OPENGL_API)) { - _SAPP_PANIC(LINUX_EGL_BIND_OPENGL_API_FAILED); - } - #else - if (!eglBindAPI(EGL_OPENGL_ES_API)) { - _SAPP_PANIC(LINUX_EGL_BIND_OPENGL_ES_API_FAILED); - } - #endif - - _sapp.egl.display = eglGetDisplay((EGLNativeDisplayType)_sapp.x11.display); - if (EGL_NO_DISPLAY == _sapp.egl.display) { - _SAPP_PANIC(LINUX_EGL_GET_DISPLAY_FAILED); - } - - EGLint major, minor; - if (!eglInitialize(_sapp.egl.display, &major, &minor)) { - _SAPP_PANIC(LINUX_EGL_INITIALIZE_FAILED); - } - - EGLint sample_count = _sapp.desc.sample_count > 1 ? _sapp.desc.sample_count : 0; - EGLint alpha_size = _sapp.desc.alpha ? 8 : 0; - const EGLint config_attrs[] = { - EGL_SURFACE_TYPE, EGL_WINDOW_BIT, - #if defined(SOKOL_GLCORE) - EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, - #elif defined(SOKOL_GLES3) - EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, - #endif - EGL_RED_SIZE, 8, - EGL_GREEN_SIZE, 8, - EGL_BLUE_SIZE, 8, - EGL_ALPHA_SIZE, alpha_size, - EGL_DEPTH_SIZE, 24, - EGL_STENCIL_SIZE, 8, - EGL_SAMPLE_BUFFERS, _sapp.desc.sample_count > 1 ? 1 : 0, - EGL_SAMPLES, sample_count, - EGL_NONE, - }; - - EGLConfig egl_configs[32]; - EGLint config_count; - if (!eglChooseConfig(_sapp.egl.display, config_attrs, egl_configs, 32, &config_count) || config_count == 0) { - _SAPP_PANIC(LINUX_EGL_NO_CONFIGS); - } - - EGLConfig config = egl_configs[0]; - for (int i = 0; i < config_count; ++i) { - EGLConfig c = egl_configs[i]; - EGLint r, g, b, a, d, s, n; - if (eglGetConfigAttrib(_sapp.egl.display, c, EGL_RED_SIZE, &r) && - eglGetConfigAttrib(_sapp.egl.display, c, EGL_GREEN_SIZE, &g) && - eglGetConfigAttrib(_sapp.egl.display, c, EGL_BLUE_SIZE, &b) && - eglGetConfigAttrib(_sapp.egl.display, c, EGL_ALPHA_SIZE, &a) && - eglGetConfigAttrib(_sapp.egl.display, c, EGL_DEPTH_SIZE, &d) && - eglGetConfigAttrib(_sapp.egl.display, c, EGL_STENCIL_SIZE, &s) && - eglGetConfigAttrib(_sapp.egl.display, c, EGL_SAMPLES, &n) && - (r == 8) && (g == 8) && (b == 8) && (a == alpha_size) && (d == 24) && (s == 8) && (n == sample_count)) { - config = c; - break; - } - } - - EGLint visual_id; - if (!eglGetConfigAttrib(_sapp.egl.display, config, EGL_NATIVE_VISUAL_ID, &visual_id)) { - _SAPP_PANIC(LINUX_EGL_NO_NATIVE_VISUAL); - } - - _SAPP_STRUCT(XVisualInfo, visual_info_template); - visual_info_template.visualid = (VisualID)visual_id; - - int num_visuals; - XVisualInfo* visual_info = XGetVisualInfo(_sapp.x11.display, VisualIDMask, &visual_info_template, &num_visuals); - if (!visual_info) { - _SAPP_PANIC(LINUX_EGL_GET_VISUAL_INFO_FAILED); - } - - _sapp_x11_create_window(visual_info->visual, visual_info->depth); - XFree(visual_info); - - _sapp.egl.surface = eglCreateWindowSurface(_sapp.egl.display, config, (EGLNativeWindowType)_sapp.x11.window, NULL); - if (EGL_NO_SURFACE == _sapp.egl.surface) { - _SAPP_PANIC(LINUX_EGL_CREATE_WINDOW_SURFACE_FAILED); - } - - EGLint ctx_attrs[] = { - EGL_CONTEXT_MAJOR_VERSION, _sapp.desc.gl.major_version, - EGL_CONTEXT_MINOR_VERSION, _sapp.desc.gl.minor_version, - #if defined(SOKOL_GLCORE) - EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, - #endif - EGL_NONE, - }; - - _sapp.egl.context = eglCreateContext(_sapp.egl.display, config, EGL_NO_CONTEXT, ctx_attrs); - if (EGL_NO_CONTEXT == _sapp.egl.context) { - _SAPP_PANIC(LINUX_EGL_CREATE_CONTEXT_FAILED); - } - - if (!eglMakeCurrent(_sapp.egl.display, _sapp.egl.surface, _sapp.egl.surface, _sapp.egl.context)) { - _SAPP_PANIC(LINUX_EGL_MAKE_CURRENT_FAILED); - } - glGetIntegerv(GL_FRAMEBUFFER_BINDING, (GLint*)&_sapp.gl.framebuffer); - - eglSwapInterval(_sapp.egl.display, _sapp.swap_interval); -} - -_SOKOL_PRIVATE void _sapp_egl_destroy(void) { - if (_sapp.egl.display != EGL_NO_DISPLAY) { - eglMakeCurrent(_sapp.egl.display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); - - if (_sapp.egl.context != EGL_NO_CONTEXT) { - eglDestroyContext(_sapp.egl.display, _sapp.egl.context); - _sapp.egl.context = EGL_NO_CONTEXT; - } - - if (_sapp.egl.surface != EGL_NO_SURFACE) { - eglDestroySurface(_sapp.egl.display, _sapp.egl.surface); - _sapp.egl.surface = EGL_NO_SURFACE; - } - - eglTerminate(_sapp.egl.display); - _sapp.egl.display = EGL_NO_DISPLAY; - } -} - -#endif // _SAPP_EGL - -_SOKOL_PRIVATE void _sapp_linux_frame(void) { - _sapp_x11_update_dimensions_from_window_size(); - #if defined(SOKOL_WGPU) - _sapp_wgpu_frame(); - #elif defined(SOKOL_VULKAN) - _sapp_vk_frame(); - #else - _sapp_frame(); - #if defined(_SAPP_GLX) - _sapp_glx_swap_buffers(); - #elif defined(_SAPP_EGL) - // Modified by tettou771 for TrussC: skip present support - if (_sapp.skip_present) { _sapp.skip_present = false; } - else { eglSwapBuffers(_sapp.egl.display, _sapp.egl.surface); } - #endif - #endif -} - -_SOKOL_PRIVATE void _sapp_linux_run(const sapp_desc* desc) { - /* The following lines are here to trigger a linker error instead of an - obscure runtime error if the user has forgotten to add -pthread to - the compiler or linker options. They have no other purpose. - */ - pthread_attr_t pthread_attr; - pthread_attr_init(&pthread_attr); - pthread_attr_destroy(&pthread_attr); - - _sapp_init_state(desc); - _sapp.x11.window_state = NormalState; - - XInitThreads(); - XrmInitialize(); - _sapp.x11.display = XOpenDisplay(NULL); - if (!_sapp.x11.display) { - _SAPP_PANIC(LINUX_X11_OPEN_DISPLAY_FAILED); - } - _sapp.x11.screen = DefaultScreen(_sapp.x11.display); - _sapp.x11.root = DefaultRootWindow(_sapp.x11.display); - _sapp_x11_query_system_dpi(); - // NOTE: on Linux system-window-size to frame-buffer-size mapping is always 1:1 - _sapp.dpi_scale = _sapp.x11.dpi / 96.0f; - _sapp_x11_init_extensions(); - _sapp_x11_create_standard_cursors(); - XkbSetDetectableAutoRepeat(_sapp.x11.display, true, NULL); - _sapp_x11_init_keytable(); - #if defined(_SAPP_GLX) - _sapp_glx_init(); - Visual* visual = 0; - int depth = 0; - _sapp_glx_choose_visual(&visual, &depth); - _sapp_x11_create_window(visual, depth); - _sapp_glx_create_context(); - _sapp_glx_swapinterval(_sapp.swap_interval); - #elif defined(_SAPP_EGL) - _sapp_egl_init(); - #elif defined(SOKOL_WGPU) - _sapp_x11_create_window(0, 0); - _sapp_wgpu_init(); - #elif defined(SOKOL_VULKAN) - _sapp_x11_create_window(0, 0); - _sapp_vk_init(); - #endif - sapp_set_icon(&desc->icon); - _sapp.valid = true; - _sapp_x11_show_window(); - if (_sapp.fullscreen) { - _sapp_x11_set_fullscreen(true); - } - - XFlush(_sapp.x11.display); - while (!_sapp.quit_ordered) { - _sapp_timing_update(&_sapp.timing, 0.0); - int count = XPending(_sapp.x11.display); - while (count--) { - XEvent event; - XNextEvent(_sapp.x11.display, &event); - _sapp_x11_process_event(&event); - } - _sapp_linux_frame(); - XFlush(_sapp.x11.display); - // handle quit-requested, either from window or from sapp_request_quit() - if (_sapp.quit_requested && !_sapp.quit_ordered) { - // give user code a chance to intervene - _sapp_x11_app_event(SAPP_EVENTTYPE_QUIT_REQUESTED); - /* if user code hasn't intervened, quit the app */ - if (_sapp.quit_requested) { - _sapp.quit_ordered = true; - } - } - } - _sapp_call_cleanup(); - #if defined(_SAPP_GLX) - _sapp_glx_destroy_context(); - #elif defined(_SAPP_EGL) - _sapp_egl_destroy(); - #elif defined(SOKOL_WGPU) - _sapp_wgpu_discard(); - #elif defined(SOKOL_VULKAN) - _sapp_vk_discard(); - #endif - _sapp_x11_destroy_window(); - _sapp_x11_destroy_standard_cursors(); - XCloseDisplay(_sapp.x11.display); - _sapp_discard_state(); -} - -#if !defined(SOKOL_NO_ENTRY) -int main(int argc, char* argv[]) { - sapp_desc desc = sokol_main(argc, argv); - _sapp_linux_run(&desc); - return 0; -} -#endif /* SOKOL_NO_ENTRY */ -#endif /* _SAPP_LINUX */ - -// ██████ ██ ██ ██████ ██ ██ ██████ -// ██ ██ ██ ██ ██ ██ ██ ██ ██ -// ██████ ██ ██ ██████ ██ ██ ██ -// ██ ██ ██ ██ ██ ██ ██ ██ -// ██ ██████ ██████ ███████ ██ ██████ -// -// >>public -#if defined(SOKOL_NO_ENTRY) -SOKOL_API_IMPL void sapp_run(const sapp_desc* desc) { - SOKOL_ASSERT(desc); - #if defined(_SAPP_MACOS) - _sapp_macos_run(desc); - #elif defined(_SAPP_IOS) - _sapp_ios_run(desc); - #elif defined(_SAPP_EMSCRIPTEN) - _sapp_emsc_run(desc); - #elif defined(_SAPP_WIN32) - _sapp_win32_run(desc); - #elif defined(_SAPP_LINUX) - _sapp_linux_run(desc); - #else - #error "sapp_run() not supported on this platform" - #endif -} - -/* this is just a stub so the linker doesn't complain */ -sapp_desc sokol_main(int argc, char* argv[]) { - _SOKOL_UNUSED(argc); - _SOKOL_UNUSED(argv); - _SAPP_STRUCT(sapp_desc, desc); - return desc; -} -#else -/* likewise, in normal mode, sapp_run() is just an empty stub */ -SOKOL_API_IMPL void sapp_run(const sapp_desc* desc) { - _SOKOL_UNUSED(desc); -} -#endif - -SOKOL_API_IMPL bool sapp_isvalid(void) { - return _sapp.valid; -} - -SOKOL_API_IMPL void* sapp_userdata(void) { - return _sapp.desc.user_data; -} - -SOKOL_API_IMPL sapp_desc sapp_query_desc(void) { - return _sapp.desc; -} - -SOKOL_API_IMPL uint64_t sapp_frame_count(void) { - return _sapp.frame_count; -} - -SOKOL_API_IMPL double sapp_frame_duration(void) { - #if defined(_SAPP_MACOS) && defined(SOKOL_METAL) - return _sapp_macos_mtl_timing_frame_duration(); - #elif defined(_SAPP_IOS) && defined(SOKOL_METAL) - return _sapp_ios_mtl_timing_frame_duration(); - #else - return _sapp_timing_get(&_sapp.timing); - #endif -} - -SOKOL_API_IMPL double sapp_frame_duration_unfiltered(void) { - return _sapp.timing.dt; -} - -SOKOL_API_IMPL int sapp_width(void) { - return (_sapp.framebuffer_width > 0) ? _sapp.framebuffer_width : 1; -} - -SOKOL_API_IMPL float sapp_widthf(void) { - return (float)sapp_width(); -} - -SOKOL_API_IMPL int sapp_height(void) { - return (_sapp.framebuffer_height > 0) ? _sapp.framebuffer_height : 1; -} - -SOKOL_API_IMPL float sapp_heightf(void) { - return (float)sapp_height(); -} - -SOKOL_API_IMPL sapp_pixel_format sapp_color_format(void) { - #if defined(SOKOL_WGPU) - switch (_sapp.wgpu.render_format) { - case WGPUTextureFormat_RGBA8Unorm: - return SAPP_PIXELFORMAT_RGBA8; - case WGPUTextureFormat_BGRA8Unorm: - return SAPP_PIXELFORMAT_BGRA8; - default: - SOKOL_UNREACHABLE; - return SAPP_PIXELFORMAT_NONE; - } - #elif defined(SOKOL_VULKAN) - switch (_sapp.vk.surface_format.format) { - case VK_FORMAT_R8G8B8A8_UNORM: - return SAPP_PIXELFORMAT_RGBA8; - case VK_FORMAT_B8G8R8A8_UNORM: - return SAPP_PIXELFORMAT_BGRA8; - default: - // FIXME! - SOKOL_UNREACHABLE; - return SAPP_PIXELFORMAT_NONE; - } - #elif defined(SOKOL_METAL) || defined(SOKOL_D3D11) - // Modified by tettou771 for TrussC: report 10-bit color format (RGB10A2) - return SAPP_PIXELFORMAT_RGB10A2; - #else - return SAPP_PIXELFORMAT_RGBA8; - #endif -} - -SOKOL_API_IMPL sapp_pixel_format sapp_depth_format(void) { - return SAPP_PIXELFORMAT_DEPTH_STENCIL; -} - -SOKOL_API_IMPL int sapp_sample_count(void) { - return _sapp.sample_count; -} - -SOKOL_API_IMPL bool sapp_high_dpi(void) { - return _sapp.desc.high_dpi && (_sapp.dpi_scale >= 1.5f); -} - -SOKOL_API_IMPL float sapp_dpi_scale(void) { - return _sapp.dpi_scale; -} - -SOKOL_API_IMPL const void* sapp_egl_get_display(void) { - SOKOL_ASSERT(_sapp.valid); - #if defined(_SAPP_ANDROID) - return _sapp.android.display; - #elif defined(_SAPP_LINUX) && defined(_SAPP_EGL) - return _sapp.egl.display; - #else - return 0; - #endif -} - -SOKOL_API_IMPL const void* sapp_egl_get_context(void) { - SOKOL_ASSERT(_sapp.valid); - #if defined(_SAPP_ANDROID) - return _sapp.android.context; - #elif defined(_SAPP_LINUX) && defined(_SAPP_EGL) - return _sapp.egl.context; - #else - return 0; - #endif -} - -SOKOL_API_IMPL void sapp_show_keyboard(bool show) { - #if defined(_SAPP_IOS) - _sapp_ios_show_keyboard(show); - #elif defined(_SAPP_ANDROID) - _sapp_android_show_keyboard(show); - #else - _SOKOL_UNUSED(show); - #endif -} - -SOKOL_API_IMPL bool sapp_keyboard_shown(void) { - return _sapp.onscreen_keyboard_shown; -} - -SOKOL_API_IMPL bool sapp_is_fullscreen(void) { - return _sapp.fullscreen; -} - -SOKOL_API_IMPL void sapp_toggle_fullscreen(void) { - #if defined(_SAPP_MACOS) - _sapp_macos_toggle_fullscreen(); - #elif defined(_SAPP_WIN32) - _sapp_win32_toggle_fullscreen(); - #elif defined(_SAPP_LINUX) - _sapp_x11_toggle_fullscreen(); - #elif defined(_SAPP_EMSCRIPTEN) - _sapp_emsc_toggle_fullscreen(); - #endif -} - -_SOKOL_PRIVATE void _sapp_update_cursor(sapp_mouse_cursor cursor, bool shown) { - #if defined(_SAPP_MACOS) - _sapp_macos_update_cursor(cursor, shown); - #elif defined(_SAPP_WIN32) - _sapp_win32_update_cursor(cursor, shown, false); - #elif defined(_SAPP_LINUX) - _sapp_x11_update_cursor(cursor, shown); - #elif defined(_SAPP_EMSCRIPTEN) - _sapp_emsc_update_cursor(cursor, shown); - #endif - _sapp.mouse.current_cursor = cursor; - _sapp.mouse.shown = shown; -} - -/* NOTE that sapp_show_mouse() does not "stack" like the Win32 or macOS API functions! */ -SOKOL_API_IMPL void sapp_show_mouse(bool show) { - if (_sapp.mouse.shown != show) { - _sapp_update_cursor(_sapp.mouse.current_cursor, show); - } -} - -SOKOL_API_IMPL bool sapp_mouse_shown(void) { - return _sapp.mouse.shown; -} - -SOKOL_API_IMPL void sapp_lock_mouse(bool lock) { - #if defined(_SAPP_MACOS) - _sapp_macos_lock_mouse(lock); - #elif defined(_SAPP_EMSCRIPTEN) - _sapp_emsc_lock_mouse(lock); - #elif defined(_SAPP_WIN32) - _sapp_win32_lock_mouse(lock); - #elif defined(_SAPP_LINUX) - _sapp_x11_lock_mouse(lock); - #else - _sapp.mouse.locked = lock; - #endif -} - -SOKOL_API_IMPL bool sapp_mouse_locked(void) { - return _sapp.mouse.locked; -} - -SOKOL_API_IMPL void sapp_set_mouse_cursor(sapp_mouse_cursor cursor) { - SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); - if (_sapp.mouse.current_cursor != cursor) { - _sapp_update_cursor(cursor, _sapp.mouse.shown); - } -} - -SOKOL_API_IMPL sapp_mouse_cursor sapp_get_mouse_cursor(void) { - return _sapp.mouse.current_cursor; -} - -SOKOL_API_IMPL sapp_mouse_cursor sapp_bind_mouse_cursor_image(sapp_mouse_cursor cursor, const sapp_image_desc* desc) { - SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); - // NOTE: It seems that for some reason, the hotspot doesn't work if it is one less - // than the dimension of the cursor image (or more), on windows. So for a cursor - // that is 32 by 32 px, a hotspot of x = 30 works, but not x = 31. - // The cursor simply dissapears in such cases. Asserting for all platforms to make - // the behaviour consistent. - SOKOL_ASSERT(desc->cursor_hotspot_x < desc->width - 1 && desc->cursor_hotspot_y < desc->height - 1); - SOKOL_ASSERT(desc->width * desc->height * 4 == (int) desc->pixels.size); - - sapp_unbind_mouse_cursor_image(cursor); - - bool res = false; - #if defined(_SAPP_MACOS) - res = _sapp_macos_make_custom_mouse_cursor(cursor, desc); - #elif defined(_SAPP_EMSCRIPTEN) - res = _sapp_emsc_make_custom_mouse_cursor(cursor, desc); - #elif defined(_SAPP_WIN32) - res = _sapp_win32_make_custom_mouse_cursor(cursor, desc); - #elif defined(_SAPP_LINUX) - res = _sapp_x11_make_custom_mouse_cursor(cursor, desc); - #else - _SOKOL_UNUSED(desc); - #endif - _sapp.custom_cursor_bound[(int)cursor] = res; - - // Update the displayed cursor in case the current cursor is the one we just bound. - if (_sapp.mouse.current_cursor == cursor) { - _sapp_update_cursor(cursor, _sapp.mouse.shown); - } - return cursor; // returning the passed-in cursor puerly for convenience, in case you want to asign the value to a variable. -} - -SOKOL_API_IMPL void sapp_unbind_mouse_cursor_image(sapp_mouse_cursor cursor) { - SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); - if (_sapp.custom_cursor_bound[(int)cursor]) { - // if this is the active cursor, first restore it to its default image, - // this must be done before attempting to destroy any cursor image - // resources which at least on win32 would fail if the cursor is still in use - _sapp.custom_cursor_bound[(int)cursor] = false; - if (_sapp.mouse.current_cursor == cursor) { - _sapp_update_cursor(cursor, _sapp.mouse.shown); - } - #if defined(_SAPP_MACOS) - _sapp_macos_destroy_custom_mouse_cursor(cursor); - #elif defined(_SAPP_EMSCRIPTEN) - _sapp_emsc_destroy_custom_mouse_cursor(cursor); - #elif defined(_SAPP_WIN32) - _sapp_win32_destroy_custom_mouse_cursor(cursor); - #elif defined(_SAPP_LINUX) - _sapp_x11_destroy_custom_mouse_cursor(cursor); - #endif - } -} - -SOKOL_API_IMPL void sapp_request_quit(void) { - _sapp.quit_requested = true; -} - -SOKOL_API_IMPL void sapp_cancel_quit(void) { - _sapp.quit_requested = false; -} - -SOKOL_API_IMPL void sapp_quit(void) { - _sapp.quit_ordered = true; -} - -SOKOL_API_IMPL void sapp_consume_event(void) { - _sapp.event_consumed = true; -} - -/* NOTE: on HTML5, sapp_set_clipboard_string() must be called from within event handler! */ -SOKOL_API_IMPL void sapp_set_clipboard_string(const char* str) { - if (!_sapp.clipboard.enabled) { - return; - } - SOKOL_ASSERT(str); - #if defined(_SAPP_MACOS) - _sapp_macos_set_clipboard_string(str); - #elif defined(_SAPP_EMSCRIPTEN) - _sapp_emsc_set_clipboard_string(str); - #elif defined(_SAPP_WIN32) - _sapp_win32_set_clipboard_string(str); - #elif defined(_SAPP_LINUX) - _sapp_x11_set_clipboard_string(str); - #else - /* not implemented */ - #endif - _sapp_strcpy(str, _sapp.clipboard.buffer, (size_t)_sapp.clipboard.buf_size); -} - -SOKOL_API_IMPL const char* sapp_get_clipboard_string(void) { - if (!_sapp.clipboard.enabled) { - return ""; - } - #if defined(_SAPP_MACOS) - return _sapp_macos_get_clipboard_string(); - #elif defined(_SAPP_EMSCRIPTEN) - return _sapp.clipboard.buffer; - #elif defined(_SAPP_WIN32) - return _sapp_win32_get_clipboard_string(); - #elif defined(_SAPP_LINUX) - return _sapp_x11_get_clipboard_string(); - #else - /* not implemented */ - return _sapp.clipboard.buffer; - #endif -} - -SOKOL_API_IMPL void sapp_set_window_title(const char* title) { - SOKOL_ASSERT(title); - _sapp_strcpy(title, _sapp.window_title, sizeof(_sapp.window_title)); - #if defined(_SAPP_MACOS) - _sapp_macos_update_window_title(); - #elif defined(_SAPP_WIN32) - _sapp_win32_update_window_title(); - #elif defined(_SAPP_LINUX) - _sapp_x11_update_window_title(); - #endif -} - -SOKOL_API_IMPL void sapp_set_icon(const sapp_icon_desc* desc) { - SOKOL_ASSERT(desc); - if (desc->sokol_default) { - if (0 == _sapp.default_icon_pixels) { - _sapp_setup_default_icon(); - } - SOKOL_ASSERT(0 != _sapp.default_icon_pixels); - desc = &_sapp.default_icon_desc; - } - const int num_images = _sapp_icon_num_images(desc); - if (num_images == 0) { - return; - } - SOKOL_ASSERT((num_images > 0) && (num_images <= SAPP_MAX_ICONIMAGES)); - if (!_sapp_validate_icon_desc(desc, num_images)) { - return; - } - #if defined(_SAPP_MACOS) - _sapp_macos_set_icon(desc, num_images); - #elif defined(_SAPP_WIN32) - _sapp_win32_set_icon(desc, num_images); - #elif defined(_SAPP_LINUX) - _sapp_x11_set_icon(desc, num_images); - #elif defined(_SAPP_EMSCRIPTEN) - _sapp_emsc_set_icon(desc, num_images); - #endif -} - -SOKOL_API_IMPL int sapp_get_num_dropped_files(void) { - if (!_sapp.drop.enabled) { - return 0; - } - return _sapp.drop.num_files; -} - -SOKOL_API_IMPL const char* sapp_get_dropped_file_path(int index) { - SOKOL_ASSERT((index >= 0) && (index < _sapp.drop.num_files)); - if (!_sapp.drop.enabled) { - return ""; - } - SOKOL_ASSERT(_sapp.drop.buffer); - if ((index < 0) || (index >= _sapp.drop.max_files)) { - return ""; - } - return (const char*) _sapp_dropped_file_path_ptr(index); -} - -SOKOL_API_IMPL uint32_t sapp_html5_get_dropped_file_size(int index) { - SOKOL_ASSERT((index >= 0) && (index < _sapp.drop.num_files)); - #if defined(_SAPP_EMSCRIPTEN) - if (!_sapp.drop.enabled) { - return 0; - } - return sapp_js_dropped_file_size(index); - #else - (void)index; - return 0; - #endif -} - -SOKOL_API_IMPL void sapp_html5_fetch_dropped_file(const sapp_html5_fetch_request* request) { - SOKOL_ASSERT(_sapp.drop.enabled); - SOKOL_ASSERT(request); - SOKOL_ASSERT(request->callback); - SOKOL_ASSERT(request->buffer.ptr); - SOKOL_ASSERT(request->buffer.size > 0); - #if defined(_SAPP_EMSCRIPTEN) - const int index = request->dropped_file_index; - sapp_html5_fetch_error error_code = SAPP_HTML5_FETCH_ERROR_NO_ERROR; - if ((index < 0) || (index >= _sapp.drop.num_files)) { - error_code = SAPP_HTML5_FETCH_ERROR_OTHER; - } - if (sapp_html5_get_dropped_file_size(index) > request->buffer.size) { - error_code = SAPP_HTML5_FETCH_ERROR_BUFFER_TOO_SMALL; - } - if (SAPP_HTML5_FETCH_ERROR_NO_ERROR != error_code) { - _sapp_emsc_invoke_fetch_cb(index, - false, // success - (int)error_code, - request->callback, - 0, // fetched_size - (void*)request->buffer.ptr, - request->buffer.size, - request->user_data); - } else { - sapp_js_fetch_dropped_file(index, - request->callback, - (void*)request->buffer.ptr, - request->buffer.size, - request->user_data); - } - #else - (void)request; - #endif -} - -SOKOL_API_IMPL sapp_environment sapp_get_environment(void) { - SOKOL_ASSERT(_sapp.valid); - _SAPP_STRUCT(sapp_environment, res); - res.defaults.color_format = sapp_color_format(); - res.defaults.depth_format = sapp_depth_format(); - res.defaults.sample_count = sapp_sample_count(); - #if defined(SOKOL_METAL) - #if defined(_SAPP_MACOS) - res.metal.device = (__bridge const void*) _sapp.macos.mtl.device; - #else - res.metal.device = (__bridge const void*) _sapp.ios.mtl.device; - #endif - #endif - #if defined(SOKOL_D3D11) - res.d3d11.device = (const void*) _sapp.d3d11.device; - res.d3d11.device_context = (const void*) _sapp.d3d11.device_context; - #endif - #if defined(SOKOL_WGPU) - res.wgpu.device = (const void*) _sapp.wgpu.device; - #endif - #if defined(SOKOL_VULKAN) - res.vulkan.instance = (const void*) _sapp.vk.instance; - res.vulkan.physical_device = (const void*) _sapp.vk.physical_device; - res.vulkan.device = (const void*) _sapp.vk.device; - res.vulkan.queue = (const void*) _sapp.vk.queue; - res.vulkan.queue_family_index = _sapp.vk.queue_family_index; - #endif - return res; -} - -SOKOL_API_IMPL sapp_swapchain sapp_get_swapchain(void) { - SOKOL_ASSERT(_sapp.valid); - _SAPP_STRUCT(sapp_swapchain, res); - #if defined(SOKOL_METAL) - #if defined(_SAPP_MACOS) - res.metal.current_drawable = (__bridge const void*) _sapp_macos_mtl_swapchain_next(); - res.metal.depth_stencil_texture = (__bridge const void*) _sapp.macos.mtl.depth_tex; - res.metal.msaa_color_texture = (__bridge const void*) _sapp.macos.mtl.msaa_tex; - #else - res.metal.current_drawable = (__bridge const void*) _sapp_ios_mtl_swapchain_next(); - res.metal.depth_stencil_texture = (__bridge const void*) _sapp.ios.mtl.depth_tex; - res.metal.msaa_color_texture = (__bridge const void*) _sapp.ios.mtl.msaa_tex; - #endif - #endif - #if defined(SOKOL_D3D11) - SOKOL_ASSERT(_sapp.d3d11.rtv); - if (_sapp.sample_count > 1) { - SOKOL_ASSERT(_sapp.d3d11.msaa_rtv); - res.d3d11.render_view = (const void*) _sapp.d3d11.msaa_rtv; - res.d3d11.resolve_view = (const void*) _sapp.d3d11.rtv; - } else { - res.d3d11.render_view = (const void*) _sapp.d3d11.rtv; - } - res.d3d11.depth_stencil_view = (const void*) _sapp.d3d11.dsv; - #endif - #if defined(SOKOL_WGPU) - SOKOL_ASSERT(0 == _sapp.wgpu.swapchain_view); - _sapp_wgpu_swapchain_next(); - // FIXME: swapchain_view being null must be allowed and should skip the frame - SOKOL_ASSERT(_sapp.wgpu.swapchain_view); - if (_sapp.sample_count > 1) { - SOKOL_ASSERT(_sapp.wgpu.msaa_view); - res.wgpu.render_view = (const void*) _sapp.wgpu.msaa_view; - res.wgpu.resolve_view = (const void*) _sapp.wgpu.swapchain_view; - } else { - res.wgpu.render_view = (const void*) _sapp.wgpu.swapchain_view; - } - res.wgpu.depth_stencil_view = (const void*) _sapp.wgpu.depth_stencil_view; - #endif - #if defined(SOKOL_VULKAN) - _sapp_vk_swapchain_next(); - // FIXME: swapchain_view being null must be allowed and should skip the frame - uint32_t img_idx = _sapp.vk.cur_swapchain_image_index; - if (_sapp.sample_count > 1) { - SOKOL_ASSERT(_sapp.vk.msaa.img && _sapp.vk.msaa.view); - res.vulkan.render_image = (const void*) _sapp.vk.msaa.img; - res.vulkan.render_view = (const void*) _sapp.vk.msaa.view; - res.vulkan.resolve_image = (const void*) _sapp.vk.swapchain_images[img_idx]; - res.vulkan.resolve_view = (const void*) _sapp.vk.swapchain_views[img_idx]; - } else { - res.vulkan.render_image = (const void*) _sapp.vk.swapchain_images[img_idx]; - res.vulkan.render_view = (const void*) _sapp.vk.swapchain_views[img_idx]; - } - res.vulkan.depth_stencil_image = (const void*) _sapp.vk.depth.img; - res.vulkan.depth_stencil_view = (const void*) _sapp.vk.depth.view; - // NOTE: using the current swapchain image index here is *NOT* a bug! The render_finished_semaphore *must* - // be associated with its swapchain image in case the swapchain implementation doesn't return swapchain images in order - res.vulkan.render_finished_semaphore = _sapp.vk.sync[img_idx].render_finished_sem; - res.vulkan.present_complete_semaphore = _sapp.vk.sync[_sapp.vk.sync_slot].present_complete_sem; - #endif - #if defined(_SAPP_ANY_GL) - res.gl.framebuffer = _sapp.gl.framebuffer; - #endif - res.width = sapp_width(); - res.height = sapp_height(); - res.color_format = sapp_color_format(); - res.depth_format = sapp_depth_format(); - res.sample_count = sapp_sample_count(); - return res; -} - -SOKOL_API_IMPL const void* sapp_macos_get_window(void) { - #if defined(_SAPP_MACOS) - const void* obj = (__bridge const void*) _sapp.macos.window; - SOKOL_ASSERT(obj); - return obj; - #else - return 0; - #endif -} - -SOKOL_API_IMPL const void* sapp_ios_get_window(void) { - #if defined(_SAPP_IOS) - const void* obj = (__bridge const void*) _sapp.ios.window; - SOKOL_ASSERT(obj); - return obj; - #else - return 0; - #endif -} - -// Modified by tettou771 for TrussC: runtime orientation control -SOKOL_API_IMPL void sapp_ios_set_supported_orientations(uint32_t mask) { - #if defined(_SAPP_IOS) - _sapp.ios.supported_orientations = (NSUInteger)mask; - #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 160000 - if (@available(iOS 16.0, *)) { - [_sapp.ios.view_ctrl setNeedsUpdateOfSupportedInterfaceOrientations]; - } - #endif - #else - (void)mask; - #endif -} - -SOKOL_API_IMPL const void* sapp_d3d11_get_swap_chain(void) { - SOKOL_ASSERT(_sapp.valid); -#if defined(SOKOL_D3D11) - return _sapp.d3d11.swap_chain; -#else - return 0; -#endif -} - -SOKOL_API_IMPL const void* sapp_win32_get_hwnd(void) { - SOKOL_ASSERT(_sapp.valid); - #if defined(_SAPP_WIN32) - return _sapp.win32.hwnd; - #else - return 0; - #endif -} - -SOKOL_API_IMPL int sapp_gl_get_major_version(void) { - SOKOL_ASSERT(_sapp.valid); - #if defined(_SAPP_ANY_GL) - return _sapp.desc.gl.major_version; - #else - return 0; - #endif -} - -SOKOL_API_IMPL int sapp_gl_get_minor_version(void) { - SOKOL_ASSERT(_sapp.valid); - #if defined(_SAPP_ANY_GL) - return _sapp.desc.gl.minor_version; - #else - return 0; - #endif -} - -SOKOL_API_IMPL bool sapp_gl_is_gles(void) { - #if defined(SOKOL_GLES3) - return true; - #else - return false; - #endif -} - -SOKOL_API_IMPL const void* sapp_x11_get_window(void) { - #if defined(_SAPP_LINUX) - return (void*)_sapp.x11.window; - #else - return 0; - #endif -} - -SOKOL_API_IMPL const void* sapp_x11_get_display(void) { - #if defined(_SAPP_LINUX) - return (void*)_sapp.x11.display; - #else - return 0; - #endif -} - -SOKOL_API_IMPL const void* sapp_android_get_native_activity(void) { - // NOTE: _sapp.valid is not asserted here because sapp_android_get_native_activity() - // needs to be callable from within sokol_main() (see: https://github.com/floooh/sokol/issues/708) - #if defined(_SAPP_ANDROID) - return (void*)_sapp.android.activity; - #else - return 0; - #endif -} - -SOKOL_API_IMPL void sapp_html5_ask_leave_site(bool ask) { - _sapp.html5_ask_leave_site = ask; -} - -// Modified by tettou771 for TrussC: skip the next present call (fix D3D11 flickering) -SOKOL_API_IMPL void sapp_skip_present(void) { - _sapp.skip_present = true; -} -// end of modification - -#endif /* SOKOL_APP_IMPL */ diff --git a/core/include/sokol/sokol_app_tc.h b/core/include/sokol/sokol_app_tc.h new file mode 100644 index 00000000..78f09810 --- /dev/null +++ b/core/include/sokol/sokol_app_tc.h @@ -0,0 +1,20609 @@ +#if defined(SOKOL_APP_TC_IMPL) && !defined(SOKOL_APP_TC_IMPL_INCLUDED) +#define SOKOL_APP_TC_IMPL_INCLUDED (1) +#endif +#ifndef SOKOL_APP_TC_INCLUDED +/* + sokol_app_tc.h -- TrussC's application layer: the sokol_app.h API, + implemented standalone, plus native multi-window support + + Project URL: https://github.com/TrussC-org/TrussC + Fork lineage: sokol_app.h by Andre Weissflog (https://github.com/floooh/sokol) + License: zlib/libpng (same as sokol_app.h, see end of file) + + Do this: + #define SOKOL_APP_TC_IMPL + before you include this file in *one* C++/Objective-C++ source file to + create the implementation. The header is SELF-CONTAINED: it carries the + complete sokol_app.h public interface (absorbed verbatim below -- types, + enums, sapp_* declarations and the original documentation) and implements + it on every TrussC platform. The upstream sokol_app.h file does not exist + in this tree anymore. + + WHAT IT IS + ========== + The full sapp_* application layer (sapp_run, events, clipboard, dnd, + cursors, fullscreen, ...) plus native multi-window support with + per-window vsync ticks on the desktop platforms: the "main window" is + simply window #0, created from the sapp_desc, and the app's OS run loop + is owned here. The implementation TU looks like: + + #define SOKOL_IMPL + #include "sokol_log.h" + #define SOKOL_APP_TC_IMPL + #include "sokol_app_tc.h" // declarations + implementation + #include "sokol_gfx.h" + ... + + The single-window API behaves like sokol_app.h's native backends (the + cancellable quit dance, RGB10A2 default color format, clipboard, + drag&drop, cursor control, fullscreen; macOS: lazy drawable acquisition + in sapp_get_swapchain(); Windows: skip_present honored on Present, + per-monitor-v2 DPI), with one deliberate deviation: calling + sapp_dpi_scale() before sapp_run() returns the OS display scale + instead of 0. + + What replaces sokol_app.h's Windows render loop is the same per-window + tick idea: instead of the upstream PeekMessage busy-render-loop (and + instead of a ~64Hz WM_TIMER), every window's DXGI swapchain is created + with DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT and the message + loop blocks in MsgWaitForMultipleObjectsEx on the per-window frame + latency waitable objects: each window ticks when ITS swapchain can + accept a frame (true vsync pacing per display), and the loop sleeps + otherwise. A tick that ends without a Present (skip_present, occluded, + minimized) holds its waitable "credit" and is re-paced by a timer until + the next real Present, so neither busy-spinning nor waitable starvation + can occur. + + The multi-window API shape follows floooh's multi-window sketch (sokol + PR #437 / issue #229): a small `sapp_window` handle, a `sapp_window_desc`, + and window-parameterized query functions. What is NEW compared to #437 is + the frame model, and it is the load-bearing idea: + + - There is no shared frame loop that iterates windows. Each window has + its OWN vsync source (macOS: one CADisplayLink per window, delivered + on the main run loop) which invokes the window's `tick_cb`. + - A fully occluded window's display link suspends, and an explicit + occlusion gate guards drawable acquisition. The `[CAMetalLayer + nextDrawable]` ~1s stall that killed PR #437 cannot occur, because + the acquiring code path is only ever entered for a visible window. + An occluded window simply stops ticking (fps 0) and resumes on + un-occlusion; SAPP_EVENTTYPE_SUSPENDED / _RESUMED are sent at the + transitions. + - Windows on displays with different refresh rates each tick at their + own display's rate. Tick coalescing is depth-1 per window by + construction (the run loop is the queue and a display link never + stacks callbacks). + + Rendering: all windows share the one sokol_gfx context (post-2024 + sokol_gfx takes the swapchain per-pass). During `tick_cb`, acquire this + window's swapchain handles via sapp_window_metal_current_drawable() / + ..._depth_stencil_texture() / ..._msaa_color_texture() and start a pass + with them. The drawable is acquired lazily right before tick_cb and is + valid only for that callback. + + Events: delivered as plain sapp_event through the desc's `event_cb`, + with the owning window passed alongside (sapp_event itself has no window + field while we coexist with upstream sokol_app.h). Keyboard uses the + same keycode translation table as sokol_app.h, so SAPP_KEYCODE_* values + are identical across the main window and secondary windows. + + Coordinates follow sokol_app.h conventions: mouse positions arrive in + framebuffer pixels (divide by sapp_window_dpi_scale() for logical + points). dpi_scale is derived from ONE source (the ratio actually used + to size the layer's drawable), never assumed to be 2. + + Everything runs on the main thread. All functions must be called from + the main thread. + + PLATFORM SUPPORT + ================ + macOS: implemented (NSWindow + CAMetalLayer + CADisplayLink, macOS 14+). + Windows: implemented (HWND + D3D11 + DXGI flip swapchain, one frame + latency waitable object per window; Windows 10+). + Linux: implemented (X11 + GLX, one shared GL context with a GLXWindow + drawable per window; main window paced by its blocking vsync'd + glXSwapBuffers, secondary windows swap interval 0 + timer-paced). + Web: implemented, single-window (one , rAF frame loop, WebGPU or + WebGL2; sapp_create_window() returns the invalid handle -- a second + window is not representable in a browser tab). + iOS: implemented, single-window (UIApplicationMain + UIWindowScene + + CAMetalLayer + CADisplayLink, Metal only; sapp_create_window() returns + the invalid handle -- one UIWindow per scene). + Others (Android): stubs that return an invalid handle (explicit + platform gap). +*/ +#define SOKOL_APP_TC_INCLUDED (1) +#include +#include + +/* --------------------------------------------------------------------------- + Absorbed sokol_app.h public interface (documentation, types, enums and + API declarations) -- lifted verbatim from sokol_app.h by Andre Weissflog + (zlib license, see end of file). sokol_app_tc.h is self-contained: it IS + the sapp_* implementation on every TrussC platform, and the upstream + sokol_app.h file no longer exists in this tree. The SOKOL_APP_INCLUDED + guard is kept so third-party glue (sokol_glue.h, sokol_imgui.h) that + checks for it keeps working unchanged. + ------------------------------------------------------------------------ */ +#ifndef SOKOL_APP_INCLUDED +/* + sokol_app.h -- cross-platform application wrapper + + Project URL: https://github.com/floooh/sokol + + Do this: + #define SOKOL_IMPL or + #define SOKOL_APP_IMPL + before you include this file in *one* C or C++ file to create the + implementation. + + In the same place define one of the following to select the 3D-API + which should be initialized by sokol_app.h (this must also match + the backend selected for sokol_gfx.h if both are used in the same + project): + + #define SOKOL_GLCORE + #define SOKOL_GLES3 + #define SOKOL_D3D11 + #define SOKOL_METAL + #define SOKOL_WGPU + #define SOKOL_VULKAN + #define SOKOL_NOAPI + + Optionally provide the following defines with your own implementations: + + SOKOL_ASSERT(c) - your own assert macro (default: assert(c)) + SOKOL_UNREACHABLE() - a guard macro for unreachable code (default: assert(false)) + SOKOL_WIN32_FORCE_MAIN - define this on Win32 to add a main() entry point + SOKOL_WIN32_FORCE_WINMAIN - define this on Win32 to add a WinMain() entry point (enabled by default unless + SOKOL_WIN32_FORCE_MAIN or SOKOL_NO_ENTRY is defined) + SOKOL_NO_ENTRY - define this if sokol_app.h shouldn't "hijack" the main() function + SOKOL_APP_API_DECL - public function declaration prefix (default: extern) + SOKOL_API_DECL - same as SOKOL_APP_API_DECL + SOKOL_API_IMPL - public function implementation prefix (default: -) + + Optionally define the following to force debug checks and validations + even in release mode: + + SOKOL_DEBUG - by default this is defined if NDEBUG is not defined + + If sokol_app.h is compiled as a DLL, define the following before + including the declaration or implementation: + + SOKOL_DLL + + On Windows, SOKOL_DLL will define SOKOL_APP_API_DECL as __declspec(dllexport) + or __declspec(dllimport) as needed. + + if SOKOL_WIN32_FORCE_MAIN and SOKOL_WIN32_FORCE_WINMAIN are both defined, + it is up to the developer to define the desired subsystem. + + On Linux, SOKOL_GLCORE can use either GLX or EGL. + GLX is default, set SOKOL_FORCE_EGL to override. + + For example code, see https://github.com/floooh/sokol-samples/tree/master/sapp + + Portions of the Windows and Linux GL initialization, event-, icon- etc... code + have been taken from GLFW (http://www.glfw.org/). + + iOS onscreen keyboard support 'inspired' by libgdx. + + Link with the following system libraries: + + - on macOS: + - all backends: AppKit, QuartzCore + - with SOKOL_METAL: Metal + - with SOKOL_GLCORE: OpenGL + - with SOKOL_WGPU: a WebGPU implementation library (tested with webgpu_dawn) + - on iOS: + - all backends: Foundation, UIKit, QuartzCore + - with SOKOL_METAL: Metal + - with SOKOL_GLES3: OpenGLES, GLKit + - on Linux: + - all backends: X11, Xi, Xcursor, dl, pthread, m + - with SOKOL_GLCORE: GL + - with SOKOL_GLES3: GLESv2 + - with SOKOL_WGPU: a WebGPU implementation library (tested with webgpu_dawn) + - with SOKOL_VULKAN: vulkan + - with EGL: EGL + - on Android: GLESv3, EGL, log, android + - on Windows: + - with MSVC or Clang: library dependencies are defined via `#pragma comment` + - with SOKOL_WGPU: a WebGPU implementation library (tested with webgpu_dawn) + - with SOKOL_VULKAN: + - install the Vulkan SDK + - set a header search path to $VULKAN_SDK/Include + - set a library search path to $VULKAN_SDK/Lib + - link with vulkan-1.lib + - with MINGW/MSYS2 gcc: + - compile with '-mwin32' so that _WIN32 is defined + - link with the following libs: -lkernel32 -luser32 -lshell32 + - additionally with the GL backend: -lgdi32 + - additionally with the D3D11 backend: -ld3d11 -ldxgi + + On Linux, you also need to use the -pthread compiler and linker option, otherwise weird + things will happen, see here for details: https://github.com/floooh/sokol/issues/376 + + For Linux+Vulkan install the following packages (or equivalents): + - libvulkan-dev + - vulkan-validationlayers + - vulkan-tools + + On macOS and iOS, the implementation must be compiled as Objective-C. + + On Emscripten: + - for WebGL2: add the linker option `-s USE_WEBGL2=1` + - for WebGPU: compile and link with `--use-port=emdawnwebgpu` + (for more exotic situations read: https://dawn.googlesource.com/dawn/+/refs/heads/main/src/emdawnwebgpu/pkg/README.md) + + FEATURE OVERVIEW + ================ + sokol_app.h provides a minimalistic cross-platform API which + implements the 'application-wrapper' parts of a 3D application: + + - a common application entry function + - creates a window and 3D-API context/device with a swapchain + surface, depth-stencil-buffer surface and optionally MSAA surface + - makes the rendered frame visible + - provides keyboard-, mouse- and low-level touch-events + - platforms: MacOS, iOS, HTML5, Win32, Linux/RaspberryPi, Android + - 3D-APIs: Metal, D3D11, GL4.1, GL4.3, GLES3, WebGL2, WebGPU, NOAPI + + FEATURE/PLATFORM MATRIX + ======================= + | Windows | macOS | Linux | iOS | Android | HTML5 + --------------------+---------+-------+-------+-------+---------+-------- + gl 4.x | YES | YES | YES | --- | --- | --- + gles3/webgl2 | --- | --- | YES(2)| YES | YES | YES + metal | --- | YES | --- | YES | --- | --- + d3d11 | YES | --- | --- | --- | --- | --- + webgpu | YES(4) | YES(4)| YES(4)| NO | NO | YES + noapi | YES | TODO | TODO | --- | TODO | --- + KEY_DOWN | YES | YES | YES | SOME | TODO | YES + KEY_UP | YES | YES | YES | SOME | TODO | YES + CHAR | YES | YES | YES | YES | TODO | YES + MOUSE_DOWN | YES | YES | YES | --- | --- | YES + MOUSE_UP | YES | YES | YES | --- | --- | YES + MOUSE_SCROLL | YES | YES | YES | --- | --- | YES + MOUSE_MOVE | YES | YES | YES | --- | --- | YES + MOUSE_ENTER | YES | YES | YES | --- | --- | YES + MOUSE_LEAVE | YES | YES | YES | --- | --- | YES + TOUCHES_BEGAN | --- | --- | --- | YES | YES | YES + TOUCHES_MOVED | --- | --- | --- | YES | YES | YES + TOUCHES_ENDED | --- | --- | --- | YES | YES | YES + TOUCHES_CANCELLED | --- | --- | --- | YES | YES | YES + RESIZED | YES | YES | YES | YES | YES | YES + ICONIFIED | YES | YES | YES | --- | --- | --- + RESTORED | YES | YES | YES | --- | --- | --- + FOCUSED | YES | YES | YES | --- | --- | YES + UNFOCUSED | YES | YES | YES | --- | --- | YES + SUSPENDED | --- | --- | --- | YES | YES | TODO + RESUMED | --- | --- | --- | YES | YES | TODO + QUIT_REQUESTED | YES | YES | YES | --- | --- | YES + IME | TODO | TODO? | TODO | ??? | TODO | ??? + key repeat flag | YES | YES | YES | --- | --- | YES + windowed | YES | YES | YES | --- | --- | YES + fullscreen | YES | YES | YES | YES | YES | YES(3) + mouse hide | YES | YES | YES | --- | --- | YES + mouse lock | YES | YES | YES | --- | --- | YES + set cursor type | YES | YES | YES | --- | --- | YES + screen keyboard | --- | --- | --- | YES | TODO | YES + swap interval | YES | YES | YES | YES | TODO | YES + high-dpi | YES | YES | TODO | YES | YES | YES + clipboard | YES | YES | YES | --- | --- | YES + MSAA | YES | YES | YES | YES | YES | YES + drag'n'drop | YES | YES | YES | --- | --- | YES + window icon | YES | YES(1)| YES | --- | --- | YES + + (1) macOS has no regular window icons, instead the dock icon is changed + (2) supported with EGL only (not GLX) + (3) fullscreen in the browser not supported on iphones + (4) WebGPU on native desktop platforms should be considered experimental + and mainly useful for debugging and benchmarking + + STEP BY STEP + ============ + --- Add a sokol_main() function to your code which returns a sapp_desc structure + with initialization parameters and callback function pointers. This + function is called very early, usually at the start of the + platform's entry function (e.g. main or WinMain). You should do as + little as possible here, since the rest of your code might be called + from another thread (this depends on the platform): + + sapp_desc sokol_main(int argc, char* argv[]) { + return (sapp_desc) { + .width = 640, + .height = 480, + .init_cb = my_init_func, + .frame_cb = my_frame_func, + .cleanup_cb = my_cleanup_func, + .event_cb = my_event_func, + ... + }; + } + + To get any logging output in case of errors you need to provide a log + callback. The easiest way is via sokol_log.h: + + #include "sokol_log.h" + + sapp_desc sokol_main(int argc, char* argv[]) { + return (sapp_desc) { + ... + .logger.func = slog_func, + }; + } + + There are many more setup parameters, but these are the most important. + For a complete list search for the sapp_desc structure declaration + below. + + DO NOT call any sokol-app function from inside sokol_main(), since + sokol-app will not be initialized at this point. + + The .width and .height parameters are the preferred size of the 3D + rendering canvas. The actual size may differ from this depending on + platform and other circumstances. Also the canvas size may change at + any time (for instance when the user resizes the application window, + or rotates the mobile device). You can just keep .width and .height + zero-initialized to open a default-sized window (what "default-size" + exactly means is platform-specific, but usually it's a size that covers + most of, but not all, of the display). + + All provided function callbacks will be called from the same thread, + but this may be different from the thread where sokol_main() was called. + + .init_cb (void (*)(void)) + This function is called once after the application window, + 3D rendering context and swap chain have been created. The + function takes no arguments and has no return value. + .frame_cb (void (*)(void)) + This is the per-frame callback, which is usually called 60 + times per second. This is where your application would update + most of its state and perform all rendering. + .cleanup_cb (void (*)(void)) + The cleanup callback is called once right before the application + quits. + .event_cb (void (*)(const sapp_event* event)) + The event callback is mainly for input handling, but is also + used to communicate other types of events to the application. Keep the + event_cb struct member zero-initialized if your application doesn't require + event handling. + + As you can see, those 'standard callbacks' don't have a user_data + argument, so any data that needs to be preserved between callbacks + must live in global variables. If keeping state in global variables + is not an option, there's an alternative set of callbacks with + an additional user_data pointer argument: + + .user_data (void*) + The user-data argument for the callbacks below + .init_userdata_cb (void (*)(void* user_data)) + .frame_userdata_cb (void (*)(void* user_data)) + .cleanup_userdata_cb (void (*)(void* user_data)) + .event_userdata_cb (void(*)(const sapp_event* event, void* user_data)) + + The function sapp_userdata() can be used to query the user_data + pointer provided in the sapp_desc struct. + + You can also call sapp_query_desc() to get a copy of the + original sapp_desc structure. + + NOTE that there's also an alternative compile mode where sokol_app.h + doesn't "hijack" the main() function. Search below for SOKOL_NO_ENTRY. + + --- Implement the initialization callback function (init_cb), this is called + once after the rendering surface, 3D API and swap chain have been + initialized by sokol_app. All sokol-app functions can be called + from inside the initialization callback, the most useful functions + at this point are: + + int sapp_width(void) + int sapp_height(void) + Returns the current width and height of the default framebuffer in pixels, + this may change from one frame to the next, and it may be different + from the initial size provided in the sapp_desc struct. + + float sapp_widthf(void) + float sapp_heightf(void) + These are alternatives to sapp_width() and sapp_height() which return + the default framebuffer size as float values instead of integer. This + may help to prevent casting back and forth between int and float + in more strongly typed languages than C and C++. + + double sapp_frame_duration(void) + Returns a smoothed frame duration. + + double sapp_frame_duration_unfiltered(void) + Returns the unfiltered frame duration with varying degree of + jitter (depending on platform and backend). + + int sapp_color_format(void) + int sapp_depth_format(void) + The color and depth-stencil pixelformats of the default framebuffer, + as integer values which are compatible with sokol-gfx's + sg_pixel_format enum (so that they can be plugged directly in places + where sg_pixel_format is expected). Possible values are: + + 23 == SG_PIXELFORMAT_RGBA8 + 28 == SG_PIXELFORMAT_BGRA8 + 42 == SG_PIXELFORMAT_DEPTH + 43 == SG_PIXELFORMAT_DEPTH_STENCIL + + int sapp_sample_count(void) + Return the MSAA sample count of the default framebuffer. + + const void* sapp_metal_get_device(void) + const void* sapp_metal_get_current_drawable(void) + const void* sapp_metal_get_depth_stencil_texture(void) + const void* sapp_metal_get_msaa_color_texture(void) + If the Metal backend has been selected, these functions return pointers + to various Metal API objects required for rendering, otherwise + they return a null pointer. These void pointers are actually + Objective-C ids converted with a (ARC) __bridge cast so that + the ids can be tunneled through C code. Also note that the returned + pointers may change from one frame to the next, only the Metal device + object is guaranteed to stay the same. + + const void* sapp_macos_get_window(void) + On macOS, get the NSWindow object pointer, otherwise a null pointer. + Before being used as Objective-C object, the void* must be converted + back with a (ARC) __bridge cast. + + const void* sapp_ios_get_window(void) + On iOS, get the UIWindow object pointer, otherwise a null pointer. + Before being used as Objective-C object, the void* must be converted + back with a (ARC) __bridge cast. + + const void* sapp_d3d11_get_device(void) + const void* sapp_d3d11_get_device_context(void) + const void* sapp_d3d11_get_render_view(void) + const void* sapp_d3d11_get_resolve_view(void); + const void* sapp_d3d11_get_depth_stencil_view(void) + Similar to the sapp_metal_* functions, the sapp_d3d11_* functions + return pointers to D3D11 API objects required for rendering, + only if the D3D11 backend has been selected. Otherwise they + return a null pointer. Note that the returned pointers to the + render-target-view and depth-stencil-view may change from one + frame to the next! + + const void* sapp_win32_get_hwnd(void) + On Windows, get the window's HWND, otherwise a null pointer. The + HWND has been cast to a void pointer in order to be tunneled + through code which doesn't include Windows.h. + + const void* sapp_x11_get_window(void) + On Linux, get the X11 Window, otherwise a null pointer. The + Window has been cast to a void pointer in order to be tunneled + through code which doesn't include X11/Xlib.h. + + const void* sapp_x11_get_display(void) + On Linux, get the X11 Display, otherwise a null pointer. The + Display has been cast to a void pointer in order to be tunneled + through code which doesn't include X11/Xlib.h. + + const void* sapp_wgpu_get_device(void) + const void* sapp_wgpu_get_render_view(void) + const void* sapp_wgpu_get_resolve_view(void) + const void* sapp_wgpu_get_depth_stencil_view(void) + These are the WebGPU-specific functions to get the WebGPU + objects and values required for rendering. If sokol_app.h + is not compiled with SOKOL_WGPU, these functions return null. + + uint32_t sapp_gl_get_framebuffer(void) + This returns the 'default framebuffer' of the GL context. + Typically this will be zero. + + int sapp_gl_get_major_version(void) + int sapp_gl_get_minor_version(void) + bool sapp_gl_is_gles(void) + Returns the major and minor version of the GL context and + whether the GL context is a GLES context + + const void* sapp_android_get_native_activity(void); + On Android, get the native activity ANativeActivity pointer, otherwise + a null pointer. + + --- Implement the frame-callback function, this function will be called + on the same thread as the init callback, but might be on a different + thread than the sokol_main() function. Note that the size of + the rendering framebuffer might have changed since the frame callback + was called last. Call the functions sapp_width() and sapp_height() + each frame to get the current size. + + --- Optionally implement the event-callback to handle input events. + sokol-app provides the following type of input events: + - a 'virtual key' was pressed down or released + - a single text character was entered (provided as UTF-32 encoded + UNICODE code point) + - a mouse button was pressed down or released (left, right, middle) + - mouse-wheel or 2D scrolling events + - the mouse was moved + - the mouse has entered or left the application window boundaries + - low-level, portable multi-touch events (began, moved, ended, cancelled) + - the application window was resized, iconified or restored + - the application was suspended or restored (on mobile platforms) + - the user or application code has asked to quit the application + - a string was pasted to the system clipboard + - one or more files have been dropped onto the application window + + To explicitly 'consume' an event and prevent that the event is + forwarded for further handling to the operating system, call + sapp_consume_event() from inside the event handler (NOTE that + this behaviour is currently only implemented for some HTML5 + events, support for other platforms and event types will + be added as needed, please open a GitHub ticket and/or provide + a PR if needed). + + NOTE: Do *not* call any 3D API rendering functions in the event + callback function, since the 3D API context may not be active when the + event callback is called (it may work on some platforms and 3D APIs, + but not others, and the exact behaviour may change between + sokol-app versions). + + --- Implement the cleanup-callback function, this is called once + after the user quits the application (see the section + "APPLICATION QUIT" for detailed information on quitting + behaviour, and how to intercept a pending quit - for instance to show a + "Really Quit?" dialog box). Note that the cleanup-callback isn't + guaranteed to be called on the web and mobile platforms. + + MOUSE CURSOR TYPE AND VISIBILITY + ================================ + You can show and hide the mouse cursor with + + void sapp_show_mouse(bool show) + + And to get the current shown status: + + bool sapp_mouse_shown(void) + + NOTE that hiding the mouse cursor is different and independent from + the MOUSE/POINTER LOCK feature which will also hide the mouse pointer when + active (MOUSE LOCK is described below). + + To change the mouse cursor to one of several predefined types, call + the function: + + void sapp_set_mouse_cursor(sapp_mouse_cursor cursor) + + Setting the default mouse cursor SAPP_MOUSECURSOR_DEFAULT will restore + the standard look. + + To get the currently active mouse cursor type, call: + + sapp_mouse_cursor sapp_get_mouse_cursor(void) + + MOUSE LOCK (AKA POINTER LOCK, AKA MOUSE CAPTURE) + ================================================ + In normal mouse mode, no mouse movement events are reported when the + mouse leaves the windows client area or hits the screen border (whether + it's one or the other depends on the platform), and the mouse move events + (SAPP_EVENTTYPE_MOUSE_MOVE) contain absolute mouse positions in + framebuffer pixels in the sapp_event items mouse_x and mouse_y, and + relative movement in framebuffer pixels in the sapp_event items mouse_dx + and mouse_dy. + + To get continuous mouse movement (also when the mouse leaves the window + client area or hits the screen border), activate mouse-lock mode + by calling: + + sapp_lock_mouse(true) + + When mouse lock is activated, the mouse pointer is hidden, the + reported absolute mouse position (sapp_event.mouse_x/y) appears + frozen, and the relative mouse movement in sapp_event.mouse_dx/dy + no longer has a direct relation to framebuffer pixels but instead + uses "raw mouse input" (what "raw mouse input" exactly means also + differs by platform). + + To deactivate mouse lock and return to normal mouse mode, call + + sapp_lock_mouse(false) + + And finally, to check if mouse lock is currently active, call + + if (sapp_mouse_locked()) { ... } + + Note that mouse-lock state may not change immediately after sapp_lock_mouse(true/false) + is called, instead on some platforms the actual state switch may be delayed + to the end of the current frame or even to a later frame. + + The mouse may also be unlocked automatically without calling sapp_lock_mouse(false), + most notably when the application window becomes inactive. + + On the web platform there are further restrictions to be aware of, caused + by the limitations of the HTML5 Pointer Lock API: + + - sapp_lock_mouse(true) can be called at any time, but it will + only take effect in a 'short-lived input event handler of a specific + type', meaning when one of the following events happens: + - SAPP_EVENTTYPE_MOUSE_DOWN + - SAPP_EVENTTYPE_MOUSE_UP + - SAPP_EVENTTYPE_MOUSE_SCROLL + - SAPP_EVENTTYPE_KEY_UP + - SAPP_EVENTTYPE_KEY_DOWN + - The mouse lock/unlock action on the web platform is asynchronous, + this means that sapp_mouse_locked() won't immediately return + the new status after calling sapp_lock_mouse(), instead the + reported status will only change when the pointer lock has actually + been activated or deactivated in the browser. + - On the web, mouse lock can be deactivated by the user at any time + by pressing the Esc key. When this happens, sokol_app.h behaves + the same as if sapp_lock_mouse(false) is called. + + For things like camera manipulation it's most straightforward to lock + and unlock the mouse right from the sokol_app.h event handler, for + instance the following code enters and leaves mouse lock when the + left mouse button is pressed and released, and then uses the relative + movement information to manipulate a camera (taken from the + cgltf-sapp.c sample in the sokol-samples repository + at https://github.com/floooh/sokol-samples): + + static void input(const sapp_event* ev) { + switch (ev->type) { + case SAPP_EVENTTYPE_MOUSE_DOWN: + if (ev->mouse_button == SAPP_MOUSEBUTTON_LEFT) { + sapp_lock_mouse(true); + } + break; + + case SAPP_EVENTTYPE_MOUSE_UP: + if (ev->mouse_button == SAPP_MOUSEBUTTON_LEFT) { + sapp_lock_mouse(false); + } + break; + + case SAPP_EVENTTYPE_MOUSE_MOVE: + if (sapp_mouse_locked()) { + cam_orbit(&state.camera, ev->mouse_dx * 0.25f, ev->mouse_dy * 0.25f); + } + break; + + default: + break; + } + } + + For a 'first person shooter mouse' the following code inside the sokol-app event handler + is recommended somewhere in your frame callback: + + if (!sapp_mouse_locked()) { + sapp_lock_mouse(true); + } + + CLIPBOARD SUPPORT + ================= + Applications can send and receive UTF-8 encoded text data from and to the + system clipboard. By default, clipboard support is disabled and + must be enabled at startup via the following sapp_desc struct + members: + + sapp_desc.enable_clipboard - set to true to enable clipboard support + sapp_desc.clipboard_size - size of the internal clipboard buffer in bytes + + Enabling the clipboard will dynamically allocate a clipboard buffer + for UTF-8 encoded text data of the requested size in bytes, the default + size is 8 KBytes. Strings that don't fit into the clipboard buffer + (including the terminating zero) will be silently clipped, so it's + important that you provide a big enough clipboard size for your + use case. + + To send data to the clipboard, call sapp_set_clipboard_string() with + a pointer to an UTF-8 encoded, null-terminated C-string. + + NOTE that on the HTML5 platform, sapp_set_clipboard_string() must be + called from inside a 'short-lived event handler', and there are a few + other HTML5-specific caveats to workaround. You'll basically have to + tinker until it works in all browsers :/ (maybe the situation will + improve when all browsers agree on and implement the new + HTML5 navigator.clipboard API). + + To get data from the clipboard, check for the SAPP_EVENTTYPE_CLIPBOARD_PASTED + event in your event handler function, and then call sapp_get_clipboard_string() + to obtain the pasted UTF-8 encoded text. + + NOTE that behaviour of sapp_get_clipboard_string() is slightly different + depending on platform: + + - on the HTML5 platform, the internal clipboard buffer will only be updated + right before the SAPP_EVENTTYPE_CLIPBOARD_PASTED event is sent, + and sapp_get_clipboard_string() will simply return the current content + of the clipboard buffer + - on 'native' platforms, the call to sapp_get_clipboard_string() will + update the internal clipboard buffer with the most recent data + from the system clipboard + + Portable code should check for the SAPP_EVENTTYPE_CLIPBOARD_PASTED event, + and then call sapp_get_clipboard_string() right in the event handler. + + The SAPP_EVENTTYPE_CLIPBOARD_PASTED event will be generated by sokol-app + as follows: + + - on macOS: when the Cmd+V key is pressed down + - on HTML5: when the browser sends a 'paste' event to the global 'window' object + - on all other platforms: when the Ctrl+V key is pressed down + + DRAG AND DROP SUPPORT + ===================== + PLEASE NOTE: the drag'n'drop feature works differently on WASM/HTML5 + and on the native desktop platforms (Win32, Linux and macOS) because + of security-related restrictions in the HTML5 drag'n'drop API. The + WASM/HTML5 specifics are described at the end of this documentation + section: + + Like clipboard support, drag'n'drop support must be explicitly enabled + at startup in the sapp_desc struct. + + sapp_desc sokol_main(void) { + return (sapp_desc) { + .enable_dragndrop = true, // default is false + ... + }; + } + + You can also adjust the maximum number of files that are accepted + in a drop operation, and the maximum path length in bytes if needed: + + sapp_desc sokol_main(void) { + return (sapp_desc) { + .enable_dragndrop = true, // default is false + .max_dropped_files = 8, // default is 1 + .max_dropped_file_path_length = 8192, // in bytes, default is 2048 + ... + }; + } + + When drag'n'drop is enabled, the event callback will be invoked with an + event of type SAPP_EVENTTYPE_FILES_DROPPED whenever the user drops files on + the application window. + + After the SAPP_EVENTTYPE_FILES_DROPPED is received, you can query the + number of dropped files, and their absolute paths by calling separate + functions: + + void on_event(const sapp_event* ev) { + if (ev->type == SAPP_EVENTTYPE_FILES_DROPPED) { + + // the mouse position where the drop happened + float x = ev->mouse_x; + float y = ev->mouse_y; + + // get the number of files and their paths like this: + const int num_dropped_files = sapp_get_num_dropped_files(); + for (int i = 0; i < num_dropped_files; i++) { + const char* path = sapp_get_dropped_file_path(i); + ... + } + } + } + + The returned file paths are UTF-8 encoded strings. + + You can call sapp_get_num_dropped_files() and sapp_get_dropped_file_path() + anywhere, also outside the event handler callback, but be aware that the + file path strings will be overwritten with the next drop operation. + + In any case, sapp_get_dropped_file_path() will never return a null pointer, + instead an empty string "" will be returned if the drag'n'drop feature + hasn't been enabled, the last drop-operation failed, or the file path index + is out of range. + + Drag'n'drop caveats: + + - if more files are dropped in a single drop-action + than sapp_desc.max_dropped_files, the additional + files will be silently ignored + - if any of the file paths is longer than + sapp_desc.max_dropped_file_path_length (in number of bytes, after UTF-8 + encoding) the entire drop operation will be silently ignored (this + needs some sort of error feedback in the future) + - no mouse positions are reported while the drag is in + process, this may change in the future + + Drag'n'drop on HTML5/WASM: + + The HTML5 drag'n'drop API doesn't return file paths, but instead + black-box 'file objects' which must be used to load the content + of dropped files. This is the reason why sokol_app.h adds two + HTML5-specific functions to the drag'n'drop API: + + uint32_t sapp_html5_get_dropped_file_size(int index) + Returns the size in bytes of a dropped file. + + void sapp_html5_fetch_dropped_file(const sapp_html5_fetch_request* request) + Asynchronously loads the content of a dropped file into a + provided memory buffer (which must be big enough to hold + the file content) + + To start loading the first dropped file after an SAPP_EVENTTYPE_FILES_DROPPED + event is received: + + sapp_html5_fetch_dropped_file(&(sapp_html5_fetch_request){ + .dropped_file_index = 0, + .callback = fetch_cb + .buffer = { + .ptr = buf, + .size = sizeof(buf) + }, + .user_data = ... + }); + + Make sure that the memory pointed to by 'buf' stays valid until the + callback function is called! + + As result of the asynchronous loading operation (no matter if succeeded or + failed) the 'fetch_cb' function will be called: + + void fetch_cb(const sapp_html5_fetch_response* response) { + // IMPORTANT: check if the loading operation actually succeeded: + if (response->succeeded) { + // the size of the loaded file: + const size_t num_bytes = response->data.size; + // and the pointer to the data (same as 'buf' in the fetch-call): + const void* ptr = response->data.ptr; + } else { + // on error check the error code: + switch (response->error_code) { + case SAPP_HTML5_FETCH_ERROR_BUFFER_TOO_SMALL: + ... + break; + case SAPP_HTML5_FETCH_ERROR_OTHER: + ... + break; + } + } + } + + Check the droptest-sapp example for a real-world example which works + both on native platforms and the web: + + https://github.com/floooh/sokol-samples/blob/master/sapp/droptest-sapp.c + + HIGH-DPI RENDERING + ================== + You can set the sapp_desc.high_dpi flag during initialization to request + a full-resolution framebuffer on HighDPI displays. The default behaviour + is sapp_desc.high_dpi=false, this means that the application will + render to a lower-resolution framebuffer on HighDPI displays and the + rendered content will be upscaled by the window system composer. + + In a HighDPI scenario, you still request the same window size during + sokol_main(), but the framebuffer sizes returned by sapp_width() + and sapp_height() will be scaled up according to the DPI scaling + ratio. + + Note that on some platforms the DPI scaling factor may change at any + time (for instance when a window is moved from a high-dpi display + to a low-dpi display). + + To query the current DPI scaling factor, call the function: + + float sapp_dpi_scale(void); + + For instance on a Retina Mac, returning the following sapp_desc + struct from sokol_main(): + + sapp_desc sokol_main(void) { + return (sapp_desc) { + .width = 640, + .height = 480, + .high_dpi = true, + ... + }; + } + + ...the functions the functions sapp_width(), sapp_height() + and sapp_dpi_scale() will return the following values: + + sapp_width: 1280 + sapp_height: 960 + sapp_dpi_scale: 2.0 + + If the high_dpi flag is false, or you're not running on a Retina display, + the values would be: + + sapp_width: 640 + sapp_height: 480 + sapp_dpi_scale: 1.0 + + If the window is moved from the Retina display to a low-dpi external display, + the values would change as follows: + + sapp_width: 1280 => 640 + sapp_height: 960 => 480 + sapp_dpi_scale: 2.0 => 1.0 + + Currently there is no event associated with a DPI change, but an + SAPP_EVENTTYPE_RESIZED will be sent as a side effect of the + framebuffer size changing. + + Per-monitor DPI is currently supported on macOS and Windows. + + APPLICATION QUIT + ================ + Without special quit handling, a sokol_app.h application will quit + 'gracefully' when the user clicks the window close-button unless a + platform's application model prevents this (e.g. on web or mobile). + 'Graceful exit' means that the application-provided cleanup callback will + be called before the application quits. + + On native desktop platforms sokol_app.h provides more control over the + application-quit-process. It's possible to initiate a 'programmatic quit' + from the application code, and a quit initiated by the application user can + be intercepted (for instance to show a custom dialog box). + + This 'programmatic quit protocol' is implemented through 3 functions + and 1 event: + + - sapp_quit(): This function simply quits the application without + giving the user a chance to intervene. Usually this might + be called when the user clicks the 'Ok' button in a 'Really Quit?' + dialog box + - sapp_request_quit(): Calling sapp_request_quit() will send the + event SAPP_EVENTTYPE_QUIT_REQUESTED to the applications event handler + callback, giving the user code a chance to intervene and cancel the + pending quit process (for instance to show a 'Really Quit?' dialog + box). If the event handler callback does nothing, the application + will be quit as usual. To prevent this, call the function + sapp_cancel_quit() from inside the event handler. + - sapp_cancel_quit(): Cancels a pending quit request, either initiated + by the user clicking the window close button, or programmatically + by calling sapp_request_quit(). The only place where calling this + function makes sense is from inside the event handler callback when + the SAPP_EVENTTYPE_QUIT_REQUESTED event has been received. + - SAPP_EVENTTYPE_QUIT_REQUESTED: this event is sent when the user + clicks the window's close button or application code calls the + sapp_request_quit() function. The event handler callback code can handle + this event by calling sapp_cancel_quit() to cancel the quit. + If the event is ignored, the application will quit as usual. + + On the web platform, the quit behaviour differs from native platforms, + because of web-specific restrictions: + + A `programmatic quit` initiated by calling sapp_quit() or + sapp_request_quit() will work as described above: the cleanup callback is + called, platform-specific cleanup is performed (on the web + this means that JS event handlers are unregistered), and then + the request-animation-loop will be exited. However that's all. The + web page itself will continue to exist (e.g. it's not possible to + programmatically close the browser tab). + + On the web it's also not possible to run custom code when the user + closes a browser tab, so it's not possible to prevent this with a + fancy custom dialog box. + + Instead the standard "Leave Site?" dialog box can be activated (or + deactivated) with the following function: + + sapp_html5_ask_leave_site(bool ask); + + The initial state of the associated internal flag can be provided + at startup via sapp_desc.html5.ask_leave_site. + + This feature should only be used sparingly in critical situations - for + instance when the user would loose data - since popping up modal dialog + boxes is considered quite rude in the web world. Note that there's no way + to customize the content of this dialog box or run any code as a result + of the user's decision. Also note that the user must have interacted with + the site before the dialog box will appear. These are all security measures + to prevent fishing. + + The Dear ImGui HighDPI sample contains example code of how to + implement a 'Really Quit?' dialog box with Dear ImGui (native desktop + platforms only), and for showing the hardwired "Leave Site?" dialog box + when running on the web platform: + + https://floooh.github.io/sokol-html5/wasm/imgui-highdpi-sapp.html + + FULLSCREEN + ========== + If the sapp_desc.fullscreen flag is true, sokol-app will try to create + a fullscreen window on platforms with a 'proper' window system + (mobile devices will always use fullscreen). The implementation details + depend on the target platform, in general sokol-app will use a + 'soft approach' which doesn't interfere too much with the platform's + window system (for instance borderless fullscreen window instead of + a 'real' fullscreen mode). Such details might change over time + as sokol-app is adapted for different needs. + + The most important effect of fullscreen mode to keep in mind is that + the requested canvas width and height will be ignored for the initial + window size, calling sapp_width() and sapp_height() will instead return + the resolution of the fullscreen canvas (however the provided size + might still be used for the non-fullscreen window, in case the user can + switch back from fullscreen- to windowed-mode). + + To toggle fullscreen mode programmatically, call sapp_toggle_fullscreen(). + + To check if the application window is currently in fullscreen mode, + call sapp_is_fullscreen(). + + On the web, sapp_desc.fullscreen will have no effect, and the application + will always start in non-fullscreen mode. Call sapp_toggle_fullscreen() + from within or 'near' an input event to switch to fullscreen programatically. + Note that on the web, the fullscreen state may change back to windowed at + any time (either because the browser had rejected switching into fullscreen, + or the user leaves fullscreen via Esc), this means that the result + of sapp_is_fullscreen() may change also without calling sapp_toggle_fullscreen()! + + + WINDOW ICON SUPPORT + =================== + Some sokol_app.h backends allow to change the window icon programmatically: + + - on Win32: the small icon in the window's title bar, and the + bigger icon in the task bar + - on Linux: highly dependent on the used window manager, but usually + the window's title bar icon and/or the task bar icon + - on HTML5: the favicon shown in the page's browser tab + - on macOS: the application icon shown in the dock, but only + for currently running applications + + NOTE that it is not possible to set the actual application icon which is + displayed by the operating system on the desktop or 'home screen'. Those + icons must be provided 'traditionally' through operating-system-specific + resources which are associated with the application (sokol_app.h might + later support setting the window icon from platform specific resource data + though). + + There are two ways to set the window icon: + + - at application start in the sokol_main() function by initializing + the sapp_desc.icon nested struct + - or later by calling the function sapp_set_icon() + + As a convenient shortcut, sokol_app.h comes with a builtin default-icon + (a rainbow-colored 'S', which at least looks a bit better than the Windows + default icon for applications), which can be activated like this: + + At startup in sokol_main(): + + sapp_desc sokol_main(...) { + return (sapp_desc){ + ... + icon.sokol_default = true + }; + } + + Or later by calling: + + sapp_set_icon(&(sapp_icon_desc){ .sokol_default = true }); + + NOTE that a completely zero-initialized sapp_icon_desc struct will not + update the window icon in any way. This is an 'escape hatch' so that you + can handle the window icon update yourself (or if you do this already, + sokol_app.h won't get in your way, in this case just leave the + sapp_desc.icon struct zero-initialized). + + Providing your own icon images works exactly like in GLFW (down to the + data format): + + You provide one or more 'candidate images' in different sizes, and the + sokol_app.h platform backends pick the best match for the specific backend + and icon type. + + For each candidate image, you need to provide: + + - the width in pixels + - the height in pixels + - and the actual pixel data in RGBA8 pixel format (e.g. 0xFFCC8844 + on a little-endian CPU means: alpha=0xFF, blue=0xCC, green=0x88, red=0x44) + + For instance, if you have 3 candidate images (small, medium, big) of + sizes 16x16, 32x32 and 64x64 the corresponding sapp_icon_desc struct is setup + like this: + + // the actual pixel data (RGBA8, origin top-left) + const uint32_t small[16][16] = { ... }; + const uint32_t medium[32][32] = { ... }; + const uint32_t big[64][64] = { ... }; + + const sapp_icon_desc icon_desc = { + .images = { + { .width = 16, .height = 16, .pixels = SAPP_RANGE(small) }, + { .width = 32, .height = 32, .pixels = SAPP_RANGE(medium) }, + // ...or without the SAPP_RANGE helper macro: + { .width = 64, .height = 64, .pixels = { .ptr=big, .size=sizeof(big) } } + } + }; + + An sapp_icon_desc struct initialized like this can then either be applied + at application start in sokol_main: + + sapp_desc sokol_main(...) { + return (sapp_desc){ + ... + icon = icon_desc + }; + } + + ...or later by calling sapp_set_icon(): + + sapp_set_icon(&icon_desc); + + Some window icon caveats: + + - once the window icon has been updated, there's no way to go back to + the platform's default icon, this is because some platforms (Linux + and HTML5) don't switch the icon visual back to the default even if + the custom icon is deleted or removed + - on HTML5, if the sokol_app.h icon doesn't show up in the browser + tab, check that there's no traditional favicon 'link' element + is defined in the page's index.html, sokol_app.h will only + append a new favicon link element, but not delete any manually + defined favicon in the page + + For an example and test of the window icon feature, check out the + 'icon-sapp' sample on the sokol-samples git repository. + + ONSCREEN KEYBOARD + ================= + On some platforms which don't provide a physical keyboard, sokol-app + can display the platform's integrated onscreen keyboard for text + input. To request that the onscreen keyboard is shown, call + + sapp_show_keyboard(true); + + Likewise, to hide the keyboard call: + + sapp_show_keyboard(false); + + Note that onscreen keyboard functionality is no longer supported + on the browser platform (the previous hacks and workarounds to make browser + keyboards work for on web applications that don't use HTML UIs + never really worked across browsers). + + INPUT EVENT BUBBLING ON THE WEB PLATFORM + ======================================== + By default, input event bubbling on the web platform is configured in + a way that makes the most sense for 'full-canvas' apps that cover the + entire browser client window area: + + - mouse, touch and wheel events do not bubble up, this prevents various + ugly side events, like: + - HTML text overlays being selected on double- or triple-click into + the canvas + - 'scroll bumping' even when the canvas covers the entire client area + - key_up/down events for 'character keys' *do* bubble up (otherwise + the browser will not generate UNICODE character events) + - all other key events *do not* bubble up by default (this prevents side effects + like F1 opening help, or F7 starting 'caret browsing') + - character events do not bubble up (although I haven't noticed any side effects + otherwise) + + Event bubbling can be enabled for input event categories during initialization + in the sapp_desc struct: + + sapp_desc sokol_main(int argc, char* argv[]) { + return (sapp_desc){ + //... + .html5 = { + .bubble_mouse_events = true, + .bubble_touch_events = true, + .bubble_wheel_events = true, + .bubble_key_events = true, + .bubble_char_events = true, + } + }; + } + + This basically opens the floodgates and lets *all* input events bubble up to the browser. + + To prevent individual events from bubbling, call sapp_consume_event() from within + the sokol_app.h event callback when that specific event is reported. + + + SETTING THE CANVAS OBJECT ON THE WEB PLATFORM + ============================================= + On the web, sokol_app.h and the Emscripten SDK functions need to find + the WebGL/WebGPU canvas intended for rendering and attaching event + handlers. This can happen in four ways: + + 1. do nothing and just set the id of the canvas object to 'canvas' (preferred) + 2. via a CSS Selector string (preferred) + 3. by setting the `Module.canvas` property to the canvas object + 4. by adding the canvas object to the global variable `specialHTMLTargets[]` + (this is a special variable used by the Emscripten runtime to lookup + event target objects for which document.querySelector() cannot be used) + + The easiest way is to just name your canvas object 'canvas': + + + + This works because the default css selector string used by sokol_app.h + is '#canvas'. + + If you name your canvas differently, you need to communicate that name to + sokol_app.h via `sapp_desc.html5.canvas_selector` as a regular css selector + string that's compatible with `document.querySelector()`. E.g. if your canvas + object looks like this: + + + + The `sapp_desc.html5.canvas_selector` string must be set to '#bla': + + .html5.canvas_selector = "#bla" + + If the canvas object cannot be looked up via `document.querySelector()` you + need to use one of the alternative methods, both involve the special + Emscripten runtime `Module` object which is usually setup in the index.html + like this before the WASM blob is loaded and instantiated: + + + + The first option is to set the `Module.canvas` property to your canvas object: + + + + When sokol_app.h initializes, it will check the global Module object whether + a `Module.canvas` property exists and is an object. This method will add + a new entry to the `specialHTMLTargets[]` object + + The other option is to add the canvas under a name chosen by you to the + special `specialHTMLTargets[]` map, which is used by the Emscripten runtime + to lookup 'event target objects' which are not visible to `document.querySelector()`. + Note that `specialHTMLTargets[]` must be updated after the Emscripten runtime + has started but before the WASM code is running. A good place for this is + the special `Module.preRun` array in index.html: + + + + In that case, pass the same string to sokol_app.h which is used as key + in the specialHTMLTargets[] map: + + .html5.canvas_selector = "my_canvas" + + If sokol_app.h can't find your canvas for some reason check for warning + messages on the browser console. + + + OPTIONAL: DON'T HIJACK main() (#define SOKOL_NO_ENTRY) + ====================================================== + NOTE: SOKOL_NO_ENTRY and sapp_run() is currently not supported on Android. + + In its default configuration, sokol_app.h "hijacks" the platform's + standard main() function. This was done because different platforms + have different entry point conventions which are not compatible with + C's main() (for instance WinMain on Windows has completely different + arguments). However, this "main hijacking" posed a problem for + usage scenarios like integrating sokol_app.h with other languages than + C or C++, so an alternative SOKOL_NO_ENTRY mode has been added + in which the user code provides the platform's main function: + + - define SOKOL_NO_ENTRY before including the sokol_app.h implementation + - do *not* provide a sokol_main() function + - instead provide the standard main() function of the platform + - from the main function, call the function ```sapp_run()``` which + takes a pointer to an ```sapp_desc``` structure. + - from here on```sapp_run()``` takes over control and calls the provided + init-, frame-, event- and cleanup-callbacks just like in the default model. + + sapp_run() behaves differently across platforms: + + - on some platforms, sapp_run() will return when the application quits + - on other platforms, sapp_run() will never return, even when the + application quits (the operating system is free to simply terminate + the application at any time) + - on Emscripten specifically, sapp_run() will return immediately while + the frame callback keeps being called + + This different behaviour of sapp_run() essentially means that there shouldn't + be any code *after* sapp_run(), because that may either never be called, or in + case of Emscripten will be called at an unexpected time (at application start). + + An application also should not depend on the cleanup-callback being called + when cross-platform compatibility is required. + + Since sapp_run() returns immediately on Emscripten you shouldn't activate + the 'EXIT_RUNTIME' linker option (this is disabled by default when compiling + for the browser target), since the C/C++ exit runtime would be called immediately at + application start, causing any global objects to be destroyed and global + variables to be zeroed. + + WINDOWS CONSOLE OUTPUT + ====================== + On Windows, regular windowed applications don't show any stdout/stderr text + output, which can be a bit of a hassle for printf() debugging or generally + logging text to the console. Also, console output by default uses a local + codepage setting and thus international UTF-8 encoded text is printed + as garbage. + + To help with these issues, sokol_app.h can be configured at startup + via the following Windows-specific sapp_desc flags: + + sapp_desc.win32.console_utf8 (default: false) + When set to true, the output console codepage will be switched + to UTF-8 (and restored to the original codepage on exit) + + sapp_desc.win32.console_attach (default: false) + When set to true, stdout and stderr will be attached to the + console of the parent process (if the parent process actually + has a console). This means that if the application was started + in a command line window, stdout and stderr output will be printed + to the terminal, just like a regular command line program. But if + the application is started via double-click, it will behave like + a regular UI application, and stdout/stderr will not be visible. + + sapp_desc.win32.console_create (default: false) + When set to true, a new console window will be created and + stdout/stderr will be redirected to that console window. It + doesn't matter if the application is started from the command + line or via double-click. + + NOTE: setting both win32.console_attach and win32.console_create + to true also makes sense and has the effect that output + will appear in the existing terminal when started from the cmdline, and + otherwise (when started via double-click) will open a console window. + + MEMORY ALLOCATION OVERRIDE + ========================== + You can override the memory allocation functions at initialization time + like this: + + void* my_alloc(size_t size, void* user_data) { + return malloc(size); + } + + void my_free(void* ptr, void* user_data) { + free(ptr); + } + + sapp_desc sokol_main(int argc, char* argv[]) { + return (sapp_desc){ + // ... + .allocator = { + .alloc_fn = my_alloc, + .free_fn = my_free, + .user_data = ..., + } + }; + } + + If no overrides are provided, malloc and free will be used. + + This only affects memory allocation calls done by sokol_app.h + itself though, not any allocations in OS libraries. + + + ERROR REPORTING AND LOGGING + =========================== + To get any logging information at all you need to provide a logging callback in the setup call + the easiest way is to use sokol_log.h: + + #include "sokol_log.h" + + sapp_desc sokol_main(int argc, char* argv[]) { + return (sapp_desc) { + ... + .logger.func = slog_func, + }; + } + + To override logging with your own callback, first write a logging function like this: + + void my_log(const char* tag, // e.g. 'sapp' + uint32_t log_level, // 0=panic, 1=error, 2=warn, 3=info + uint32_t log_item_id, // SAPP_LOGITEM_* + const char* message_or_null, // a message string, may be nullptr in release mode + uint32_t line_nr, // line number in sokol_app.h + const char* filename_or_null, // source filename, may be nullptr in release mode + void* user_data) + { + ... + } + + ...and then setup sokol-app like this: + + sapp_desc sokol_main(int argc, char* argv[]) { + return (sapp_desc) { + ... + .logger = { + .func = my_log, + .user_data = my_user_data, + } + }; + } + + The provided logging function must be reentrant (e.g. be callable from + different threads). + + If you don't want to provide your own custom logger it is highly recommended to use + the standard logger in sokol_log.h instead, otherwise you won't see any warnings or + errors. + + TEMP NOTE DUMP + ============== + - sapp_desc needs a bool whether to initialize depth-stencil surface + - the Android implementation calls cleanup_cb() and destroys the egl context in onDestroy + at the latest but should do it earlier, in onStop, as an app is "killable" after onStop + on Android Honeycomb and later (it can't be done at the moment as the app may be started + again after onStop and the sokol lifecycle does not yet handle context teardown/bringup) + + + LICENSE + ======= + zlib/libpng license + + Copyright (c) 2018 Andre Weissflog + + This software is provided 'as-is', without any express or implied warranty. + In no event will the authors be held liable for any damages arising from the + use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software in a + product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source + distribution. +*/ +#define SOKOL_APP_INCLUDED (1) +#include // size_t +#include +#include + +#if defined(SOKOL_API_DECL) && !defined(SOKOL_APP_API_DECL) +#define SOKOL_APP_API_DECL SOKOL_API_DECL +#endif +#ifndef SOKOL_APP_API_DECL +#if defined(_WIN32) && defined(SOKOL_DLL) && defined(SOKOL_APP_IMPL) +#define SOKOL_APP_API_DECL __declspec(dllexport) +#elif defined(_WIN32) && defined(SOKOL_DLL) +#define SOKOL_APP_API_DECL __declspec(dllimport) +#else +#define SOKOL_APP_API_DECL extern +#endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* misc constants */ +enum { + SAPP_MAX_TOUCHPOINTS = 8, + SAPP_MAX_MOUSEBUTTONS = 3, + SAPP_MAX_KEYCODES = 512, + SAPP_MAX_ICONIMAGES = 8, +}; + +/* + sapp_event_type + + The type of event that's passed to the event handler callback + in the sapp_event.type field. These are not just "traditional" + input events, but also notify the application about state changes + or other user-invoked actions. +*/ +typedef enum sapp_event_type { + SAPP_EVENTTYPE_INVALID, + SAPP_EVENTTYPE_KEY_DOWN, + SAPP_EVENTTYPE_KEY_UP, + SAPP_EVENTTYPE_CHAR, + SAPP_EVENTTYPE_MOUSE_DOWN, + SAPP_EVENTTYPE_MOUSE_UP, + SAPP_EVENTTYPE_MOUSE_SCROLL, + SAPP_EVENTTYPE_MOUSE_MOVE, + SAPP_EVENTTYPE_MOUSE_ENTER, + SAPP_EVENTTYPE_MOUSE_LEAVE, + SAPP_EVENTTYPE_TOUCHES_BEGAN, + SAPP_EVENTTYPE_TOUCHES_MOVED, + SAPP_EVENTTYPE_TOUCHES_ENDED, + SAPP_EVENTTYPE_TOUCHES_CANCELLED, + SAPP_EVENTTYPE_RESIZED, + SAPP_EVENTTYPE_ICONIFIED, + SAPP_EVENTTYPE_RESTORED, + SAPP_EVENTTYPE_FOCUSED, + SAPP_EVENTTYPE_UNFOCUSED, + SAPP_EVENTTYPE_SUSPENDED, + SAPP_EVENTTYPE_RESUMED, + SAPP_EVENTTYPE_QUIT_REQUESTED, + SAPP_EVENTTYPE_CLIPBOARD_PASTED, + SAPP_EVENTTYPE_FILES_DROPPED, + _SAPP_EVENTTYPE_NUM, + _SAPP_EVENTTYPE_FORCE_U32 = 0x7FFFFFFF +} sapp_event_type; + +/* + sapp_keycode + + The 'virtual keycode' of a KEY_DOWN or KEY_UP event in the + struct field sapp_event.key_code. + + Note that the keycode values are identical with GLFW. +*/ +typedef enum sapp_keycode { + SAPP_KEYCODE_INVALID = 0, + SAPP_KEYCODE_SPACE = 32, + SAPP_KEYCODE_APOSTROPHE = 39, /* ' */ + SAPP_KEYCODE_COMMA = 44, /* , */ + SAPP_KEYCODE_MINUS = 45, /* - */ + SAPP_KEYCODE_PERIOD = 46, /* . */ + SAPP_KEYCODE_SLASH = 47, /* / */ + SAPP_KEYCODE_0 = 48, + SAPP_KEYCODE_1 = 49, + SAPP_KEYCODE_2 = 50, + SAPP_KEYCODE_3 = 51, + SAPP_KEYCODE_4 = 52, + SAPP_KEYCODE_5 = 53, + SAPP_KEYCODE_6 = 54, + SAPP_KEYCODE_7 = 55, + SAPP_KEYCODE_8 = 56, + SAPP_KEYCODE_9 = 57, + SAPP_KEYCODE_SEMICOLON = 59, /* ; */ + SAPP_KEYCODE_EQUAL = 61, /* = */ + SAPP_KEYCODE_A = 65, + SAPP_KEYCODE_B = 66, + SAPP_KEYCODE_C = 67, + SAPP_KEYCODE_D = 68, + SAPP_KEYCODE_E = 69, + SAPP_KEYCODE_F = 70, + SAPP_KEYCODE_G = 71, + SAPP_KEYCODE_H = 72, + SAPP_KEYCODE_I = 73, + SAPP_KEYCODE_J = 74, + SAPP_KEYCODE_K = 75, + SAPP_KEYCODE_L = 76, + SAPP_KEYCODE_M = 77, + SAPP_KEYCODE_N = 78, + SAPP_KEYCODE_O = 79, + SAPP_KEYCODE_P = 80, + SAPP_KEYCODE_Q = 81, + SAPP_KEYCODE_R = 82, + SAPP_KEYCODE_S = 83, + SAPP_KEYCODE_T = 84, + SAPP_KEYCODE_U = 85, + SAPP_KEYCODE_V = 86, + SAPP_KEYCODE_W = 87, + SAPP_KEYCODE_X = 88, + SAPP_KEYCODE_Y = 89, + SAPP_KEYCODE_Z = 90, + SAPP_KEYCODE_LEFT_BRACKET = 91, /* [ */ + SAPP_KEYCODE_BACKSLASH = 92, /* \ */ + SAPP_KEYCODE_RIGHT_BRACKET = 93, /* ] */ + SAPP_KEYCODE_GRAVE_ACCENT = 96, /* ` */ + SAPP_KEYCODE_WORLD_1 = 161, /* non-US #1 */ + SAPP_KEYCODE_WORLD_2 = 162, /* non-US #2 */ + SAPP_KEYCODE_ESCAPE = 256, + SAPP_KEYCODE_ENTER = 257, + SAPP_KEYCODE_TAB = 258, + SAPP_KEYCODE_BACKSPACE = 259, + SAPP_KEYCODE_INSERT = 260, + SAPP_KEYCODE_DELETE = 261, + SAPP_KEYCODE_RIGHT = 262, + SAPP_KEYCODE_LEFT = 263, + SAPP_KEYCODE_DOWN = 264, + SAPP_KEYCODE_UP = 265, + SAPP_KEYCODE_PAGE_UP = 266, + SAPP_KEYCODE_PAGE_DOWN = 267, + SAPP_KEYCODE_HOME = 268, + SAPP_KEYCODE_END = 269, + SAPP_KEYCODE_CAPS_LOCK = 280, + SAPP_KEYCODE_SCROLL_LOCK = 281, + SAPP_KEYCODE_NUM_LOCK = 282, + SAPP_KEYCODE_PRINT_SCREEN = 283, + SAPP_KEYCODE_PAUSE = 284, + SAPP_KEYCODE_F1 = 290, + SAPP_KEYCODE_F2 = 291, + SAPP_KEYCODE_F3 = 292, + SAPP_KEYCODE_F4 = 293, + SAPP_KEYCODE_F5 = 294, + SAPP_KEYCODE_F6 = 295, + SAPP_KEYCODE_F7 = 296, + SAPP_KEYCODE_F8 = 297, + SAPP_KEYCODE_F9 = 298, + SAPP_KEYCODE_F10 = 299, + SAPP_KEYCODE_F11 = 300, + SAPP_KEYCODE_F12 = 301, + SAPP_KEYCODE_F13 = 302, + SAPP_KEYCODE_F14 = 303, + SAPP_KEYCODE_F15 = 304, + SAPP_KEYCODE_F16 = 305, + SAPP_KEYCODE_F17 = 306, + SAPP_KEYCODE_F18 = 307, + SAPP_KEYCODE_F19 = 308, + SAPP_KEYCODE_F20 = 309, + SAPP_KEYCODE_F21 = 310, + SAPP_KEYCODE_F22 = 311, + SAPP_KEYCODE_F23 = 312, + SAPP_KEYCODE_F24 = 313, + SAPP_KEYCODE_F25 = 314, + SAPP_KEYCODE_KP_0 = 320, + SAPP_KEYCODE_KP_1 = 321, + SAPP_KEYCODE_KP_2 = 322, + SAPP_KEYCODE_KP_3 = 323, + SAPP_KEYCODE_KP_4 = 324, + SAPP_KEYCODE_KP_5 = 325, + SAPP_KEYCODE_KP_6 = 326, + SAPP_KEYCODE_KP_7 = 327, + SAPP_KEYCODE_KP_8 = 328, + SAPP_KEYCODE_KP_9 = 329, + SAPP_KEYCODE_KP_DECIMAL = 330, + SAPP_KEYCODE_KP_DIVIDE = 331, + SAPP_KEYCODE_KP_MULTIPLY = 332, + SAPP_KEYCODE_KP_SUBTRACT = 333, + SAPP_KEYCODE_KP_ADD = 334, + SAPP_KEYCODE_KP_ENTER = 335, + SAPP_KEYCODE_KP_EQUAL = 336, + SAPP_KEYCODE_LEFT_SHIFT = 340, + SAPP_KEYCODE_LEFT_CONTROL = 341, + SAPP_KEYCODE_LEFT_ALT = 342, + SAPP_KEYCODE_LEFT_SUPER = 343, + SAPP_KEYCODE_RIGHT_SHIFT = 344, + SAPP_KEYCODE_RIGHT_CONTROL = 345, + SAPP_KEYCODE_RIGHT_ALT = 346, + SAPP_KEYCODE_RIGHT_SUPER = 347, + SAPP_KEYCODE_MENU = 348, +} sapp_keycode; + +/* + Android specific 'tool type' enum for touch events. This lets the + application check what type of input device was used for + touch events. + + NOTE: the values must remain in sync with the corresponding + Android SDK type, so don't change those. + + See https://developer.android.com/reference/android/view/MotionEvent#TOOL_TYPE_UNKNOWN +*/ +typedef enum sapp_android_tooltype { + SAPP_ANDROIDTOOLTYPE_UNKNOWN = 0, // TOOL_TYPE_UNKNOWN + SAPP_ANDROIDTOOLTYPE_FINGER = 1, // TOOL_TYPE_FINGER + SAPP_ANDROIDTOOLTYPE_STYLUS = 2, // TOOL_TYPE_STYLUS + SAPP_ANDROIDTOOLTYPE_MOUSE = 3, // TOOL_TYPE_MOUSE +} sapp_android_tooltype; + +/* + sapp_touchpoint + + Describes a single touchpoint in a multitouch event (TOUCHES_BEGAN, + TOUCHES_MOVED, TOUCHES_ENDED). + + Touch points are stored in the nested array sapp_event.touches[], + and the number of touches is stored in sapp_event.num_touches. +*/ +typedef struct sapp_touchpoint { + uintptr_t identifier; + float pos_x; + float pos_y; + sapp_android_tooltype android_tooltype; // only valid on Android + bool changed; +} sapp_touchpoint; + +/* + sapp_mousebutton + + The currently pressed mouse button in the events MOUSE_DOWN + and MOUSE_UP, stored in the struct field sapp_event.mouse_button. +*/ +typedef enum sapp_mousebutton { + SAPP_MOUSEBUTTON_LEFT = 0x0, + SAPP_MOUSEBUTTON_RIGHT = 0x1, + SAPP_MOUSEBUTTON_MIDDLE = 0x2, + SAPP_MOUSEBUTTON_INVALID = 0x100, +} sapp_mousebutton; + +/* + These are currently pressed modifier keys (and mouse buttons) which are + passed in the event struct field sapp_event.modifiers. +*/ +enum { + SAPP_MODIFIER_SHIFT = 0x1, // left or right shift key + SAPP_MODIFIER_CTRL = 0x2, // left or right control key + SAPP_MODIFIER_ALT = 0x4, // left or right alt key + SAPP_MODIFIER_SUPER = 0x8, // left or right 'super' key + SAPP_MODIFIER_LMB = 0x100, // left mouse button + SAPP_MODIFIER_RMB = 0x200, // right mouse button + SAPP_MODIFIER_MMB = 0x400, // middle mouse button +}; + +/* + sapp_event + + This is an all-in-one event struct passed to the event handler + user callback function. Note that it depends on the event + type what struct fields actually contain useful values, so you + should first check the event type before reading other struct + fields. +*/ +typedef struct sapp_event { + uint64_t frame_count; // current frame counter, always valid, useful for checking if two events were issued in the same frame + sapp_event_type type; // the event type, always valid + sapp_keycode key_code; // the virtual key code, only valid in KEY_UP, KEY_DOWN + uint32_t char_code; // the UTF-32 character code, only valid in CHAR events + bool key_repeat; // true if this is a key-repeat event, valid in KEY_UP, KEY_DOWN and CHAR + uint32_t modifiers; // current modifier keys, valid in all key-, char- and mouse-events + sapp_mousebutton mouse_button; // mouse button that was pressed or released, valid in MOUSE_DOWN, MOUSE_UP + float mouse_x; // current horizontal mouse position in pixels, always valid except during mouse lock + float mouse_y; // current vertical mouse position in pixels, always valid except during mouse lock + float mouse_dx; // relative horizontal mouse movement since last frame, always valid + float mouse_dy; // relative vertical mouse movement since last frame, always valid + float scroll_x; // horizontal mouse wheel scroll distance, valid in MOUSE_SCROLL events + float scroll_y; // vertical mouse wheel scroll distance, valid in MOUSE_SCROLL events + int num_touches; // number of valid items in the touches[] array + sapp_touchpoint touches[SAPP_MAX_TOUCHPOINTS]; // current touch points, valid in TOUCHES_BEGIN, TOUCHES_MOVED, TOUCHES_ENDED + int window_width; // current window- and framebuffer sizes in pixels, always valid + int window_height; + int framebuffer_width; // = window_width * dpi_scale + int framebuffer_height; // = window_height * dpi_scale +} sapp_event; + +/* + sg_range + + A general pointer/size-pair struct and constructor macros for passing binary blobs + into sokol_app.h. +*/ +typedef struct sapp_range { + const void* ptr; + size_t size; +} sapp_range; +// disabling this for every includer isn't great, but the warnings are also quite pointless +#if defined(_MSC_VER) +#pragma warning(disable:4221) /* /W4 only: nonstandard extension used: 'x': cannot be initialized using address of automatic variable 'y' */ +#pragma warning(disable:4204) /* VS2015: nonstandard extension used: non-constant aggregate initializer */ +#endif +#if defined(__cplusplus) +#define SAPP_RANGE(x) sapp_range{ &x, sizeof(x) } +#else +#define SAPP_RANGE(x) (sapp_range){ &x, sizeof(x) } +#endif + +/* + sapp_image_desc + + This is used to describe image data to sokol_app.h (window icons and cursor images). + + The pixel format is RGBA8. + + cursor_hotspot_x and _y are used only for cursors, to define which pixel + of the image should be aligned with the mouse position. +*/ +typedef struct sapp_image_desc { + int width; + int height; + int cursor_hotspot_x; + int cursor_hotspot_y; + sapp_range pixels; +} sapp_image_desc; + +/* + sapp_icon_desc + + An icon description structure for use in sapp_desc.icon and + sapp_set_icon(). + + When setting a custom image, the application can provide a number of + candidates differing in size, and sokol_app.h will pick the image(s) + closest to the size expected by the platform's window system. + + To set sokol-app's default icon, set .sokol_default to true. + + Otherwise provide candidate images of different sizes in the + images[] array. + + If both the sokol_default flag is set to true, any image candidates + will be ignored and the sokol_app.h default icon will be set. +*/ +typedef struct sapp_icon_desc { + bool sokol_default; + sapp_image_desc images[SAPP_MAX_ICONIMAGES]; +} sapp_icon_desc; + +/* + sapp_allocator + + Used in sapp_desc to provide custom memory-alloc and -free functions + to sokol_app.h. If memory management should be overridden, both the + alloc_fn and free_fn function must be provided (e.g. it's not valid to + override one function but not the other). +*/ +typedef struct sapp_allocator { + void* (*alloc_fn)(size_t size, void* user_data); + void (*free_fn)(void* ptr, void* user_data); + void* user_data; +} sapp_allocator; + +/* + sapp_log_item + + Log items are defined via X-Macros and expanded to an enum + 'sapp_log_item', and in debug mode to corresponding + human readable error messages. +*/ +#define _SAPP_LOG_ITEMS \ + _SAPP_LOGITEM_XMACRO(OK, "Ok") \ + _SAPP_LOGITEM_XMACRO(MALLOC_FAILED, "memory allocation failed") \ + _SAPP_LOGITEM_XMACRO(MACOS_INVALID_NSOPENGL_PROFILE, "macos: invalid NSOpenGLProfile (valid choices are 1.0 and 4.1)") \ + _SAPP_LOGITEM_XMACRO(METAL_CREATE_SWAPCHAIN_DEPTH_TEXTURE_FAILED, "metal: failed to create swapchain depth-buffer texture") \ + _SAPP_LOGITEM_XMACRO(METAL_CREATE_SWAPCHAIN_MSAA_TEXTURE_FAILED, "metal: failed to create swapchain msaa texture") \ + _SAPP_LOGITEM_XMACRO(WIN32_LOAD_OPENGL32_DLL_FAILED, "failed loading opengl32.dll") \ + _SAPP_LOGITEM_XMACRO(WIN32_CREATE_HELPER_WINDOW_FAILED, "failed to create helper window") \ + _SAPP_LOGITEM_XMACRO(WIN32_HELPER_WINDOW_GETDC_FAILED, "failed to get helper window DC") \ + _SAPP_LOGITEM_XMACRO(WIN32_DUMMY_CONTEXT_SET_PIXELFORMAT_FAILED, "failed to set pixel format for dummy GL context") \ + _SAPP_LOGITEM_XMACRO(WIN32_CREATE_DUMMY_CONTEXT_FAILED, "failed to create dummy GL context") \ + _SAPP_LOGITEM_XMACRO(WIN32_DUMMY_CONTEXT_MAKE_CURRENT_FAILED, "failed to make dummy GL context current") \ + _SAPP_LOGITEM_XMACRO(WIN32_GET_PIXELFORMAT_ATTRIB_FAILED, "failed to get WGL pixel format attribute") \ + _SAPP_LOGITEM_XMACRO(WIN32_WGL_FIND_PIXELFORMAT_FAILED, "failed to find matching WGL pixel format") \ + _SAPP_LOGITEM_XMACRO(WIN32_WGL_DESCRIBE_PIXELFORMAT_FAILED, "failed to get pixel format descriptor") \ + _SAPP_LOGITEM_XMACRO(WIN32_WGL_SET_PIXELFORMAT_FAILED, "failed to set selected pixel format") \ + _SAPP_LOGITEM_XMACRO(WIN32_WGL_ARB_CREATE_CONTEXT_REQUIRED, "ARB_create_context required") \ + _SAPP_LOGITEM_XMACRO(WIN32_WGL_ARB_CREATE_CONTEXT_PROFILE_REQUIRED, "ARB_create_context_profile required") \ + _SAPP_LOGITEM_XMACRO(WIN32_WGL_OPENGL_VERSION_NOT_SUPPORTED, "requested OpenGL version not supported by GL driver (ERROR_INVALID_VERSION_ARB)") \ + _SAPP_LOGITEM_XMACRO(WIN32_WGL_OPENGL_PROFILE_NOT_SUPPORTED, "requested OpenGL profile not support by GL driver (ERROR_INVALID_PROFILE_ARB)") \ + _SAPP_LOGITEM_XMACRO(WIN32_WGL_INCOMPATIBLE_DEVICE_CONTEXT, "CreateContextAttribsARB failed with ERROR_INCOMPATIBLE_DEVICE_CONTEXTS_ARB") \ + _SAPP_LOGITEM_XMACRO(WIN32_WGL_CREATE_CONTEXT_ATTRIBS_FAILED_OTHER, "CreateContextAttribsARB failed for other reason") \ + _SAPP_LOGITEM_XMACRO(WIN32_D3D11_CREATE_DEVICE_AND_SWAPCHAIN_WITH_DEBUG_FAILED, "D3D11CreateDeviceAndSwapChain() with D3D11_CREATE_DEVICE_DEBUG failed, retrying without debug flag.") \ + _SAPP_LOGITEM_XMACRO(WIN32_D3D11_GET_IDXGIFACTORY_FAILED, "could not obtain IDXGIFactory object") \ + _SAPP_LOGITEM_XMACRO(WIN32_D3D11_GET_IDXGIADAPTER_FAILED, "could not obtain IDXGIAdapter object") \ + _SAPP_LOGITEM_XMACRO(WIN32_D3D11_QUERY_INTERFACE_IDXGIDEVICE1_FAILED, "could not obtain IDXGIDevice1 interface") \ + _SAPP_LOGITEM_XMACRO(WIN32_REGISTER_RAW_INPUT_DEVICES_FAILED_MOUSE_LOCK, "RegisterRawInputDevices() failed (on mouse lock)") \ + _SAPP_LOGITEM_XMACRO(WIN32_REGISTER_RAW_INPUT_DEVICES_FAILED_MOUSE_UNLOCK, "RegisterRawInputDevices() failed (on mouse unlock)") \ + _SAPP_LOGITEM_XMACRO(WIN32_GET_RAW_INPUT_DATA_FAILED, "GetRawInputData() failed") \ + _SAPP_LOGITEM_XMACRO(WIN32_DESTROYICON_FOR_CURSOR_FAILED, "DestroyIcon() for a cursor image failed") \ + _SAPP_LOGITEM_XMACRO(LINUX_GLX_LOAD_LIBGL_FAILED, "failed to load libGL") \ + _SAPP_LOGITEM_XMACRO(LINUX_GLX_LOAD_ENTRY_POINTS_FAILED, "failed to load GLX entry points") \ + _SAPP_LOGITEM_XMACRO(LINUX_GLX_EXTENSION_NOT_FOUND, "GLX extension not found") \ + _SAPP_LOGITEM_XMACRO(LINUX_GLX_QUERY_VERSION_FAILED, "failed to query GLX version") \ + _SAPP_LOGITEM_XMACRO(LINUX_GLX_VERSION_TOO_LOW, "GLX version too low (need at least 1.3)") \ + _SAPP_LOGITEM_XMACRO(LINUX_GLX_NO_GLXFBCONFIGS, "glXGetFBConfigs() returned no configs") \ + _SAPP_LOGITEM_XMACRO(LINUX_GLX_NO_SUITABLE_GLXFBCONFIG, "failed to find a suitable GLXFBConfig") \ + _SAPP_LOGITEM_XMACRO(LINUX_GLX_GET_VISUAL_FROM_FBCONFIG_FAILED, "glXGetVisualFromFBConfig failed") \ + _SAPP_LOGITEM_XMACRO(LINUX_GLX_REQUIRED_EXTENSIONS_MISSING, "GLX extensions ARB_create_context and ARB_create_context_profile missing") \ + _SAPP_LOGITEM_XMACRO(LINUX_GLX_CREATE_CONTEXT_FAILED, "Failed to create GL context via glXCreateContextAttribsARB") \ + _SAPP_LOGITEM_XMACRO(LINUX_GLX_CREATE_WINDOW_FAILED, "glXCreateWindow() failed") \ + _SAPP_LOGITEM_XMACRO(LINUX_X11_CREATE_WINDOW_FAILED, "XCreateWindow() failed") \ + _SAPP_LOGITEM_XMACRO(LINUX_EGL_BIND_OPENGL_API_FAILED, "eglBindAPI(EGL_OPENGL_API) failed") \ + _SAPP_LOGITEM_XMACRO(LINUX_EGL_BIND_OPENGL_ES_API_FAILED, "eglBindAPI(EGL_OPENGL_ES_API) failed") \ + _SAPP_LOGITEM_XMACRO(LINUX_EGL_GET_DISPLAY_FAILED, "eglGetDisplay() failed") \ + _SAPP_LOGITEM_XMACRO(LINUX_EGL_INITIALIZE_FAILED, "eglInitialize() failed") \ + _SAPP_LOGITEM_XMACRO(LINUX_EGL_NO_CONFIGS, "eglChooseConfig() returned no configs") \ + _SAPP_LOGITEM_XMACRO(LINUX_EGL_NO_NATIVE_VISUAL, "eglGetConfigAttrib() for EGL_NATIVE_VISUAL_ID failed") \ + _SAPP_LOGITEM_XMACRO(LINUX_EGL_GET_VISUAL_INFO_FAILED, "XGetVisualInfo() failed") \ + _SAPP_LOGITEM_XMACRO(LINUX_EGL_CREATE_WINDOW_SURFACE_FAILED, "eglCreateWindowSurface() failed") \ + _SAPP_LOGITEM_XMACRO(LINUX_EGL_CREATE_CONTEXT_FAILED, "eglCreateContext() failed") \ + _SAPP_LOGITEM_XMACRO(LINUX_EGL_MAKE_CURRENT_FAILED, "eglMakeCurrent() failed") \ + _SAPP_LOGITEM_XMACRO(LINUX_X11_OPEN_DISPLAY_FAILED, "XOpenDisplay() failed") \ + _SAPP_LOGITEM_XMACRO(LINUX_X11_QUERY_SYSTEM_DPI_FAILED, "failed to query system dpi value, assuming default 96.0") \ + _SAPP_LOGITEM_XMACRO(LINUX_X11_DROPPED_FILE_URI_WRONG_SCHEME, "dropped file URL doesn't start with 'file://'") \ + _SAPP_LOGITEM_XMACRO(LINUX_X11_FAILED_TO_BECOME_OWNER_OF_CLIPBOARD, "X11: Failed to become owner of clipboard selection") \ + _SAPP_LOGITEM_XMACRO(ANDROID_UNSUPPORTED_INPUT_EVENT_INPUT_CB, "unsupported input event encountered in _sapp_android_input_cb()") \ + _SAPP_LOGITEM_XMACRO(ANDROID_UNSUPPORTED_INPUT_EVENT_MAIN_CB, "unsupported input event encountered in _sapp_android_main_cb()") \ + _SAPP_LOGITEM_XMACRO(ANDROID_READ_MSG_FAILED, "failed to read message in _sapp_android_main_cb()") \ + _SAPP_LOGITEM_XMACRO(ANDROID_WRITE_MSG_FAILED, "failed to write message in _sapp_android_msg") \ + _SAPP_LOGITEM_XMACRO(ANDROID_MSG_CREATE, "MSG_CREATE") \ + _SAPP_LOGITEM_XMACRO(ANDROID_MSG_RESUME, "MSG_RESUME") \ + _SAPP_LOGITEM_XMACRO(ANDROID_MSG_PAUSE, "MSG_PAUSE") \ + _SAPP_LOGITEM_XMACRO(ANDROID_MSG_FOCUS, "MSG_FOCUS") \ + _SAPP_LOGITEM_XMACRO(ANDROID_MSG_NO_FOCUS, "MSG_NO_FOCUS") \ + _SAPP_LOGITEM_XMACRO(ANDROID_MSG_SET_NATIVE_WINDOW, "MSG_SET_NATIVE_WINDOW") \ + _SAPP_LOGITEM_XMACRO(ANDROID_MSG_SET_INPUT_QUEUE, "MSG_SET_INPUT_QUEUE") \ + _SAPP_LOGITEM_XMACRO(ANDROID_MSG_DESTROY, "MSG_DESTROY") \ + _SAPP_LOGITEM_XMACRO(ANDROID_UNKNOWN_MSG, "unknown msg type received") \ + _SAPP_LOGITEM_XMACRO(ANDROID_LOOP_THREAD_STARTED, "loop thread started") \ + _SAPP_LOGITEM_XMACRO(ANDROID_LOOP_THREAD_DONE, "loop thread done") \ + _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONSTART, "NativeActivity onStart()") \ + _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONRESUME, "NativeActivity onResume") \ + _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONSAVEINSTANCESTATE, "NativeActivity onSaveInstanceState") \ + _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONWINDOWFOCUSCHANGED, "NativeActivity onWindowFocusChanged") \ + _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONPAUSE, "NativeActivity onPause") \ + _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONSTOP, "NativeActivity onStop()") \ + _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONNATIVEWINDOWCREATED, "NativeActivity onNativeWindowCreated") \ + _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONNATIVEWINDOWDESTROYED, "NativeActivity onNativeWindowDestroyed") \ + _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONINPUTQUEUECREATED, "NativeActivity onInputQueueCreated") \ + _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONINPUTQUEUEDESTROYED, "NativeActivity onInputQueueDestroyed") \ + _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONCONFIGURATIONCHANGED, "NativeActivity onConfigurationChanged") \ + _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONLOWMEMORY, "NativeActivity onLowMemory") \ + _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONDESTROY, "NativeActivity onDestroy") \ + _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_DONE, "NativeActivity done") \ + _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_ONCREATE, "NativeActivity onCreate") \ + _SAPP_LOGITEM_XMACRO(ANDROID_CREATE_THREAD_PIPE_FAILED, "failed to create thread pipe") \ + _SAPP_LOGITEM_XMACRO(ANDROID_NATIVE_ACTIVITY_CREATE_SUCCESS, "NativeActivity successfully created") \ + _SAPP_LOGITEM_XMACRO(ANDROID_CHOREOGRAPHER_ENABLED, "Choreographer frame loop enabled") \ + _SAPP_LOGITEM_XMACRO(ANDROID_CHOREOGRAPHER_UNAVAILABLE, "Choreographer unavailable, using poll loop") \ + _SAPP_LOGITEM_XMACRO(WGPU_DEVICE_LOST, "wgpu: device lost") \ + _SAPP_LOGITEM_XMACRO(WGPU_DEVICE_LOG, "wgpu: device log") \ + _SAPP_LOGITEM_XMACRO(WGPU_DEVICE_UNCAPTURED_ERROR, "wgpu: uncaptured error") \ + _SAPP_LOGITEM_XMACRO(WGPU_SWAPCHAIN_CREATE_SURFACE_FAILED, "wgpu: failed to create surface for swapchain") \ + _SAPP_LOGITEM_XMACRO(WGPU_SWAPCHAIN_SURFACE_GET_CAPABILITIES_FAILED, "wgpu: wgpuSurfaceGetCapabilities failed") \ + _SAPP_LOGITEM_XMACRO(WGPU_SWAPCHAIN_CREATE_DEPTH_STENCIL_TEXTURE_FAILED, "wgpu: failed to create depth-stencil texture for swapchain") \ + _SAPP_LOGITEM_XMACRO(WGPU_SWAPCHAIN_CREATE_DEPTH_STENCIL_VIEW_FAILED, "wgpu: failed to create view object for swapchain depth-stencil texture") \ + _SAPP_LOGITEM_XMACRO(WGPU_SWAPCHAIN_CREATE_MSAA_TEXTURE_FAILED, "wgpu: failed to create msaa texture for swapchain") \ + _SAPP_LOGITEM_XMACRO(WGPU_SWAPCHAIN_CREATE_MSAA_VIEW_FAILED, "wgpu: failed to create view object for swapchain msaa texture") \ + _SAPP_LOGITEM_XMACRO(WGPU_SWAPCHAIN_GETCURRENTTEXTURE_FAILED, "wgpu: wgpuSurfaceGetCurrentTexture() failed") \ + _SAPP_LOGITEM_XMACRO(WGPU_REQUEST_DEVICE_STATUS_ERROR, "wgpu: requesting device failed with status 'error'") \ + _SAPP_LOGITEM_XMACRO(WGPU_REQUEST_DEVICE_STATUS_UNKNOWN, "wgpu: requesting device failed with status 'unknown'") \ + _SAPP_LOGITEM_XMACRO(WGPU_REQUEST_ADAPTER_STATUS_UNAVAILABLE, "wgpu: requesting adapter failed with 'unavailable'") \ + _SAPP_LOGITEM_XMACRO(WGPU_REQUEST_ADAPTER_STATUS_ERROR, "wgpu: requesting adapter failed with status 'error'") \ + _SAPP_LOGITEM_XMACRO(WGPU_REQUEST_ADAPTER_STATUS_UNKNOWN, "wgpu: requesting adapter failed with status 'unknown'") \ + _SAPP_LOGITEM_XMACRO(WGPU_CREATE_INSTANCE_FAILED, "wgpu: failed to create instance") \ + _SAPP_LOGITEM_XMACRO(VULKAN_REQUIRED_INSTANCE_EXTENSION_FUNCTION_MISSING, "vulkan: could not lookup a required instance extension function pointer") \ + _SAPP_LOGITEM_XMACRO(VULKAN_ALLOC_DEVICE_MEMORY_NO_SUITABLE_MEMORY_TYPE, "vulkan: could not find suitable memory type") \ + _SAPP_LOGITEM_XMACRO(VULKAN_ALLOCATE_MEMORY_FAILED, "vulkan: vkAllocateMemory() failed!") \ + _SAPP_LOGITEM_XMACRO(VULKAN_CREATE_INSTANCE_FAILED, "vulkan: vkCreateInstance failed") \ + _SAPP_LOGITEM_XMACRO(VULKAN_ENUMERATE_PHYSICAL_DEVICES_FAILED, "vulkan: vkEnumeratePhysicalDevices failed") \ + _SAPP_LOGITEM_XMACRO(VULKAN_NO_PHYSICAL_DEVICES_FOUND, "vulkan: vkEnumeratePhysicalDevices return no devices") \ + _SAPP_LOGITEM_XMACRO(VULKAN_NO_SUITABLE_PHYSICAL_DEVICE_FOUND, "vulkan: no suitable physical device found") \ + _SAPP_LOGITEM_XMACRO(VULKAN_CREATE_DEVICE_FAILED_EXTENSION_NOT_PRESENT, "vulkan: vkCreateDevice failed (extension not present)") \ + _SAPP_LOGITEM_XMACRO(VULKAN_CREATE_DEVICE_FAILED_FEATURE_NOT_PRESENT, "vulkan: vkCreateDevice failed (feature not present)") \ + _SAPP_LOGITEM_XMACRO(VULKAN_CREATE_DEVICE_FAILED_INITIALIZATION_FAILED, "vulkan: vkCreateDevice failed (initialization failed)") \ + _SAPP_LOGITEM_XMACRO(VULKAN_CREATE_DEVICE_FAILED_OTHER, "vulkan: vkCreateDevice failed (other)") \ + _SAPP_LOGITEM_XMACRO(VULKAN_CREATE_SURFACE_FAILED, "vulkan: vkCreate*SurfaceKHR failed") \ + _SAPP_LOGITEM_XMACRO(VULKAN_CREATE_SWAPCHAIN_FAILED, "vulkan: vkCreateSwapchainKHR failed") \ + _SAPP_LOGITEM_XMACRO(VULKAN_SWAPCHAIN_CREATE_IMAGE_VIEW_FAILED, "vulkan: vkCreateImageView for swapchain image failed") \ + _SAPP_LOGITEM_XMACRO(VULKAN_SWAPCHAIN_CREATE_IMAGE_FAILED, "vulkan: vkCreateImage for depth-stencil image failed") \ + _SAPP_LOGITEM_XMACRO(VULKAN_SWAPCHAIN_ALLOC_IMAGE_DEVICE_MEMORY_FAILED, "vulkan: failed to allocate device memory for depth-stencil image") \ + _SAPP_LOGITEM_XMACRO(VULKAN_SWAPCHAIN_BIND_IMAGE_MEMORY_FAILED, "vulkan: vkBindImageMemory() for depth-stencil image failed") \ + _SAPP_LOGITEM_XMACRO(VULKAN_ACQUIRE_NEXT_IMAGE_FAILED, "vulkan: vkAcquireNextImageKHR failed") \ + _SAPP_LOGITEM_XMACRO(VULKAN_QUEUE_PRESENT_FAILED, "vulkan: vkQueuePresentKHR failed") \ + _SAPP_LOGITEM_XMACRO(IMAGE_DATA_SIZE_MISMATCH, "image data size mismatch (must be width*height*4 bytes)") \ + _SAPP_LOGITEM_XMACRO(DROPPED_FILE_PATH_TOO_LONG, "dropped file path too long (sapp_desc.max_dropped_filed_path_length)") \ + _SAPP_LOGITEM_XMACRO(CLIPBOARD_STRING_TOO_BIG, "clipboard string didn't fit into clipboard buffer") \ + +#define _SAPP_LOGITEM_XMACRO(item,msg) SAPP_LOGITEM_##item, +typedef enum sapp_log_item { + _SAPP_LOG_ITEMS +} sapp_log_item; +#undef _SAPP_LOGITEM_XMACRO + +/* + sapp_pixel_format + + Defines the pixel format for swapchain surfaces. + + NOTE: when using sokol_gfx.h do not assume that the underlying + values are compatible with sg_pixel_format! + +*/ +typedef enum sapp_pixel_format { + _SAPP_PIXELFORMAT_DEFAULT, + SAPP_PIXELFORMAT_NONE, + SAPP_PIXELFORMAT_RGBA8, + SAPP_PIXELFORMAT_SRGB8A8, + SAPP_PIXELFORMAT_BGRA8, + SAPP_PIXELFORMAT_RGB10A2, // Modified by tettou771 for TrussC: 10-bit color output support + SAPP_PIXELFORMAT_SBGRA8, + SAPP_PIXELFORMAT_DEPTH, + SAPP_PIXELFORMAT_DEPTH_STENCIL, + _SAPP_PIXELFORMAT_FORCE_U32 = 0x7FFFFFFF +} sapp_pixel_format; + +/* + sapp_environment + + Used to provide runtime environment information to the + outside world (like default pixel formats and the backend + 3D API device pointer) via a call to sapp_get_environment(). + + NOTE: when using sokol_gfx.h, don't assume that sapp_environment + is binary compatible with sg_environment! Always use a translation + function like sglue_environment() to populate sg_environment + from sapp_environment! +*/ +typedef struct sapp_environment_defaults { + sapp_pixel_format color_format; + sapp_pixel_format depth_format; + int sample_count; +} sapp_environment_defaults; + +typedef struct sapp_metal_environment { + const void* device; +} sapp_metal_environment; + +typedef struct sapp_d3d11_environment { + const void* device; + const void* device_context; +} sapp_d3d11_environment; + +typedef struct sapp_wgpu_environment { + const void* device; +} sapp_wgpu_environment; + +typedef struct sapp_vulkan_environment { + const void* instance; + const void* physical_device; + const void* device; + const void* queue; + uint32_t queue_family_index; +} sapp_vulkan_environment; + +typedef struct sapp_environment { + sapp_environment_defaults defaults; + sapp_metal_environment metal; + sapp_d3d11_environment d3d11; + sapp_wgpu_environment wgpu; + sapp_vulkan_environment vulkan; +} sapp_environment; + +/* + sapp_swapchain + + Provides swapchain information for the current frame to the outside + world via a call to sapp_get_swapchain(). + + NOTE: sapp_get_swapchain() must be called exactly once per frame since + on some backends it will also acquire the next swapchain image. + + NOTE: when using sokol_gfx.h, don't assume that the sapp_swapchain struct + has the same memory layout as sg_swapchain! Use the sokol_log.h helper + function sglue_swapchain() to translate sapp_swapchain into a + sg_swapchain instead. +*/ +typedef struct sapp_metal_swapchain { + const void* current_drawable; // CAMetalDrawable (NOT MTLDrawable!!!) + const void* depth_stencil_texture; // MTLTexture + const void* msaa_color_texture; // MTLTexture +} sapp_metal_swapchain; + +typedef struct sapp_d3d11_swapchain { + const void* render_view; // ID3D11RenderTargetView + const void* resolve_view; // ID3D11RenderTargetView + const void* depth_stencil_view; // ID3D11DepthStencilView +} sapp_d3d11_swapchain; + +typedef struct sapp_wgpu_swapchain { + const void* render_view; // WGPUTextureView + const void* resolve_view; // WGPUTextureView + const void* depth_stencil_view; // WGPUTextureView +} sapp_wgpu_swapchain; + +typedef struct sapp_vulkan_swapchain { + const void* render_image; // vkImage + const void* render_view; // vkImageView + const void* resolve_image; // vkImage; + const void* resolve_view; // vkImageView + const void* depth_stencil_image; // vkImage + const void* depth_stencil_view; // vkImageView + const void* render_finished_semaphore; // vkSemaphore + const void* present_complete_semaphore; // vkSemaphore +} sapp_vulkan_swapchain; + +typedef struct sapp_gl_swapchain { + uint32_t framebuffer; // GL framebuffer object +} sapp_gl_swapchain; + +typedef struct sapp_swapchain { + int width; + int height; + int sample_count; + sapp_pixel_format color_format; + sapp_pixel_format depth_format; + sapp_metal_swapchain metal; + sapp_d3d11_swapchain d3d11; + sapp_wgpu_swapchain wgpu; + sapp_vulkan_swapchain vulkan; + sapp_gl_swapchain gl; +} sapp_swapchain; + +/* + sapp_logger + + Used in sapp_desc to provide a logging function. Please be aware that + without logging function, sokol-app will be completely silent, e.g. it will + not report errors or warnings. For maximum error verbosity, compile in + debug mode (e.g. NDEBUG *not* defined) and install a logger (for instance + the standard logging function from sokol_log.h). +*/ +typedef struct sapp_logger { + void (*func)( + const char* tag, // always "sapp" + uint32_t log_level, // 0=panic, 1=error, 2=warning, 3=info + uint32_t log_item_id, // SAPP_LOGITEM_* + const char* message_or_null, // a message string, may be nullptr in release mode + uint32_t line_nr, // line number in sokol_app.h + const char* filename_or_null, // source filename, may be nullptr in release mode + void* user_data); + void* user_data; +} sapp_logger; + +/* + sokol-app initialization options, used as return value of sokol_main() + or sapp_run() argument. +*/ +typedef struct sapp_gl_desc { + int major_version; // override GL/GLES major and minor version (defaults: GL4.1 (macOS) or GL4.3, GLES3.1 (Android) or GLES3.0 + int minor_version; +} sapp_gl_desc; + +typedef struct sapp_win32_desc { + bool console_utf8; // if true, set the output console codepage to UTF-8 + bool console_create; // if true, attach stdout/stderr to a new console window + bool console_attach; // if true, attach stdout/stderr to parent process +} sapp_win32_desc; + +typedef struct sapp_html5_desc { + const char* canvas_selector; // css selector of the HTML5 canvas element, default is "#canvas" + bool canvas_resize; // if true, the HTML5 canvas size is set to sapp_desc.width/height, otherwise canvas size is tracked + bool preserve_drawing_buffer; // HTML5 only: whether to preserve default framebuffer content between frames + bool premultiplied_alpha; // HTML5 only: whether the rendered pixels use premultiplied alpha convention + bool ask_leave_site; // initial state of the internal html5_ask_leave_site flag (see sapp_html5_ask_leave_site()) + bool update_document_title; // if true, update the HTML document.title with sapp_desc.window_title + bool bubble_mouse_events; // if true, mouse events will bubble up to the web page + bool bubble_touch_events; // same for touch events + bool bubble_wheel_events; // same for wheel events + bool bubble_key_events; // if true, bubble up *all* key events to browser, not just key events that represent characters + bool bubble_char_events; // if true, bubble up character events to browser + bool use_emsc_set_main_loop; // if true, use emscripten_set_main_loop() instead of emscripten_request_animation_frame_loop() + bool emsc_set_main_loop_simulate_infinite_loop; // this will be passed as the simulate_infinite_loop arg to emscripten_set_main_loop() +} sapp_html5_desc; + +typedef struct sapp_ios_desc { + bool keyboard_resizes_canvas; // if true, showing the iOS keyboard shrinks the canvas +} sapp_ios_desc; + +typedef struct sapp_desc { + void (*init_cb)(void); // these are the user-provided callbacks without user data + void (*frame_cb)(void); + void (*cleanup_cb)(void); + void (*event_cb)(const sapp_event*); + + void* user_data; // these are the user-provided callbacks with user data + void (*init_userdata_cb)(void*); + void (*frame_userdata_cb)(void*); + void (*cleanup_userdata_cb)(void*); + void (*event_userdata_cb)(const sapp_event*, void*); + + int width; // the preferred width of the window / canvas + int height; // the preferred height of the window / canvas + int sample_count; // MSAA sample count + int swap_interval; // the preferred swap interval (ignored on some platforms) + bool high_dpi; // whether the rendering canvas is full-resolution on HighDPI displays + bool fullscreen; // whether the window should be created in fullscreen mode + bool alpha; // whether the framebuffer should have an alpha channel (ignored on some platforms) + const char* window_title; // the window title as UTF-8 encoded string + bool enable_clipboard; // enable clipboard access, default is false + int clipboard_size; // max size of clipboard content in bytes + bool enable_dragndrop; // enable file dropping (drag'n'drop), default is false + int max_dropped_files; // max number of dropped files to process (default: 1) + int max_dropped_file_path_length; // max length in bytes of a dropped UTF-8 file path (default: 2048) + sapp_icon_desc icon; // the initial window icon to set + sapp_allocator allocator; // optional memory allocation overrides (default: malloc/free) + sapp_logger logger; // logging callback override (default: NO LOGGING!) + + // backend-specific options + sapp_gl_desc gl; + sapp_win32_desc win32; + sapp_html5_desc html5; + sapp_ios_desc ios; +} sapp_desc; + +/* HTML5 specific: request and response structs for + asynchronously loading dropped-file content. +*/ +typedef enum sapp_html5_fetch_error { + SAPP_HTML5_FETCH_ERROR_NO_ERROR, + SAPP_HTML5_FETCH_ERROR_BUFFER_TOO_SMALL, + SAPP_HTML5_FETCH_ERROR_OTHER, +} sapp_html5_fetch_error; + +typedef struct sapp_html5_fetch_response { + bool succeeded; // true if the loading operation has succeeded + sapp_html5_fetch_error error_code; + int file_index; // index of the dropped file (0..sapp_get_num_dropped_filed()-1) + sapp_range data; // pointer and size of the fetched data (data.ptr == buffer.ptr, data.size <= buffer.size) + sapp_range buffer; // the user-provided buffer ptr/size pair (buffer.ptr == data.ptr, buffer.size >= data.size) + void* user_data; // user-provided user data pointer +} sapp_html5_fetch_response; + +typedef struct sapp_html5_fetch_request { + int dropped_file_index; // 0..sapp_get_num_dropped_files()-1 + void (*callback)(const sapp_html5_fetch_response*); // response callback function pointer (required) + sapp_range buffer; // ptr/size of a memory buffer to load the data into + void* user_data; // optional userdata pointer +} sapp_html5_fetch_request; + +/* + sapp_mouse_cursor + + Predefined cursor image definitions, set with sapp_set_mouse_cursor(sapp_mouse_cursor cursor) +*/ +typedef enum sapp_mouse_cursor { + SAPP_MOUSECURSOR_DEFAULT = 0, // equivalent with system default cursor + SAPP_MOUSECURSOR_ARROW, + SAPP_MOUSECURSOR_IBEAM, + SAPP_MOUSECURSOR_CROSSHAIR, + SAPP_MOUSECURSOR_POINTING_HAND, + SAPP_MOUSECURSOR_RESIZE_EW, + SAPP_MOUSECURSOR_RESIZE_NS, + SAPP_MOUSECURSOR_RESIZE_NWSE, + SAPP_MOUSECURSOR_RESIZE_NESW, + SAPP_MOUSECURSOR_RESIZE_ALL, + SAPP_MOUSECURSOR_NOT_ALLOWED, + SAPP_MOUSECURSOR_CUSTOM_0, + SAPP_MOUSECURSOR_CUSTOM_1, + SAPP_MOUSECURSOR_CUSTOM_2, + SAPP_MOUSECURSOR_CUSTOM_3, + SAPP_MOUSECURSOR_CUSTOM_4, + SAPP_MOUSECURSOR_CUSTOM_5, + SAPP_MOUSECURSOR_CUSTOM_6, + SAPP_MOUSECURSOR_CUSTOM_7, + SAPP_MOUSECURSOR_CUSTOM_8, + SAPP_MOUSECURSOR_CUSTOM_9, + SAPP_MOUSECURSOR_CUSTOM_10, + SAPP_MOUSECURSOR_CUSTOM_11, + SAPP_MOUSECURSOR_CUSTOM_12, + SAPP_MOUSECURSOR_CUSTOM_13, + SAPP_MOUSECURSOR_CUSTOM_14, + SAPP_MOUSECURSOR_CUSTOM_15, + _SAPP_MOUSECURSOR_NUM, +} sapp_mouse_cursor; + +/* user-provided functions */ +extern sapp_desc sokol_main(int argc, char* argv[]); + +/* returns true after sokol-app has been initialized */ +SOKOL_APP_API_DECL bool sapp_isvalid(void); +/* returns the current framebuffer width in pixels */ +SOKOL_APP_API_DECL int sapp_width(void); +/* same as sapp_width(), but returns float */ +SOKOL_APP_API_DECL float sapp_widthf(void); +/* returns the current framebuffer height in pixels */ +SOKOL_APP_API_DECL int sapp_height(void); +/* same as sapp_height(), but returns float */ +SOKOL_APP_API_DECL float sapp_heightf(void); +/* get default framebuffer color pixel format */ +SOKOL_APP_API_DECL sapp_pixel_format sapp_color_format(void); +/* get default framebuffer depth pixel format */ +SOKOL_APP_API_DECL sapp_pixel_format sapp_depth_format(void); +/* get default framebuffer sample count */ +SOKOL_APP_API_DECL int sapp_sample_count(void); +/* returns true when high_dpi was requested and actually running in a high-dpi scenario */ +SOKOL_APP_API_DECL bool sapp_high_dpi(void); +/* returns the dpi scaling factor (window pixels to framebuffer pixels) */ +SOKOL_APP_API_DECL float sapp_dpi_scale(void); +/* show or hide the mobile device onscreen keyboard */ +SOKOL_APP_API_DECL void sapp_show_keyboard(bool show); +/* return true if the mobile device onscreen keyboard is currently shown */ +SOKOL_APP_API_DECL bool sapp_keyboard_shown(void); +/* query fullscreen mode */ +SOKOL_APP_API_DECL bool sapp_is_fullscreen(void); +/* toggle fullscreen mode */ +SOKOL_APP_API_DECL void sapp_toggle_fullscreen(void); +/* show or hide the mouse cursor */ +SOKOL_APP_API_DECL void sapp_show_mouse(bool show); +/* show or hide the mouse cursor */ +SOKOL_APP_API_DECL bool sapp_mouse_shown(void); +/* enable/disable mouse-pointer-lock mode */ +SOKOL_APP_API_DECL void sapp_lock_mouse(bool lock); +/* return true if in mouse-pointer-lock mode (this may toggle a few frames later) */ +SOKOL_APP_API_DECL bool sapp_mouse_locked(void); +/* set mouse cursor type */ +SOKOL_APP_API_DECL void sapp_set_mouse_cursor(sapp_mouse_cursor cursor); +/* get current mouse cursor type */ +SOKOL_APP_API_DECL sapp_mouse_cursor sapp_get_mouse_cursor(void); +/* associate a custom mouse cursor image to a sapp_mouse_cursor enum entry */ +SOKOL_APP_API_DECL sapp_mouse_cursor sapp_bind_mouse_cursor_image(sapp_mouse_cursor cursor, const sapp_image_desc* desc); +/* restore the sapp_mouse_cursor enum entry to it's default system appearance */ +SOKOL_APP_API_DECL void sapp_unbind_mouse_cursor_image(sapp_mouse_cursor cursor); +/* return the userdata pointer optionally provided in sapp_desc */ +SOKOL_APP_API_DECL void* sapp_userdata(void); +/* return a copy of the sapp_desc structure */ +SOKOL_APP_API_DECL sapp_desc sapp_query_desc(void); +/* initiate a "soft quit" (sends SAPP_EVENTTYPE_QUIT_REQUESTED) */ +SOKOL_APP_API_DECL void sapp_request_quit(void); +/* cancel a pending quit (when SAPP_EVENTTYPE_QUIT_REQUESTED has been received) */ +SOKOL_APP_API_DECL void sapp_cancel_quit(void); +/* initiate a "hard quit" (quit application without sending SAPP_EVENTTYPE_QUIT_REQUESTED) */ +SOKOL_APP_API_DECL void sapp_quit(void); +/* call from inside event callback to consume the current event (don't forward to platform) */ +SOKOL_APP_API_DECL void sapp_consume_event(void); +/* get the current frame counter (for comparison with sapp_event.frame_count) */ +SOKOL_APP_API_DECL uint64_t sapp_frame_count(void); +/* get an averaged/smoothed frame duration in seconds */ +SOKOL_APP_API_DECL double sapp_frame_duration(void); +/* get 'raw' unfiltered frame duration in seconds */ +SOKOL_APP_API_DECL double sapp_frame_duration_unfiltered(void); +/* write string into clipboard */ +SOKOL_APP_API_DECL void sapp_set_clipboard_string(const char* str); +/* read string from clipboard (usually during SAPP_EVENTTYPE_CLIPBOARD_PASTED) */ +SOKOL_APP_API_DECL const char* sapp_get_clipboard_string(void); +/* set the window title (only on desktop platforms) */ +SOKOL_APP_API_DECL void sapp_set_window_title(const char* str); +/* set the window icon (only on Windows and Linux) */ +SOKOL_APP_API_DECL void sapp_set_icon(const sapp_icon_desc* icon_desc); +/* gets the total number of dropped files (after an SAPP_EVENTTYPE_FILES_DROPPED event) */ +SOKOL_APP_API_DECL int sapp_get_num_dropped_files(void); +/* gets the dropped file paths */ +SOKOL_APP_API_DECL const char* sapp_get_dropped_file_path(int index); + +/* special run-function for SOKOL_NO_ENTRY (in standard mode this is an empty stub) */ +SOKOL_APP_API_DECL void sapp_run(const sapp_desc* desc); + +/* get runtime environment information */ +SOKOL_APP_API_DECL sapp_environment sapp_get_environment(void); +/* get current frame's swapchain information (call once per frame!) */ +SOKOL_APP_API_DECL sapp_swapchain sapp_get_swapchain(void); + +/* EGL: get EGLDisplay object */ +SOKOL_APP_API_DECL const void* sapp_egl_get_display(void); +/* EGL: get EGLContext object */ +SOKOL_APP_API_DECL const void* sapp_egl_get_context(void); + +/* HTML5: enable or disable the hardwired "Leave Site?" dialog box */ +SOKOL_APP_API_DECL void sapp_html5_ask_leave_site(bool ask); +/* HTML5: get byte size of a dropped file */ +SOKOL_APP_API_DECL uint32_t sapp_html5_get_dropped_file_size(int index); +/* HTML5: asynchronously load the content of a dropped file */ +SOKOL_APP_API_DECL void sapp_html5_fetch_dropped_file(const sapp_html5_fetch_request* request); + +/* macOS: get bridged pointer to macOS NSWindow */ +SOKOL_APP_API_DECL const void* sapp_macos_get_window(void); +/* iOS: get bridged pointer to iOS UIWindow */ +SOKOL_APP_API_DECL const void* sapp_ios_get_window(void); +/* iOS: set supported interface orientations (UIInterfaceOrientationMask values) */ +SOKOL_APP_API_DECL void sapp_ios_set_supported_orientations(uint32_t mask); + +/* D3D11: get pointer to IDXGISwapChain object */ +SOKOL_APP_API_DECL const void* sapp_d3d11_get_swap_chain(void); + +/* Win32: get the HWND window handle */ +SOKOL_APP_API_DECL const void* sapp_win32_get_hwnd(void); + +/* GL: get major version */ +SOKOL_APP_API_DECL int sapp_gl_get_major_version(void); +/* GL: get minor version */ +SOKOL_APP_API_DECL int sapp_gl_get_minor_version(void); +/* GL: return true if the context is GLES */ +SOKOL_APP_API_DECL bool sapp_gl_is_gles(void); + +/* X11: get Window */ +SOKOL_APP_API_DECL const void* sapp_x11_get_window(void); +/* X11: get Display */ +SOKOL_APP_API_DECL const void* sapp_x11_get_display(void); + +/* Android: get native activity handle */ +SOKOL_APP_API_DECL const void* sapp_android_get_native_activity(void); + +// Modified by tettou771 for TrussC: skip the next present call (for event-driven rendering) +SOKOL_APP_API_DECL void sapp_skip_present(void); + +#ifdef __cplusplus +} /* extern "C" */ + +/* reference-based equivalents for C++ */ +inline void sapp_run(const sapp_desc& desc) { return sapp_run(&desc); } + +#endif + +#endif // SOKOL_APP_INCLUDED + +#if defined(__cplusplus) +extern "C" { +#endif + +/* an opaque window handle (0 is the invalid handle; the sokol_app.h main + window is NOT addressable through this API while both coexist) */ +typedef struct sapp_window { uint32_t id; } sapp_window; + +typedef struct sapp_window_desc { + int x, y; /* top-left position in screen points (-1 = default) */ + int width, height; /* content size in logical points (0 = 640x480) */ + const char* title; + bool borderless; /* no title bar / decorations */ + bool no_high_dpi; /* true: framebuffer = logical size (dpi_scale 1) */ + int sample_count; /* MSAA sample count (0 = 1) */ + const void* mtl_device; /* macOS: the id sokol_gfx renders with + (Windows: unused; the shared D3D11 device is owned here) */ + + /* callbacks -- all invoked on the main thread */ + void (*tick_cb)(sapp_window win, void* user); /* this window's vsync; only while visible */ + void (*event_cb)(const sapp_event* ev, sapp_window win, void* user); + void (*close_cb)(sapp_window win, void* user); /* user closed the window; call sapp_destroy_window() to accept */ + void* user_data; +} sapp_window_desc; + +SOKOL_APP_API_DECL sapp_window sapp_create_window(const sapp_window_desc* desc); +SOKOL_APP_API_DECL void sapp_destroy_window(sapp_window win); +SOKOL_APP_API_DECL bool sapp_window_valid(sapp_window win); + +/* geometry (valid whenever the window is alive) */ +SOKOL_APP_API_DECL int sapp_window_width(sapp_window win); /* logical points */ +SOKOL_APP_API_DECL int sapp_window_height(sapp_window win); +SOKOL_APP_API_DECL int sapp_window_framebuffer_width(sapp_window win); /* pixels */ +SOKOL_APP_API_DECL int sapp_window_framebuffer_height(sapp_window win); +SOKOL_APP_API_DECL float sapp_window_dpi_scale(sapp_window win); /* fb / logical, ONE source */ +SOKOL_APP_API_DECL int sapp_window_sample_count(sapp_window win); +SOKOL_APP_API_DECL bool sapp_window_occluded(sapp_window win); +SOKOL_APP_API_DECL void sapp_window_set_title(sapp_window win, const char* title); + +/* measured interval between this window's ticks in seconds (its display's + refresh period; a best-effort estimate before the second tick) */ +SOKOL_APP_API_DECL double sapp_window_frame_duration(sapp_window win); + +/* Metal swapchain handles -- valid ONLY inside this window's tick_cb */ +SOKOL_APP_API_DECL const void* sapp_window_metal_current_drawable(sapp_window win); +SOKOL_APP_API_DECL const void* sapp_window_metal_depth_stencil_texture(sapp_window win); +SOKOL_APP_API_DECL const void* sapp_window_metal_msaa_color_texture(sapp_window win); + +/* D3D11 swapchain handles -- valid ONLY inside this window's tick_cb + (render_view is the MSAA target and resolve_view the backbuffer when + sample_count > 1; otherwise render_view is the backbuffer and + resolve_view is 0 -- same contract as sapp_swapchain.d3d11) */ +SOKOL_APP_API_DECL const void* sapp_window_d3d11_render_view(sapp_window win); +SOKOL_APP_API_DECL const void* sapp_window_d3d11_resolve_view(sapp_window win); +SOKOL_APP_API_DECL const void* sapp_window_d3d11_depth_stencil_view(sapp_window win); + +/* GL swapchain handle -- the default-framebuffer id (Linux/GLX: rendering + targets the current drawable, which is this window's during its tick_cb) */ +SOKOL_APP_API_DECL uint32_t sapp_window_gl_framebuffer(sapp_window win); + +/* native escape hatches (macOS: NSWindow*; Windows: HWND; Linux: X11 Window XID) */ +SOKOL_APP_API_DECL const void* sapp_window_macos_get_window(sapp_window win); +SOKOL_APP_API_DECL const void* sapp_window_win32_get_hwnd(sapp_window win); +SOKOL_APP_API_DECL const void* sapp_window_x11_get_window(sapp_window win); + +#if defined(__cplusplus) +} /* extern "C" */ +#endif +#endif /* SOKOL_APP_TC_INCLUDED */ + +/*=== IMPLEMENTATION =========================================================*/ +#if defined(SOKOL_APP_TC_IMPL_INCLUDED) && !defined(SOKOL_APP_TC_IMPL_DONE) +#define SOKOL_APP_TC_IMPL_DONE (1) + +#include +#include +#include +#if defined(__APPLE__) +#include +#endif + +#if defined(__APPLE__) && TARGET_OS_OSX +/*== macOS ==================================================================*/ +#if !defined(__OBJC__) +#error "sokol_app_tc.h macOS implementation must be compiled as Objective-C++ (.mm)" +#endif +#import +#import +#import +#import + +#if defined(SOKOL_APP_IMPL_INCLUDED) +#error "sokol_app_tc.h owns the macOS implementation of the sapp_* API; include sokol_app.h WITHOUT an implementation define in this TU" +#endif + +@class _sapp_tc_view; +@class _sapp_tc_win_delegate; +@class _sapp_tc_app_delegate; + +typedef struct _sapp_tc_window_t { + uint32_t win_id; + sapp_window_desc desc; + bool is_main; /* window #0: created by sapp_run, drives the app callbacks */ + NSWindow* window; + _sapp_tc_view* view; + _sapp_tc_win_delegate* delegate; + CAMetalLayer* layer; + CADisplayLink* link; + MTLPixelFormat color_fmt; /* layer + MSAA color texture format */ + id depth_tex; /* depth-stencil, drawable-sized */ + id msaa_tex; /* MSAA color (sample_count > 1) */ + id frame_drawable; /* valid during one tick only (main: lazily acquired) */ + int fb_width, fb_height; /* aux/drawable size (pixels) */ + int win_width, win_height; /* logical points (last seen) */ + float dpi_scale; + double frame_duration; /* measured tick interval */ + double last_tick_time; /* CADisplayLink timestamp */ + bool occluded; + bool in_tick; +} _sapp_tc_window_t; + +#define _SAPP_TC_MAX_WINDOWS (32) + +/* EMA-filtered frame timer, port of sokol_app.h's _sapp_timing_* — used only + while the main window ticks from the occluded-fallback NSTimer (the display + link timestamps are the source whenever the link is active) */ +typedef struct { + double last; /* previous sample time, 0 = none yet */ + double ema; + double smooth_dt; + double dt; /* last raw (clamped) delta */ +} _sapp_tc_timing_t; + +static struct { + bool keytable_valid; + sapp_keycode keycodes[SAPP_MAX_KEYCODES]; + uint32_t next_id; + _sapp_tc_window_t* windows[_SAPP_TC_MAX_WINDOWS]; + + /* ---- app / main-window state (this header owns sapp_run on macOS) ---- */ + struct { + sapp_desc desc; /* sanitized copy; window_title points at the buffer below */ + char window_title[256]; + bool valid; + bool init_called; + bool cleanup_called; + bool quit_requested; + bool quit_ordered; + bool fullscreen; + bool event_consumed; + bool skip_present; /* stored for API parity; ignored on Metal (as upstream) */ + uint64_t frame_count; + _sapp_tc_window_t* main; + _sapp_tc_app_delegate* dlg; + id keyup_monitor; /* Cmd+key-up workaround (Cocoa swallows those) */ + id device; + NSTimer* fallback_timer; /* ~60Hz tick source while the main window is occluded */ + _sapp_tc_timing_t timing; + /* mouse cursor */ + bool mouse_shown; + sapp_mouse_cursor current_cursor; + NSCursor* standard_cursors[_SAPP_MOUSECURSOR_NUM]; + NSCursor* custom_cursors[_SAPP_MOUSECURSOR_NUM]; + bool custom_cursor_bound[_SAPP_MOUSECURSOR_NUM]; + /* clipboard */ + bool clipboard_enabled; + int clipboard_size; + char* clipboard; + /* drag & drop */ + bool drop_enabled; + int drop_max_files; + int drop_max_path_length; + int drop_num_files; + char* drop_buffer; + } app; +} _sapp_tc; + +static void _sapp_tc_main_tick(CADisplayLink* link); +static void _sapp_tc_update_main_dimensions(bool allow_event); +static void _sapp_tc_apply_cursor(sapp_mouse_cursor cursor, bool shown); + +static void _sapp_tc_timing_reset(_sapp_tc_timing_t* t) { + t->last = 0.0; + t->ema = t->smooth_dt = t->dt = 1.0 / 60.0; +} + +static double _sapp_tc_timing_clamp(double dt) { + if (dt < 0.000001) return 0.000001; + if (dt > 0.1) return 0.1; + return dt; +} + +static void _sapp_tc_timing_update(_sapp_tc_timing_t* t) { + const double now = CACurrentMediaTime(); + if (t->last > 0.0) { + double dt = _sapp_tc_timing_clamp(now - t->last); + t->dt = dt; + /* big jump: reset the filter; otherwise EMA-smooth */ + if (fabs(dt - t->smooth_dt) > 0.004) { + t->ema = t->smooth_dt = dt; + } else { + t->ema += 0.025 * (dt - t->ema); + t->smooth_dt = _sapp_tc_timing_clamp(t->ema); + } + } + t->last = now; +} + +/* keycode table lifted verbatim from sokol_app.h's _sapp_macos_init_keytable() + so SAPP_KEYCODE_* values are identical across main and secondary windows */ +static void _sapp_tc_init_keytable(void) { + if (_sapp_tc.keytable_valid) return; + _sapp_tc.keytable_valid = true; + _sapp_tc.keycodes[0x1D] = SAPP_KEYCODE_0; + _sapp_tc.keycodes[0x12] = SAPP_KEYCODE_1; + _sapp_tc.keycodes[0x13] = SAPP_KEYCODE_2; + _sapp_tc.keycodes[0x14] = SAPP_KEYCODE_3; + _sapp_tc.keycodes[0x15] = SAPP_KEYCODE_4; + _sapp_tc.keycodes[0x17] = SAPP_KEYCODE_5; + _sapp_tc.keycodes[0x16] = SAPP_KEYCODE_6; + _sapp_tc.keycodes[0x1A] = SAPP_KEYCODE_7; + _sapp_tc.keycodes[0x1C] = SAPP_KEYCODE_8; + _sapp_tc.keycodes[0x19] = SAPP_KEYCODE_9; + _sapp_tc.keycodes[0x00] = SAPP_KEYCODE_A; + _sapp_tc.keycodes[0x0B] = SAPP_KEYCODE_B; + _sapp_tc.keycodes[0x08] = SAPP_KEYCODE_C; + _sapp_tc.keycodes[0x02] = SAPP_KEYCODE_D; + _sapp_tc.keycodes[0x0E] = SAPP_KEYCODE_E; + _sapp_tc.keycodes[0x03] = SAPP_KEYCODE_F; + _sapp_tc.keycodes[0x05] = SAPP_KEYCODE_G; + _sapp_tc.keycodes[0x04] = SAPP_KEYCODE_H; + _sapp_tc.keycodes[0x22] = SAPP_KEYCODE_I; + _sapp_tc.keycodes[0x26] = SAPP_KEYCODE_J; + _sapp_tc.keycodes[0x28] = SAPP_KEYCODE_K; + _sapp_tc.keycodes[0x25] = SAPP_KEYCODE_L; + _sapp_tc.keycodes[0x2E] = SAPP_KEYCODE_M; + _sapp_tc.keycodes[0x2D] = SAPP_KEYCODE_N; + _sapp_tc.keycodes[0x1F] = SAPP_KEYCODE_O; + _sapp_tc.keycodes[0x23] = SAPP_KEYCODE_P; + _sapp_tc.keycodes[0x0C] = SAPP_KEYCODE_Q; + _sapp_tc.keycodes[0x0F] = SAPP_KEYCODE_R; + _sapp_tc.keycodes[0x01] = SAPP_KEYCODE_S; + _sapp_tc.keycodes[0x11] = SAPP_KEYCODE_T; + _sapp_tc.keycodes[0x20] = SAPP_KEYCODE_U; + _sapp_tc.keycodes[0x09] = SAPP_KEYCODE_V; + _sapp_tc.keycodes[0x0D] = SAPP_KEYCODE_W; + _sapp_tc.keycodes[0x07] = SAPP_KEYCODE_X; + _sapp_tc.keycodes[0x10] = SAPP_KEYCODE_Y; + _sapp_tc.keycodes[0x06] = SAPP_KEYCODE_Z; + _sapp_tc.keycodes[0x27] = SAPP_KEYCODE_APOSTROPHE; + _sapp_tc.keycodes[0x2A] = SAPP_KEYCODE_BACKSLASH; + _sapp_tc.keycodes[0x2B] = SAPP_KEYCODE_COMMA; + _sapp_tc.keycodes[0x18] = SAPP_KEYCODE_EQUAL; + _sapp_tc.keycodes[0x32] = SAPP_KEYCODE_GRAVE_ACCENT; + _sapp_tc.keycodes[0x21] = SAPP_KEYCODE_LEFT_BRACKET; + _sapp_tc.keycodes[0x1B] = SAPP_KEYCODE_MINUS; + _sapp_tc.keycodes[0x2F] = SAPP_KEYCODE_PERIOD; + _sapp_tc.keycodes[0x1E] = SAPP_KEYCODE_RIGHT_BRACKET; + _sapp_tc.keycodes[0x29] = SAPP_KEYCODE_SEMICOLON; + _sapp_tc.keycodes[0x2C] = SAPP_KEYCODE_SLASH; + _sapp_tc.keycodes[0x0A] = SAPP_KEYCODE_WORLD_1; + _sapp_tc.keycodes[0x33] = SAPP_KEYCODE_BACKSPACE; + _sapp_tc.keycodes[0x39] = SAPP_KEYCODE_CAPS_LOCK; + _sapp_tc.keycodes[0x75] = SAPP_KEYCODE_DELETE; + _sapp_tc.keycodes[0x7D] = SAPP_KEYCODE_DOWN; + _sapp_tc.keycodes[0x77] = SAPP_KEYCODE_END; + _sapp_tc.keycodes[0x24] = SAPP_KEYCODE_ENTER; + _sapp_tc.keycodes[0x35] = SAPP_KEYCODE_ESCAPE; + _sapp_tc.keycodes[0x7A] = SAPP_KEYCODE_F1; + _sapp_tc.keycodes[0x78] = SAPP_KEYCODE_F2; + _sapp_tc.keycodes[0x63] = SAPP_KEYCODE_F3; + _sapp_tc.keycodes[0x76] = SAPP_KEYCODE_F4; + _sapp_tc.keycodes[0x60] = SAPP_KEYCODE_F5; + _sapp_tc.keycodes[0x61] = SAPP_KEYCODE_F6; + _sapp_tc.keycodes[0x62] = SAPP_KEYCODE_F7; + _sapp_tc.keycodes[0x64] = SAPP_KEYCODE_F8; + _sapp_tc.keycodes[0x65] = SAPP_KEYCODE_F9; + _sapp_tc.keycodes[0x6D] = SAPP_KEYCODE_F10; + _sapp_tc.keycodes[0x67] = SAPP_KEYCODE_F11; + _sapp_tc.keycodes[0x6F] = SAPP_KEYCODE_F12; + _sapp_tc.keycodes[0x69] = SAPP_KEYCODE_F13; + _sapp_tc.keycodes[0x6B] = SAPP_KEYCODE_F14; + _sapp_tc.keycodes[0x71] = SAPP_KEYCODE_F15; + _sapp_tc.keycodes[0x6A] = SAPP_KEYCODE_F16; + _sapp_tc.keycodes[0x40] = SAPP_KEYCODE_F17; + _sapp_tc.keycodes[0x4F] = SAPP_KEYCODE_F18; + _sapp_tc.keycodes[0x50] = SAPP_KEYCODE_F19; + _sapp_tc.keycodes[0x5A] = SAPP_KEYCODE_F20; + _sapp_tc.keycodes[0x73] = SAPP_KEYCODE_HOME; + _sapp_tc.keycodes[0x72] = SAPP_KEYCODE_INSERT; + _sapp_tc.keycodes[0x7B] = SAPP_KEYCODE_LEFT; + _sapp_tc.keycodes[0x3A] = SAPP_KEYCODE_LEFT_ALT; + _sapp_tc.keycodes[0x3B] = SAPP_KEYCODE_LEFT_CONTROL; + _sapp_tc.keycodes[0x38] = SAPP_KEYCODE_LEFT_SHIFT; + _sapp_tc.keycodes[0x37] = SAPP_KEYCODE_LEFT_SUPER; + _sapp_tc.keycodes[0x6E] = SAPP_KEYCODE_MENU; + _sapp_tc.keycodes[0x47] = SAPP_KEYCODE_NUM_LOCK; + _sapp_tc.keycodes[0x79] = SAPP_KEYCODE_PAGE_DOWN; + _sapp_tc.keycodes[0x74] = SAPP_KEYCODE_PAGE_UP; + _sapp_tc.keycodes[0x7C] = SAPP_KEYCODE_RIGHT; + _sapp_tc.keycodes[0x3D] = SAPP_KEYCODE_RIGHT_ALT; + _sapp_tc.keycodes[0x3E] = SAPP_KEYCODE_RIGHT_CONTROL; + _sapp_tc.keycodes[0x3C] = SAPP_KEYCODE_RIGHT_SHIFT; + _sapp_tc.keycodes[0x36] = SAPP_KEYCODE_RIGHT_SUPER; + _sapp_tc.keycodes[0x31] = SAPP_KEYCODE_SPACE; + _sapp_tc.keycodes[0x30] = SAPP_KEYCODE_TAB; + _sapp_tc.keycodes[0x7E] = SAPP_KEYCODE_UP; + _sapp_tc.keycodes[0x52] = SAPP_KEYCODE_KP_0; + _sapp_tc.keycodes[0x53] = SAPP_KEYCODE_KP_1; + _sapp_tc.keycodes[0x54] = SAPP_KEYCODE_KP_2; + _sapp_tc.keycodes[0x55] = SAPP_KEYCODE_KP_3; + _sapp_tc.keycodes[0x56] = SAPP_KEYCODE_KP_4; + _sapp_tc.keycodes[0x57] = SAPP_KEYCODE_KP_5; + _sapp_tc.keycodes[0x58] = SAPP_KEYCODE_KP_6; + _sapp_tc.keycodes[0x59] = SAPP_KEYCODE_KP_7; + _sapp_tc.keycodes[0x5B] = SAPP_KEYCODE_KP_8; + _sapp_tc.keycodes[0x5C] = SAPP_KEYCODE_KP_9; + _sapp_tc.keycodes[0x45] = SAPP_KEYCODE_KP_ADD; + _sapp_tc.keycodes[0x41] = SAPP_KEYCODE_KP_DECIMAL; + _sapp_tc.keycodes[0x4B] = SAPP_KEYCODE_KP_DIVIDE; + _sapp_tc.keycodes[0x4C] = SAPP_KEYCODE_KP_ENTER; + _sapp_tc.keycodes[0x51] = SAPP_KEYCODE_KP_EQUAL; + _sapp_tc.keycodes[0x43] = SAPP_KEYCODE_KP_MULTIPLY; + _sapp_tc.keycodes[0x4E] = SAPP_KEYCODE_KP_SUBTRACT; +} + +static sapp_keycode _sapp_tc_translate_key(int scancode) { + if ((scancode >= 0) && (scancode < SAPP_MAX_KEYCODES)) { + return _sapp_tc.keycodes[scancode]; + } + return SAPP_KEYCODE_INVALID; +} + +static _sapp_tc_window_t* _sapp_tc_lookup(sapp_window win) { + if (win.id == 0) return 0; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* w = _sapp_tc.windows[i]; + if (w && w->win_id == win.id) return w; + } + return 0; +} + +static uint32_t _sapp_tc_mods(NSEvent* ev) { + const NSEventModifierFlags f = ev.modifierFlags; + uint32_t m = 0; + if (f & NSEventModifierFlagShift) m |= SAPP_MODIFIER_SHIFT; + if (f & NSEventModifierFlagControl) m |= SAPP_MODIFIER_CTRL; + if (f & NSEventModifierFlagOption) m |= SAPP_MODIFIER_ALT; + if (f & NSEventModifierFlagCommand) m |= SAPP_MODIFIER_SUPER; + return m; +} + +static void _sapp_tc_send(_sapp_tc_window_t* w, sapp_event* ev) { + if (!w->desc.event_cb) return; + ev->frame_count = _sapp_tc.app.frame_count; + ev->window_width = w->win_width; + ev->window_height = w->win_height; + ev->framebuffer_width = w->fb_width; + ev->framebuffer_height = w->fb_height; + sapp_window handle = { w->win_id }; + w->desc.event_cb(ev, handle, w->desc.user_data); +} + +/*-- main window: event routing to the sapp_desc callbacks ------------------*/ +static bool _sapp_tc_app_events_enabled(void) { + /* same gate as sokol_app.h: nothing fires before the first tick's init_cb */ + return _sapp_tc.app.init_called && + (_sapp_tc.app.desc.event_cb || _sapp_tc.app.desc.event_userdata_cb); +} + +static void _sapp_tc_main_event_tramp(const sapp_event* ev, sapp_window win, void* user) { + (void)win; (void)user; + if (!_sapp_tc_app_events_enabled()) return; + _sapp_tc.app.event_consumed = false; + if (_sapp_tc.app.desc.event_cb) { + _sapp_tc.app.desc.event_cb(ev); + } else { + _sapp_tc.app.desc.event_userdata_cb(ev, _sapp_tc.app.desc.user_data); + } +} + +/*-- mouse cursor (app-global, same model as sokol_app.h) -------------------*/ +/* private-but-stable AppKit cursor selectors (same set sokol_app/GLFW use) */ +@interface NSCursor(_sapp_tc_private_cursors) ++ (NSCursor*)_windowResizeEastWestCursor; ++ (NSCursor*)_windowResizeNorthSouthCursor; ++ (NSCursor*)_windowResizeNorthWestSouthEastCursor; ++ (NSCursor*)_windowResizeNorthEastSouthWestCursor; +@end + +static NSCursor* _sapp_tc_priv_cursor(SEL sel, NSCursor* fallback) { + if ([NSCursor respondsToSelector:sel]) { + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Warc-performSelector-leaks" + NSCursor* cur = [NSCursor performSelector:sel]; + #pragma clang diagnostic pop + if (cur) return cur; + } + return fallback; +} + +static void _sapp_tc_init_cursors(void) { + _sapp_tc.app.standard_cursors[SAPP_MOUSECURSOR_DEFAULT] = nil; /* uses arrowCursor */ + _sapp_tc.app.standard_cursors[SAPP_MOUSECURSOR_ARROW] = [NSCursor arrowCursor]; + _sapp_tc.app.standard_cursors[SAPP_MOUSECURSOR_IBEAM] = [NSCursor IBeamCursor]; + _sapp_tc.app.standard_cursors[SAPP_MOUSECURSOR_CROSSHAIR] = [NSCursor crosshairCursor]; + _sapp_tc.app.standard_cursors[SAPP_MOUSECURSOR_POINTING_HAND] = [NSCursor pointingHandCursor]; + _sapp_tc.app.standard_cursors[SAPP_MOUSECURSOR_RESIZE_EW] = + _sapp_tc_priv_cursor(@selector(_windowResizeEastWestCursor), [NSCursor resizeLeftRightCursor]); + _sapp_tc.app.standard_cursors[SAPP_MOUSECURSOR_RESIZE_NS] = + _sapp_tc_priv_cursor(@selector(_windowResizeNorthSouthCursor), [NSCursor resizeUpDownCursor]); + _sapp_tc.app.standard_cursors[SAPP_MOUSECURSOR_RESIZE_NWSE] = + _sapp_tc_priv_cursor(@selector(_windowResizeNorthWestSouthEastCursor), [NSCursor closedHandCursor]); + _sapp_tc.app.standard_cursors[SAPP_MOUSECURSOR_RESIZE_NESW] = + _sapp_tc_priv_cursor(@selector(_windowResizeNorthEastSouthWestCursor), [NSCursor closedHandCursor]); + _sapp_tc.app.standard_cursors[SAPP_MOUSECURSOR_RESIZE_ALL] = [NSCursor closedHandCursor]; + _sapp_tc.app.standard_cursors[SAPP_MOUSECURSOR_NOT_ALLOWED] = [NSCursor operationNotAllowedCursor]; +} + +static void _sapp_tc_apply_cursor(sapp_mouse_cursor cursor, bool shown) { + /* [NSCursor hide/unhide] stack, so only act on an actual change */ + if (shown != _sapp_tc.app.mouse_shown) { + if (shown) [NSCursor unhide]; else [NSCursor hide]; + } + NSCursor* ns = [NSCursor arrowCursor]; + if (_sapp_tc.app.custom_cursor_bound[cursor] && _sapp_tc.app.custom_cursors[cursor]) { + ns = _sapp_tc.app.custom_cursors[cursor]; + } else if (_sapp_tc.app.standard_cursors[cursor]) { + ns = _sapp_tc.app.standard_cursors[cursor]; + } + [ns set]; + _sapp_tc.app.current_cursor = cursor; + _sapp_tc.app.mouse_shown = shown; +} + +/*-- content view -----------------------------------------------------------*/ +@interface _sapp_tc_view : NSView +@property (assign) _sapp_tc_window_t* w; +@end + +@implementation _sapp_tc_view + +- (BOOL)acceptsFirstResponder { return YES; } +- (BOOL)isFlipped { return YES; } /* top-left origin */ + +- (void)updateTrackingAreas { + for (NSTrackingArea* a in self.trackingAreas) [self removeTrackingArea:a]; + /* ActiveAlways: hover must work while the window is NOT key (a control + panel window tracks the pointer while the main window keeps focus) */ + NSTrackingArea* area = [[NSTrackingArea alloc] initWithRect:self.bounds + options:(NSTrackingMouseMoved | NSTrackingMouseEnteredAndExited | + NSTrackingActiveAlways | NSTrackingEnabledDuringMouseDrag | + NSTrackingCursorUpdate | NSTrackingInVisibleRect) + owner:self userInfo:nil]; + [self addTrackingArea:area]; + [super updateTrackingAreas]; +} + +/* keeps a custom/hidden cursor sticky when the pointer (re)enters the view */ +- (void)cursorUpdate:(NSEvent*)ev { + (void)ev; + _sapp_tc_apply_cursor(_sapp_tc.app.current_cursor, _sapp_tc.app.mouse_shown); +} + +- (void)mouseEvt:(NSEvent*)ev type:(sapp_event_type)type button:(sapp_mousebutton)btn { + _sapp_tc_window_t* w = self.w; + if (!w) return; + NSPoint p = [self convertPoint:ev.locationInWindow fromView:nil]; + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = type; + e.mouse_button = btn; + /* framebuffer pixels, same convention as sokol_app.h with high_dpi */ + e.mouse_x = (float)p.x * w->dpi_scale; + e.mouse_y = (float)p.y * w->dpi_scale; + e.mouse_dx = (float)ev.deltaX * w->dpi_scale; + e.mouse_dy = (float)ev.deltaY * w->dpi_scale; + e.modifiers = _sapp_tc_mods(ev); + _sapp_tc_send(w, &e); +} + +- (void)mouseDown:(NSEvent*)ev { [self mouseEvt:ev type:SAPP_EVENTTYPE_MOUSE_DOWN button:SAPP_MOUSEBUTTON_LEFT]; } +- (void)mouseUp:(NSEvent*)ev { [self mouseEvt:ev type:SAPP_EVENTTYPE_MOUSE_UP button:SAPP_MOUSEBUTTON_LEFT]; } +- (void)rightMouseDown:(NSEvent*)ev { [self mouseEvt:ev type:SAPP_EVENTTYPE_MOUSE_DOWN button:SAPP_MOUSEBUTTON_RIGHT]; } +- (void)rightMouseUp:(NSEvent*)ev { [self mouseEvt:ev type:SAPP_EVENTTYPE_MOUSE_UP button:SAPP_MOUSEBUTTON_RIGHT]; } +- (void)otherMouseDown:(NSEvent*)ev { [self mouseEvt:ev type:SAPP_EVENTTYPE_MOUSE_DOWN button:SAPP_MOUSEBUTTON_MIDDLE]; } +- (void)otherMouseUp:(NSEvent*)ev { [self mouseEvt:ev type:SAPP_EVENTTYPE_MOUSE_UP button:SAPP_MOUSEBUTTON_MIDDLE]; } +- (void)mouseMoved:(NSEvent*)ev { [self mouseEvt:ev type:SAPP_EVENTTYPE_MOUSE_MOVE button:SAPP_MOUSEBUTTON_INVALID]; } +- (void)mouseDragged:(NSEvent*)ev { [self mouseEvt:ev type:SAPP_EVENTTYPE_MOUSE_MOVE button:SAPP_MOUSEBUTTON_INVALID]; } +- (void)rightMouseDragged:(NSEvent*)ev { [self mouseEvt:ev type:SAPP_EVENTTYPE_MOUSE_MOVE button:SAPP_MOUSEBUTTON_INVALID]; } +- (void)otherMouseDragged:(NSEvent*)ev { [self mouseEvt:ev type:SAPP_EVENTTYPE_MOUSE_MOVE button:SAPP_MOUSEBUTTON_INVALID]; } +- (void)mouseEntered:(NSEvent*)ev { [self mouseEvt:ev type:SAPP_EVENTTYPE_MOUSE_ENTER button:SAPP_MOUSEBUTTON_INVALID]; } +- (void)mouseExited:(NSEvent*)ev { [self mouseEvt:ev type:SAPP_EVENTTYPE_MOUSE_LEAVE button:SAPP_MOUSEBUTTON_INVALID]; } + +- (void)scrollWheel:(NSEvent*)ev { + _sapp_tc_window_t* w = self.w; + if (!w) return; + float dx = (float)ev.scrollingDeltaX; + float dy = (float)ev.scrollingDeltaY; + if (ev.hasPreciseScrollingDeltas) { dx *= 0.1f; dy *= 0.1f; } + if ((fabsf(dx) > 0.0f) || (fabsf(dy) > 0.0f)) { + NSPoint p = [self convertPoint:ev.locationInWindow fromView:nil]; + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_MOUSE_SCROLL; + e.mouse_x = (float)p.x * w->dpi_scale; + e.mouse_y = (float)p.y * w->dpi_scale; + e.scroll_x = dx; + e.scroll_y = dy; + e.modifiers = _sapp_tc_mods(ev); + _sapp_tc_send(w, &e); + } +} + +- (void)keyEvt:(NSEvent*)ev pressed:(bool)down { + _sapp_tc_window_t* w = self.w; + if (!w) return; + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = down ? SAPP_EVENTTYPE_KEY_DOWN : SAPP_EVENTTYPE_KEY_UP; + e.key_code = _sapp_tc_translate_key(ev.keyCode); + e.key_repeat = ev.isARepeat; + e.modifiers = _sapp_tc_mods(ev); + _sapp_tc_send(w, &e); + + /* CHAR events on key down, same filtering as sokol_app.h (skip the + function-key private range and control characters) */ + if (down) { + NSString* chars = ev.characters; + const NSUInteger len = chars.length; + for (NSUInteger i = 0; i < len; i++) { + uint32_t cp = [chars characterAtIndex:i]; + if (CFStringIsSurrogateHighCharacter((UniChar)cp) && (i + 1) < len) { + const UniChar lo = [chars characterAtIndex:i + 1]; + if (CFStringIsSurrogateLowCharacter(lo)) { + cp = (uint32_t)CFStringGetLongCharacterForSurrogatePair((UniChar)cp, lo); + i++; + } + } + if ((cp & 0xFF00) == 0xF700) continue; /* NSF...FunctionKey range */ + if (cp < 32 || cp == 127) continue; + sapp_event ce; + memset(&ce, 0, sizeof(ce)); + ce.type = SAPP_EVENTTYPE_CHAR; + ce.char_code = cp; + ce.key_repeat = ev.isARepeat; + ce.modifiers = _sapp_tc_mods(ev); + _sapp_tc_send(w, &ce); + } + } + + /* Cmd+V paste notification, same trigger as sokol_app.h (exact-match on + the Super modifier); the app reads the clipboard in its handler */ + if (down && _sapp_tc.app.clipboard_enabled && + (_sapp_tc_mods(ev) == SAPP_MODIFIER_SUPER) && + (_sapp_tc_translate_key(ev.keyCode) == SAPP_KEYCODE_V)) { + sapp_event pe; + memset(&pe, 0, sizeof(pe)); + pe.type = SAPP_EVENTTYPE_CLIPBOARD_PASTED; + pe.modifiers = SAPP_MODIFIER_SUPER; + _sapp_tc_send(w, &pe); + } +} + +/*-- drag & drop (NSDraggingDestination; registered on the main view) -------*/ +- (NSDragOperation)draggingEntered:(id)sender { + (void)sender; + return NSDragOperationCopy; +} + +- (NSDragOperation)draggingUpdated:(id)sender { + (void)sender; + return NSDragOperationCopy; +} + +- (BOOL)performDragOperation:(id)sender { + _sapp_tc_window_t* w = self.w; + if (!w || !_sapp_tc.app.drop_enabled || !_sapp_tc.app.drop_buffer) return NO; + NSPasteboard* pboard = sender.draggingPasteboard; + if (![pboard.types containsObject:NSPasteboardTypeFileURL]) return NO; + const int max_files = _sapp_tc.app.drop_max_files; + const int max_path = _sapp_tc.app.drop_max_path_length; + memset(_sapp_tc.app.drop_buffer, 0, (size_t)max_files * (size_t)max_path); + _sapp_tc.app.drop_num_files = 0; + int num = (int)pboard.pasteboardItems.count; + if (num > max_files) num = max_files; + for (int i = 0; i < num; i++) { + NSString* str = [pboard.pasteboardItems[(NSUInteger)i] stringForType:NSPasteboardTypeFileURL]; + if (!str) return NO; + NSURL* url = [NSURL URLWithString:str]; + const char* path = url.standardizedURL.path.UTF8String; + if (!path || (int)strlen(path) >= max_path) return NO; /* path too long: drop everything */ + strcpy(_sapp_tc.app.drop_buffer + (size_t)i * (size_t)max_path, path); + } + _sapp_tc.app.drop_num_files = num; + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_FILES_DROPPED; + NSPoint p = [self convertPoint:sender.draggingLocation fromView:nil]; + e.mouse_x = (float)p.x * w->dpi_scale; + e.mouse_y = (float)p.y * w->dpi_scale; + _sapp_tc_send(w, &e); + return YES; +} + +- (void)keyDown:(NSEvent*)ev { [self keyEvt:ev pressed:true]; } +- (void)keyUp:(NSEvent*)ev { [self keyEvt:ev pressed:false]; } + +- (void)flagsChanged:(NSEvent*)ev { + _sapp_tc_window_t* w = self.w; + if (!w) return; + const sapp_keycode kc = _sapp_tc_translate_key(ev.keyCode); + if (kc == SAPP_KEYCODE_INVALID) return; + /* device-dependent modifier bits (same values GLFW/sokol_app rely on) */ + const NSEventModifierFlags f = ev.modifierFlags; + bool down = false; + switch (kc) { + case SAPP_KEYCODE_LEFT_SHIFT: down = (f & 0x0002) != 0; break; + case SAPP_KEYCODE_RIGHT_SHIFT: down = (f & 0x0004) != 0; break; + case SAPP_KEYCODE_LEFT_CONTROL: down = (f & 0x0001) != 0; break; + case SAPP_KEYCODE_RIGHT_CONTROL: down = (f & 0x2000) != 0; break; + case SAPP_KEYCODE_LEFT_ALT: down = (f & 0x0020) != 0; break; + case SAPP_KEYCODE_RIGHT_ALT: down = (f & 0x0040) != 0; break; + case SAPP_KEYCODE_LEFT_SUPER: down = (f & 0x0008) != 0; break; + case SAPP_KEYCODE_RIGHT_SUPER: down = (f & 0x0010) != 0; break; + case SAPP_KEYCODE_CAPS_LOCK: down = (f & NSEventModifierFlagCapsLock) != 0; break; + default: return; + } + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = down ? SAPP_EVENTTYPE_KEY_DOWN : SAPP_EVENTTYPE_KEY_UP; + e.key_code = kc; + e.modifiers = _sapp_tc_mods(ev); + _sapp_tc_send(w, &e); +} + +/*-- per-frame tick (CADisplayLink target) ---------------------------------*/ +- (void)tick:(CADisplayLink*)link { + _sapp_tc_window_t* w = self.w; + if (!w) return; + + /* window #0 runs the app frame loop (sokol_app.h parity: no occlusion + gate — while occluded the link auto-suspends and a ~60Hz fallback + timer keeps the frame callback alive, exactly like upstream) */ + if (w->is_main) { + _sapp_tc_main_tick(link); + return; + } + + /* Occluded => not due. THIS is what makes the nextDrawable stall + structurally impossible: the acquiring code below only runs for a + provably visible window. (The display link also auto-suspends for + occluded views; this gate covers the transition race.) */ + const bool occluded = (w->window.occlusionState & NSWindowOcclusionStateVisible) == 0; + if (occluded != w->occluded) { + w->occluded = occluded; + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = occluded ? SAPP_EVENTTYPE_SUSPENDED : SAPP_EVENTTYPE_RESUMED; + _sapp_tc_send(w, &e); + } + if (occluded) return; + + /* measured tick interval (the display's refresh period) */ + if (w->last_tick_time > 0.0) { + w->frame_duration = link.timestamp - w->last_tick_time; + } else { + w->frame_duration = link.targetTimestamp - link.timestamp; + } + w->last_tick_time = link.timestamp; + + /* keep the layer sized to the view; ONE dpi ratio source: the scale we + size the drawable with is the scale we report (and the scale mouse + coordinates are multiplied by) -- never assume backingScale == 2 */ + const CGFloat scale = w->desc.no_high_dpi ? 1.0 : w->window.backingScaleFactor; + const NSSize sz = self.bounds.size; + const int fbw = (int)(sz.width * scale); + const int fbh = (int)(sz.height * scale); + if (fbw <= 0 || fbh <= 0) return; + if ((int)w->layer.drawableSize.width != fbw || (int)w->layer.drawableSize.height != fbh) { + w->layer.contentsScale = scale; + w->layer.drawableSize = CGSizeMake(fbw, fbh); + } + const bool resized = (fbw != w->fb_width) || (fbh != w->fb_height) || + ((int)sz.width != w->win_width) || ((int)sz.height != w->win_height); + w->fb_width = fbw; + w->fb_height = fbh; + w->win_width = (int)sz.width; + w->win_height = (int)sz.height; + w->dpi_scale = (float)scale; + if (resized) { + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_RESIZED; + _sapp_tc_send(w, &e); + } + + /* depth-stencil / MSAA textures, drawable-sized */ + if (w->depth_tex == nil || (int)w->depth_tex.width != fbw || (int)w->depth_tex.height != fbh) { + id dev = w->layer.device; + MTLTextureDescriptor* dd = [MTLTextureDescriptor + texture2DDescriptorWithPixelFormat:MTLPixelFormatDepth32Float_Stencil8 + width:fbw height:fbh mipmapped:NO]; + dd.usage = MTLTextureUsageRenderTarget; + dd.storageMode = MTLStorageModePrivate; + dd.sampleCount = w->desc.sample_count; + dd.textureType = w->desc.sample_count > 1 ? MTLTextureType2DMultisample : MTLTextureType2D; + w->depth_tex = [dev newTextureWithDescriptor:dd]; + if (w->desc.sample_count > 1) { + MTLTextureDescriptor* md = [MTLTextureDescriptor + texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm + width:fbw height:fbh mipmapped:NO]; + md.usage = MTLTextureUsageRenderTarget; + md.storageMode = MTLStorageModePrivate; + md.sampleCount = w->desc.sample_count; + md.textureType = MTLTextureType2DMultisample; + w->msaa_tex = [dev newTextureWithDescriptor:md]; + } + } + + w->frame_drawable = [w->layer nextDrawable]; + if (w->frame_drawable == nil) return; + w->in_tick = true; + if (w->desc.tick_cb) { + sapp_window handle = { w->win_id }; + w->desc.tick_cb(handle, w->desc.user_data); + } + w->in_tick = false; + w->frame_drawable = nil; +} + +@end + +/*-- window delegate --------------------------------------------------------*/ +@interface _sapp_tc_win_delegate : NSObject +@property (assign) _sapp_tc_window_t* w; +@end + +static void _sapp_tc_start_fallback_timer(void); +static void _sapp_tc_stop_fallback_timer(void); + +@implementation _sapp_tc_win_delegate +- (BOOL)windowShouldClose:(id)sender { + (void)sender; + _sapp_tc_window_t* w = self.w; + if (!w || !w->is_main) return YES; + /* main window: sokol_app.h's cancellable quit dance. sapp_quit() pre-sets + quit_ordered (unvetoable); otherwise QUIT_REQUESTED is delivered and a + handler may call sapp_cancel_quit() to keep the window open. */ + if (!_sapp_tc.app.quit_ordered) { + _sapp_tc.app.quit_requested = true; + sapp_event e; memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_QUIT_REQUESTED; + _sapp_tc_send(w, &e); + if (_sapp_tc.app.quit_requested) { + _sapp_tc.app.quit_ordered = true; + } + } + return _sapp_tc.app.quit_ordered ? YES : NO; +} +- (void)windowWillClose:(NSNotification*)n { + (void)n; + _sapp_tc_window_t* w = self.w; + if (!w) return; + if (w->is_main) { + /* main window gone = app gone, even if secondary windows are still + open. terminate: runs applicationWillTerminate (cleanup_cb) and + never returns to the run loop. */ + [NSApp terminate:nil]; + return; + } + if (w->desc.close_cb) { + sapp_window handle = { w->win_id }; + w->desc.close_cb(handle, w->desc.user_data); + } +} +- (void)windowDidResize:(NSNotification*)n { + (void)n; + _sapp_tc_window_t* w = self.w; + if (w && w->is_main) _sapp_tc_update_main_dimensions(true); +} +- (void)windowDidChangeScreen:(NSNotification*)n { + (void)n; + _sapp_tc_window_t* w = self.w; + if (w && w->is_main) _sapp_tc_update_main_dimensions(true); +} +- (void)windowDidChangeOcclusionState:(NSNotification*)n { + (void)n; + _sapp_tc_window_t* w = self.w; + if (!w || !w->is_main) return; + if (w->window.occlusionState & NSWindowOcclusionStateVisible) { + _sapp_tc_stop_fallback_timer(); /* the display link auto-resumes */ + } else { + _sapp_tc_start_fallback_timer(); /* keep frame_cb alive while hidden */ + } +} +- (void)windowDidMiniaturize:(NSNotification*)n { + (void)n; + _sapp_tc_window_t* w = self.w; + if (!w || !w->is_main) return; + _sapp_tc_start_fallback_timer(); + sapp_event e; memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_ICONIFIED; + _sapp_tc_send(w, &e); +} +- (void)windowDidDeminiaturize:(NSNotification*)n { + (void)n; + _sapp_tc_window_t* w = self.w; + if (!w || !w->is_main) return; + _sapp_tc_stop_fallback_timer(); + sapp_event e; memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_RESTORED; + _sapp_tc_send(w, &e); +} +- (void)windowDidEnterFullScreen:(NSNotification*)n { + (void)n; + _sapp_tc_window_t* w = self.w; + if (w && w->is_main) _sapp_tc.app.fullscreen = true; +} +- (void)windowDidExitFullScreen:(NSNotification*)n { + (void)n; + _sapp_tc_window_t* w = self.w; + if (w && w->is_main) _sapp_tc.app.fullscreen = false; +} +- (void)windowDidBecomeKey:(NSNotification*)n { + _sapp_tc_window_t* w = self.w; + if (!w) return; + sapp_event e; memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_FOCUSED; + _sapp_tc_send(w, &e); +} +- (void)windowDidResignKey:(NSNotification*)n { + _sapp_tc_window_t* w = self.w; + if (!w) return; + sapp_event e; memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_UNFOCUSED; + _sapp_tc_send(w, &e); +} +@end + +/*-- main window / app lifecycle (this header owns sapp_run on macOS) -------*/ + +/* drawable-sized depth-stencil + MSAA color textures for one window */ +static void _sapp_tc_ensure_swapchain_textures(_sapp_tc_window_t* w) { + if (w->fb_width <= 0 || w->fb_height <= 0) return; + if (w->depth_tex != nil && + (int)w->depth_tex.width == w->fb_width && + (int)w->depth_tex.height == w->fb_height) return; + id dev = w->layer.device; + MTLTextureDescriptor* dd = [MTLTextureDescriptor + texture2DDescriptorWithPixelFormat:MTLPixelFormatDepth32Float_Stencil8 + width:(NSUInteger)w->fb_width height:(NSUInteger)w->fb_height mipmapped:NO]; + dd.usage = MTLTextureUsageRenderTarget; + dd.storageMode = MTLStorageModePrivate; + dd.sampleCount = (NSUInteger)w->desc.sample_count; + dd.textureType = w->desc.sample_count > 1 ? MTLTextureType2DMultisample : MTLTextureType2D; + w->depth_tex = [dev newTextureWithDescriptor:dd]; + if (w->desc.sample_count > 1) { + MTLTextureDescriptor* md = [MTLTextureDescriptor + texture2DDescriptorWithPixelFormat:w->color_fmt + width:(NSUInteger)w->fb_width height:(NSUInteger)w->fb_height mipmapped:NO]; + md.usage = MTLTextureUsageRenderTarget; + md.storageMode = MTLStorageModePrivate; + md.sampleCount = (NSUInteger)w->desc.sample_count; + md.textureType = MTLTextureType2DMultisample; + w->msaa_tex = [dev newTextureWithDescriptor:md]; + } +} + +/* re-sample scale + sizes, keep the layer drawable in sync, emit RESIZED. + Called from the tick, windowDidResize / windowDidChangeScreen, and once + silently right after window creation (allow_event=false, sokol parity: + the startup update never fires RESIZED). */ +static void _sapp_tc_update_main_dimensions(bool allow_event) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w || !w->view) return; + CGFloat scale = 1.0; + if (_sapp_tc.app.desc.high_dpi) { + NSScreen* screen = w->window.screen ? w->window.screen : [NSScreen mainScreen]; + scale = screen.backingScaleFactor; + if (scale <= 0.0) scale = 1.0; + } + w->dpi_scale = (float)scale; + const NSSize sz = w->view.bounds.size; + int fbw = (int)((CGFloat)sz.width * scale); + int fbh = (int)((CGFloat)sz.height * scale); + if (fbw <= 0 || fbh <= 0) return; + /* re-apply contentsScale every time: live-resize installs a non-scaling + layer placement that would otherwise stick (sokol_app.h does the same) */ + w->layer.contentsScale = scale; + if ((int)w->layer.drawableSize.width != fbw || (int)w->layer.drawableSize.height != fbh) { + w->layer.drawableSize = CGSizeMake((CGFloat)fbw, (CGFloat)fbh); + } + const bool changed = (fbw != w->fb_width) || (fbh != w->fb_height) || + ((int)sz.width != w->win_width) || ((int)sz.height != w->win_height); + w->fb_width = fbw; + w->fb_height = fbh; + w->win_width = (int)sz.width; + w->win_height = (int)sz.height; + _sapp_tc_ensure_swapchain_textures(w); + if (changed && allow_event) { + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_RESIZED; + _sapp_tc_send(w, &e); + } +} + +/* one main-window frame: timing -> dimensions -> init_cb (first tick only) + -> frame_cb -> quit check. The drawable is NOT acquired here; it is + acquired lazily by sapp_get_swapchain() (upstream parity) and released + when the tick ends. `link` is nil when driven by the fallback timer. */ +static void _sapp_tc_main_tick(CADisplayLink* link) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w || w->in_tick) return; + _sapp_tc_timing_update(&_sapp_tc.app.timing); /* feeds the fallback estimate */ + if (link) { + if (w->last_tick_time > 0.0) { + const double dt = link.timestamp - w->last_tick_time; + if ((dt > 0.000001) && (dt < 0.1)) w->frame_duration = dt; + } + w->last_tick_time = link.timestamp; + } + _sapp_tc_update_main_dimensions(true); + @autoreleasepool { + w->in_tick = true; + if (!_sapp_tc.app.init_called) { + _sapp_tc.app.init_called = true; + if (_sapp_tc.app.desc.init_cb) { + _sapp_tc.app.desc.init_cb(); + } else if (_sapp_tc.app.desc.init_userdata_cb) { + _sapp_tc.app.desc.init_userdata_cb(_sapp_tc.app.desc.user_data); + } + } + if (_sapp_tc.app.desc.frame_cb) { + _sapp_tc.app.desc.frame_cb(); + } else if (_sapp_tc.app.desc.frame_userdata_cb) { + _sapp_tc.app.desc.frame_userdata_cb(_sapp_tc.app.desc.user_data); + } + _sapp_tc.app.frame_count++; + w->in_tick = false; + w->frame_drawable = nil; /* presented by sg_commit; release our ref */ + } + if (_sapp_tc.app.quit_requested || _sapp_tc.app.quit_ordered) { + [w->window performClose:nil]; + } +} + +static void _sapp_tc_start_fallback_timer(void) { + if (_sapp_tc.app.fallback_timer != nil || _sapp_tc.app.dlg == nil) return; + NSTimer* t = [NSTimer timerWithTimeInterval:(1.0 / 60.0) + target:_sapp_tc.app.dlg + selector:@selector(fallbackTick:) + userInfo:nil + repeats:YES]; + [[NSRunLoop mainRunLoop] addTimer:t forMode:NSRunLoopCommonModes]; + _sapp_tc.app.fallback_timer = t; +} + +static void _sapp_tc_stop_fallback_timer(void) { + if (_sapp_tc.app.fallback_timer == nil) return; + [_sapp_tc.app.fallback_timer invalidate]; + _sapp_tc.app.fallback_timer = nil; +} + +static void _sapp_tc_create_main_window(void) { + const sapp_desc* d = &_sapp_tc.app.desc; + int slot = -1; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + if (_sapp_tc.windows[i] == 0) { slot = i; break; } + } + if (slot < 0) return; + + _sapp_tc_window_t* w = new _sapp_tc_window_t(); + w->win_id = ++_sapp_tc.next_id; + w->is_main = true; + w->color_fmt = MTLPixelFormatRGB10A2Unorm; /* TrussC 10-bit output */ + w->dpi_scale = 1.0f; + + /* content size in points; 0 = 4/5 of the main screen (sokol parity) */ + int width = d->width; + int height = d->height; + const NSRect screen_rect = NSScreen.mainScreen.frame; + if (width <= 0) width = (int)(screen_rect.size.width * 0.8); + if (height <= 0) height = (int)(screen_rect.size.height * 0.8); + + /* per-window desc: the app callbacks are reached via the trampoline */ + w->desc.width = width; + w->desc.height = height; + w->desc.sample_count = d->sample_count; + w->desc.no_high_dpi = !d->high_dpi; + w->desc.event_cb = _sapp_tc_main_event_tramp; + + id dev = MTLCreateSystemDefaultDevice(); + _sapp_tc.app.device = dev; + + const NSRect rect = NSMakeRect(0, 0, width, height); + const NSWindowStyleMask style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | + NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable; + w->window = [[NSWindow alloc] initWithContentRect:rect styleMask:style + backing:NSBackingStoreBuffered defer:NO]; + w->window.title = [NSString stringWithUTF8String:_sapp_tc.app.window_title]; + w->window.releasedWhenClosed = NO; /* the object must outlive performClose */ + w->window.acceptsMouseMovedEvents = YES; + w->window.restorable = YES; + + _sapp_tc_view* view = [[_sapp_tc_view alloc] initWithFrame:rect]; + view.w = w; + w->view = view; + w->layer = [CAMetalLayer layer]; + w->layer.device = dev; + w->layer.pixelFormat = w->color_fmt; + w->layer.opaque = YES; + w->layer.magnificationFilter = kCAFilterNearest; + w->layer.framebufferOnly = NO; /* captureWindow() reads the drawable */ + view.wantsLayer = YES; + view.layer = w->layer; + [view registerForDraggedTypes:@[NSPasteboardTypeFileURL]]; + w->window.contentView = view; + [w->window makeFirstResponder:view]; + + _sapp_tc_win_delegate* del = [_sapp_tc_win_delegate new]; + del.w = w; + w->delegate = del; + w->window.delegate = del; + + [w->window center]; + _sapp_tc.windows[slot] = w; + _sapp_tc.app.main = w; + _sapp_tc.app.valid = true; + + if (d->fullscreen) { + _sapp_tc.app.fullscreen = true; + [w->window toggleFullScreen:nil]; + } + [w->window makeKeyAndOrderFront:nil]; + _sapp_tc_update_main_dimensions(false); /* silent startup sizing */ + + /* window #0's vsync source; swap_interval > 1 divides the display rate */ + w->link = [view displayLinkWithTarget:view selector:@selector(tick:)]; + if (d->swap_interval > 1) { + const float maxfps = (float)[NSScreen mainScreen].maximumFramesPerSecond; + const float p = maxfps / (float)d->swap_interval; + w->link.preferredFrameRateRange = CAFrameRateRangeMake(p, p, p); + } + [w->link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; +} + +@interface _sapp_tc_app_delegate : NSObject +@end + +@implementation _sapp_tc_app_delegate +- (void)applicationDidFinishLaunching:(NSNotification*)n { + (void)n; + /* activation policy must be set before window creation (sokol #1500) */ + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + _sapp_tc_init_cursors(); + _sapp_tc_create_main_window(); + [NSEvent setMouseCoalescingEnabled:NO]; + [NSApp activateIgnoringOtherApps:YES]; + /* focus workaround (sokol #982): make sure the window has focus even if + the first tick's init callback blocks for a long time */ + NSEvent* focusevent = [NSEvent otherEventWithType:NSEventTypeAppKitDefined + location:NSZeroPoint + modifierFlags:0 + timestamp:0 + windowNumber:0 + context:nil + subtype:NSEventSubtypeApplicationActivated + data1:0 + data2:0]; + [NSApp postEvent:focusevent atStart:YES]; +} +- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)sender { + (void)sender; + return YES; +} +- (void)applicationWillTerminate:(NSNotification*)n { + (void)n; + /* stop all tick sources first so no frame runs during teardown */ + _sapp_tc_stop_fallback_timer(); + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* w = _sapp_tc.windows[i]; + if (w && w->link) { [w->link invalidate]; w->link = nil; } + } + /* user cleanup runs BEFORE any GPU object release (the app shuts down + sokol_gfx here; we only hold the device/layer references) */ + if (!_sapp_tc.app.cleanup_called) { + _sapp_tc.app.cleanup_called = true; + if (_sapp_tc.app.desc.cleanup_cb) { + _sapp_tc.app.desc.cleanup_cb(); + } else if (_sapp_tc.app.desc.cleanup_userdata_cb) { + _sapp_tc.app.desc.cleanup_userdata_cb(_sapp_tc.app.desc.user_data); + } + } + if (_sapp_tc.app.keyup_monitor) { + [NSEvent removeMonitor:_sapp_tc.app.keyup_monitor]; + _sapp_tc.app.keyup_monitor = nil; + } + if (_sapp_tc.app.clipboard) { free(_sapp_tc.app.clipboard); _sapp_tc.app.clipboard = 0; } + if (_sapp_tc.app.drop_buffer) { free(_sapp_tc.app.drop_buffer); _sapp_tc.app.drop_buffer = 0; } +} +- (void)fallbackTick:(NSTimer*)t { + (void)t; + _sapp_tc_main_tick(nil); +} +@end + +/*-- public API -------------------------------------------------------------*/ +extern "C" { + +sapp_window sapp_create_window(const sapp_window_desc* desc) { + sapp_window invalid = { 0 }; + if (!desc) return invalid; + _sapp_tc_init_keytable(); + + int slot = -1; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + if (_sapp_tc.windows[i] == 0) { slot = i; break; } + } + if (slot < 0) return invalid; + + _sapp_tc_window_t* w = new _sapp_tc_window_t(); + w->win_id = ++_sapp_tc.next_id; + w->desc = *desc; + if (w->desc.width <= 0) w->desc.width = 640; + if (w->desc.height <= 0) w->desc.height = 480; + if (w->desc.sample_count <= 0) w->desc.sample_count = 1; + w->dpi_scale = 1.0f; + + const int px = (desc->x >= 0) ? desc->x : 120; + const int py = (desc->y >= 0) ? desc->y : 120; + NSRect rect = NSMakeRect(px, py, w->desc.width, w->desc.height); + NSWindowStyleMask style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | + NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable; + if (w->desc.borderless) style = NSWindowStyleMaskBorderless; + w->window = [[NSWindow alloc] initWithContentRect:rect styleMask:style + backing:NSBackingStoreBuffered defer:NO]; + if (w->desc.title) w->window.title = [NSString stringWithUTF8String:w->desc.title]; + w->window.releasedWhenClosed = NO; + w->window.acceptsMouseMovedEvents = YES; + + _sapp_tc_view* view = [[_sapp_tc_view alloc] initWithFrame:rect]; + view.w = w; + w->view = view; + w->layer = [CAMetalLayer layer]; + w->layer.device = (__bridge id)w->desc.mtl_device; + w->layer.pixelFormat = MTLPixelFormatBGRA8Unorm; + w->layer.opaque = YES; + view.wantsLayer = YES; + view.layer = w->layer; + w->window.contentView = view; + + _sapp_tc_win_delegate* del = [_sapp_tc_win_delegate new]; + del.w = w; + w->delegate = del; + w->window.delegate = del; + + _sapp_tc.windows[slot] = w; + [w->window makeKeyAndOrderFront:nil]; + + /* per-window display link: fires on the main run loop at THIS window's + display rate; auto-suspends while the view is not visible (macOS 14+) */ + w->link = [view displayLinkWithTarget:view selector:@selector(tick:)]; + [w->link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + + sapp_window handle = { w->win_id }; + return handle; +} + +void sapp_destroy_window(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w) return; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + if (_sapp_tc.windows[i] == w) { _sapp_tc.windows[i] = 0; break; } + } + w->desc.close_cb = 0; /* no callback from the orderOut below */ + if (w->link) { [w->link invalidate]; w->link = nil; } + w->view.w = 0; + w->delegate.w = 0; + if (w->window) { + w->window.delegate = nil; + [w->window orderOut:nil]; + w->window = nil; + } + w->view = nil; + w->delegate = nil; + w->layer = nil; + w->depth_tex = nil; + w->msaa_tex = nil; + w->frame_drawable = nil; + delete w; +} + +bool sapp_window_valid(sapp_window win) { + return _sapp_tc_lookup(win) != 0; +} + +int sapp_window_width(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w || !w->view) return 0; + return (int)w->view.bounds.size.width; +} + +int sapp_window_height(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w || !w->view) return 0; + return (int)w->view.bounds.size.height; +} + +int sapp_window_framebuffer_width(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->fb_width : 0; +} + +int sapp_window_framebuffer_height(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->fb_height : 0; +} + +float sapp_window_dpi_scale(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w) return 1.0f; + if (w->dpi_scale > 0.0f && w->fb_width > 0) return w->dpi_scale; + return w->desc.no_high_dpi ? 1.0f : (float)w->window.backingScaleFactor; +} + +int sapp_window_sample_count(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->desc.sample_count : 1; +} + +bool sapp_window_occluded(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->occluded : false; +} + +void sapp_window_set_title(sapp_window win, const char* title) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (w && w->window && title) { + w->window.title = [NSString stringWithUTF8String:title]; + } +} + +double sapp_window_frame_duration(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w) return 1.0 / 60.0; + return (w->frame_duration > 0.0) ? w->frame_duration : 1.0 / 60.0; +} + +const void* sapp_window_metal_current_drawable(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w || !w->in_tick) return 0; + return (__bridge const void*)w->frame_drawable; +} + +const void* sapp_window_metal_depth_stencil_texture(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w || !w->in_tick) return 0; + return (__bridge const void*)w->depth_tex; +} + +const void* sapp_window_metal_msaa_color_texture(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w || !w->in_tick || w->desc.sample_count <= 1) return 0; + return (__bridge const void*)w->msaa_tex; +} + +const void* sapp_window_macos_get_window(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? (__bridge const void*)w->window : 0; +} + +/* D3D11 / Win32 / GL / X11 handles do not exist on macOS */ +const void* sapp_window_d3d11_render_view(sapp_window win) { (void)win; return 0; } +const void* sapp_window_d3d11_resolve_view(sapp_window win) { (void)win; return 0; } +const void* sapp_window_d3d11_depth_stencil_view(sapp_window win) { (void)win; return 0; } +const void* sapp_window_win32_get_hwnd(sapp_window win) { (void)win; return 0; } +const void* sapp_window_x11_get_window(sapp_window win) { (void)win; return 0; } +uint32_t sapp_window_gl_framebuffer(sapp_window win) { (void)win; return 0; } + +/*-- sokol_app.h public API (macOS implementation lives here) ---------------*/ + +void sapp_run(const sapp_desc* desc) { + if (!desc) return; + _sapp_tc.app.desc = *desc; + sapp_desc* d = &_sapp_tc.app.desc; + if (d->sample_count <= 0) d->sample_count = 1; + if (d->swap_interval <= 0) d->swap_interval = 1; + if (d->clipboard_size <= 0) d->clipboard_size = 8192; + if (d->max_dropped_files <= 0) d->max_dropped_files = 1; + if (d->max_dropped_file_path_length <= 0) d->max_dropped_file_path_length = 2048; + strncpy(_sapp_tc.app.window_title, d->window_title ? d->window_title : "sokol", + sizeof(_sapp_tc.app.window_title) - 1); + d->window_title = _sapp_tc.app.window_title; + if (d->enable_clipboard) { + _sapp_tc.app.clipboard_enabled = true; + _sapp_tc.app.clipboard_size = d->clipboard_size; + _sapp_tc.app.clipboard = (char*)calloc(1, (size_t)d->clipboard_size); + } + if (d->enable_dragndrop) { + _sapp_tc.app.drop_enabled = true; + _sapp_tc.app.drop_max_files = d->max_dropped_files; + _sapp_tc.app.drop_max_path_length = d->max_dropped_file_path_length; + _sapp_tc.app.drop_buffer = (char*)calloc( + (size_t)d->max_dropped_files, (size_t)d->max_dropped_file_path_length); + } + _sapp_tc.app.mouse_shown = true; + _sapp_tc.app.current_cursor = SAPP_MOUSECURSOR_DEFAULT; + _sapp_tc_timing_reset(&_sapp_tc.app.timing); + _sapp_tc_init_keytable(); + + [NSApplication sharedApplication]; + _sapp_tc.app.dlg = [[_sapp_tc_app_delegate alloc] init]; + NSApp.delegate = _sapp_tc.app.dlg; + /* Cocoa swallows key-up events while Cmd is held; force-forward them + (same workaround as sokol_app.h / GLFW) */ + _sapp_tc.app.keyup_monitor = [NSEvent + addLocalMonitorForEventsMatchingMask:NSEventMaskKeyUp + handler:^NSEvent* (NSEvent* event) { + if ([event modifierFlags] & NSEventModifierFlagCommand) { + [[NSApp keyWindow] sendEvent:event]; + } + return event; + }]; + [NSApp run]; + /* never returns; cleanup runs in applicationWillTerminate */ +} + +bool sapp_isvalid(void) { + return _sapp_tc.app.valid; +} + +int sapp_width(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return (w && w->fb_width > 0) ? w->fb_width : 1; +} + +float sapp_widthf(void) { + return (float)sapp_width(); +} + +int sapp_height(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return (w && w->fb_height > 0) ? w->fb_height : 1; +} + +float sapp_heightf(void) { + return (float)sapp_height(); +} + +int sapp_sample_count(void) { + return _sapp_tc.app.desc.sample_count > 0 ? _sapp_tc.app.desc.sample_count : 1; +} + +bool sapp_high_dpi(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return _sapp_tc.app.desc.high_dpi && w && (w->dpi_scale >= 1.5f); +} + +float sapp_dpi_scale(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (w) return w->dpi_scale; + /* before sapp_run: report the main screen's scale (deliberate deviation + from sokol_app.h, which returns 0 here) */ + const float s = (float)[NSScreen mainScreen].backingScaleFactor; + return s > 0.0f ? s : 1.0f; +} + +uint64_t sapp_frame_count(void) { + return _sapp_tc.app.frame_count; +} + +double sapp_frame_duration(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w) return 1.0 / 60.0; + if (_sapp_tc.app.fallback_timer != nil) return _sapp_tc.app.timing.smooth_dt; + if (w->frame_duration > 0.0) return w->frame_duration; + const double maxfps = (double)[NSScreen mainScreen].maximumFramesPerSecond; + return (maxfps > 0.0) ? (1.0 / maxfps) : (1.0 / 60.0); +} + +void sapp_request_quit(void) { + _sapp_tc.app.quit_requested = true; +} + +void sapp_cancel_quit(void) { + _sapp_tc.app.quit_requested = false; +} + +void sapp_quit(void) { + _sapp_tc.app.quit_ordered = true; +} + +void sapp_consume_event(void) { + _sapp_tc.app.event_consumed = true; +} + +void sapp_skip_present(void) { + /* API parity: stored but ignored on Metal (upstream behaves the same; + not starting a swapchain pass already skips the present) */ + _sapp_tc.app.skip_present = true; +} + +void sapp_show_keyboard(bool show) { + (void)show; /* on-screen keyboard: mobile only */ +} + +bool sapp_keyboard_shown(void) { + return false; +} + +void sapp_show_mouse(bool show) { + if (_sapp_tc.app.mouse_shown != show) { + _sapp_tc_apply_cursor(_sapp_tc.app.current_cursor, show); + } +} + +bool sapp_mouse_shown(void) { + return _sapp_tc.app.mouse_shown; +} + +void sapp_set_mouse_cursor(sapp_mouse_cursor cursor) { + if ((int)cursor < 0 || (int)cursor >= _SAPP_MOUSECURSOR_NUM) return; + if (cursor != _sapp_tc.app.current_cursor) { + _sapp_tc_apply_cursor(cursor, _sapp_tc.app.mouse_shown); + } +} + +sapp_mouse_cursor sapp_get_mouse_cursor(void) { + return _sapp_tc.app.current_cursor; +} + +sapp_mouse_cursor sapp_bind_mouse_cursor_image(sapp_mouse_cursor cursor, const sapp_image_desc* desc) { + if ((int)cursor < 0 || (int)cursor >= _SAPP_MOUSECURSOR_NUM) return cursor; + if (!desc || !desc->pixels.ptr || desc->width <= 0 || desc->height <= 0) return cursor; + if (desc->pixels.size < (size_t)(desc->width * desc->height * 4)) return cursor; + sapp_unbind_mouse_cursor_image(cursor); + NSBitmapImageRep* rep = [[NSBitmapImageRep alloc] + initWithBitmapDataPlanes:NULL + pixelsWide:desc->width pixelsHigh:desc->height + bitsPerSample:8 samplesPerPixel:4 hasAlpha:YES isPlanar:NO + colorSpaceName:NSCalibratedRGBColorSpace + bitmapFormat:NSBitmapFormatAlphaNonpremultiplied + bytesPerRow:desc->width * 4 bitsPerPixel:32]; + if (rep == nil) return cursor; + memcpy(rep.bitmapData, desc->pixels.ptr, (size_t)(desc->width * desc->height * 4)); + NSImage* img = [[NSImage alloc] initWithSize:NSMakeSize(desc->width, desc->height)]; + [img addRepresentation:rep]; + NSCursor* cur = [[NSCursor alloc] initWithImage:img + hotSpot:NSMakePoint(desc->cursor_hotspot_x, desc->cursor_hotspot_y)]; + _sapp_tc.app.custom_cursors[cursor] = cur; + _sapp_tc.app.custom_cursor_bound[cursor] = true; + if (_sapp_tc.app.current_cursor == cursor) { + _sapp_tc_apply_cursor(cursor, _sapp_tc.app.mouse_shown); + } + return cursor; +} + +void sapp_unbind_mouse_cursor_image(sapp_mouse_cursor cursor) { + if ((int)cursor < 0 || (int)cursor >= _SAPP_MOUSECURSOR_NUM) return; + if (!_sapp_tc.app.custom_cursor_bound[cursor]) return; + _sapp_tc.app.custom_cursor_bound[cursor] = false; + if (_sapp_tc.app.current_cursor == cursor) { + _sapp_tc_apply_cursor(cursor, _sapp_tc.app.mouse_shown); + } + _sapp_tc.app.custom_cursors[cursor] = nil; +} + +void sapp_set_clipboard_string(const char* str) { + if (!_sapp_tc.app.clipboard_enabled || !str) return; + @autoreleasepool { + NSPasteboard* pboard = [NSPasteboard generalPasteboard]; + [pboard declareTypes:@[NSPasteboardTypeString] owner:nil]; + [pboard setString:[NSString stringWithUTF8String:str] forType:NSPasteboardTypeString]; + } + strncpy(_sapp_tc.app.clipboard, str, (size_t)_sapp_tc.app.clipboard_size - 1); + _sapp_tc.app.clipboard[_sapp_tc.app.clipboard_size - 1] = 0; +} + +const char* sapp_get_clipboard_string(void) { + if (!_sapp_tc.app.clipboard_enabled || !_sapp_tc.app.clipboard) return ""; + @autoreleasepool { + _sapp_tc.app.clipboard[0] = 0; + NSPasteboard* pboard = [NSPasteboard generalPasteboard]; + if ([pboard.types containsObject:NSPasteboardTypeString]) { + NSString* str = [pboard stringForType:NSPasteboardTypeString]; + if (str) { + strncpy(_sapp_tc.app.clipboard, str.UTF8String, + (size_t)_sapp_tc.app.clipboard_size - 1); + _sapp_tc.app.clipboard[_sapp_tc.app.clipboard_size - 1] = 0; + } + } + } + return _sapp_tc.app.clipboard; +} + +void sapp_set_window_title(const char* str) { + if (!str) return; + strncpy(_sapp_tc.app.window_title, str, sizeof(_sapp_tc.app.window_title) - 1); + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (w && w->window) { + w->window.title = [NSString stringWithUTF8String:_sapp_tc.app.window_title]; + } +} + +bool sapp_is_fullscreen(void) { + return _sapp_tc.app.fullscreen; +} + +void sapp_toggle_fullscreen(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w || !w->window) return; + /* optimistic flip; the delegate notifications are the authority */ + _sapp_tc.app.fullscreen = !_sapp_tc.app.fullscreen; + [w->window toggleFullScreen:nil]; +} + +int sapp_get_num_dropped_files(void) { + return _sapp_tc.app.drop_enabled ? _sapp_tc.app.drop_num_files : 0; +} + +const char* sapp_get_dropped_file_path(int index) { + if (!_sapp_tc.app.drop_enabled || !_sapp_tc.app.drop_buffer) return ""; + if (index < 0 || index >= _sapp_tc.app.drop_num_files) return ""; + return _sapp_tc.app.drop_buffer + (size_t)index * (size_t)_sapp_tc.app.drop_max_path_length; +} + +sapp_pixel_format sapp_color_format(void) { + return SAPP_PIXELFORMAT_RGB10A2; /* TrussC 10-bit output on Metal */ +} + +sapp_pixel_format sapp_depth_format(void) { + return SAPP_PIXELFORMAT_DEPTH_STENCIL; +} + +const void* sapp_macos_get_window(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return w ? (__bridge const void*)w->window : 0; +} + +const void* sapp_metal_get_device(void) { + return (__bridge const void*)_sapp_tc.app.device; +} + +/* main-window drawable: acquired lazily HERE (upstream parity — sokol_gfx's + begin-pass is the acquisition point via sglue_swapchain()). Inside a tick + the drawable is cached so repeated queries stay safe; outside a tick each + call acquires a fresh drawable, exactly like sokol_app.h. */ +static id _sapp_tc_main_next_drawable(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w || !w->layer) return nil; + if (w->in_tick) { + if (w->frame_drawable == nil) { + w->frame_drawable = [w->layer nextDrawable]; + } + return w->frame_drawable; + } + return [w->layer nextDrawable]; +} + +const void* sapp_metal_get_current_drawable(void) { + return (__bridge const void*)_sapp_tc_main_next_drawable(); +} + +const void* sapp_metal_get_depth_stencil_texture(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return w ? (__bridge const void*)w->depth_tex : 0; +} + +const void* sapp_metal_get_msaa_color_texture(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w || w->desc.sample_count <= 1) return 0; + return (__bridge const void*)w->msaa_tex; +} + +sapp_environment sapp_get_environment(void) { + sapp_environment env; + memset(&env, 0, sizeof(env)); + env.defaults.color_format = sapp_color_format(); + env.defaults.depth_format = sapp_depth_format(); + env.defaults.sample_count = sapp_sample_count(); + env.metal.device = (__bridge const void*)_sapp_tc.app.device; + return env; +} + +sapp_swapchain sapp_get_swapchain(void) { + sapp_swapchain sc; + memset(&sc, 0, sizeof(sc)); + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w) return sc; + sc.width = sapp_width(); + sc.height = sapp_height(); + sc.sample_count = sapp_sample_count(); + sc.color_format = sapp_color_format(); + sc.depth_format = sapp_depth_format(); + sc.metal.current_drawable = (__bridge const void*)_sapp_tc_main_next_drawable(); + sc.metal.depth_stencil_texture = (__bridge const void*)w->depth_tex; + sc.metal.msaa_color_texture = (w->desc.sample_count > 1) + ? (__bridge const void*)w->msaa_tex : 0; + return sc; +} + +} /* extern "C" */ + +#elif defined(__APPLE__) && TARGET_OS_IPHONE +/*== iOS (Metal) ============================================================ + Implements the public sokol_app.h API on iOS plus the multi-window API + (as stubs -- there is exactly ONE UIWindow bound to the connecting + UIWindowScene, so a second window is not representable; + sapp_create_window() returns the invalid handle and logs). + + UIKit owns the loop: sapp_run() -> UIApplicationMain() never returns. + The real setup (UIWindow + CAMetalLayer + CADisplayLink) happens later, + scene-driven, inside scene:willConnectToSession: -- the scene lifecycle + is adopted PROGRAMMATICALLY via the app delegate's + configurationForConnectingSceneSession: (the TrussC Info-iOS.plist has + NO UIApplicationSceneManifest; that method is what makes scenes work). + The frame driver is the CADisplayLink firing displayLinkFired: every + vsync; backgrounding pauses the link (rendering while suspended is an + iOS watchdog kill) and fires SAPP_EVENTTYPE_SUSPENDED/RESUMED. + + Structure mirrors sokol_app.h's iOS backend including all TrussC + patches: runtime orientation control (_sapp_tc_ios_view_ctrl + + sapp_ios_set_supported_orientations), immersive mode (the NON-STATIC + global _sapp_ios_immersive_mode -- tcPlatform_ios.mm externs it by + name, keep it), view-bounds dimension update, drawable-size readback + after resize (sapp_width/height must match what Metal actually + renders), RGB10A2 10-bit layer + MSAA format, framebufferOnly=false + for captureWindow() readback, and sapp_ios_get_window() for the + document picker. Input is multi-touch plus an on-screen keyboard + driven through a hidden UITextField (CHAR + Enter/Space/Backspace + only). Upstream's GLES3/EAGL path is not ported -- TrussC iOS is + Metal-only. The tvOS press-event code is lifted but unexercised. */ +#if !defined(__OBJC__) +#error "sokol_app_tc.h iOS implementation must be compiled as Objective-C++ (.mm)" +#endif +#if !defined(SOKOL_METAL) +#error "sokol_app_tc.h: TrussC iOS builds are Metal-only (define SOKOL_METAL)" +#endif +#import +#import +#import +#import +#include +#include +#include + +#if defined(SOKOL_APP_IMPL_INCLUDED) +#error "sokol_app_tc.h owns the iOS implementation of the sapp_* API; include sokol_app.h WITHOUT an implementation define in this TU" +#endif + +#ifndef SOKOL_ASSERT +#include +#define SOKOL_ASSERT(c) assert(c) +#endif +#ifndef _SOKOL_PRIVATE +#define _SOKOL_PRIVATE static +#endif +#ifndef _SOKOL_UNUSED +#define _SOKOL_UNUSED(x) (void)(x) +#endif +#ifndef SOKOL_API_IMPL +#define SOKOL_API_IMPL +#endif +#ifndef SOKOL_UNREACHABLE +#define SOKOL_UNREACHABLE SOKOL_ASSERT(false) +#endif + +/* the lifted upstream code keys Apple/iOS forks on these */ +#define _SAPP_APPLE (1) +#define _SAPP_IOS (1) + +#define _SAPP_MAX_TITLE_LENGTH (128) +#define _SAPP_FALLBACK_DEFAULT_WINDOW_WIDTH (640) +#define _SAPP_FALLBACK_DEFAULT_WINDOW_HEIGHT (480) +#define _sapp_tc_def(val, def) (((val) == 0) ? (def) : (val)) +/* always ObjC++ here, so the C++ forms are unconditional (ARC compatible) */ +#define _SAPP_STRUCT(TYPE, NAME) TYPE NAME = {} +#define _SAPP_CLEAR_ARC_STRUCT(type, item) { item = type(); } +#if __has_feature(objc_arc) +#define _SAPP_OBJC_RELEASE(obj) { obj = nil; } +#else +#define _SAPP_OBJC_RELEASE(obj) { [obj release]; obj = nil; } +#endif + +/* controlled failure instead of sokol_app.h's log-item machinery */ +#define _SAPP_PANIC(code) do { fprintf(stderr, "sokol_app_tc.h: panic: " #code "\n"); abort(); } while (0) +#define _SAPP_ERROR(code) fprintf(stderr, "sokol_app_tc.h: error: " #code "\n") +#define _SAPP_ERROR_MSG(code, msg) fprintf(stderr, "sokol_app_tc.h: error: " #code ": %s\n", msg) +#define _SAPP_WARN_MSG(code, msg) fprintf(stderr, "sokol_app_tc.h: warn: " #code ": %s\n", msg) +#define _SAPP_INFO_MSG(code, msg) fprintf(stderr, "sokol_app_tc.h: info: " #code ": %s\n", msg) +typedef struct { + #if defined(_SAPP_APPLE) + struct { + mach_timebase_info_data_t timebase; + uint64_t start; + } mach; + #elif defined(_SAPP_EMSCRIPTEN) + int _dummy; + #elif defined(_SAPP_WIN32) + struct { + LARGE_INTEGER freq; + LARGE_INTEGER start; + } win; + #else // Linux, Android, ... + #ifdef CLOCK_MONOTONIC + #define _SAPP_CLOCK_MONOTONIC CLOCK_MONOTONIC + #else + // on some embedded platforms, CLOCK_MONOTONIC isn't defined + #define _SAPP_CLOCK_MONOTONIC (1) + #endif + struct { + uint64_t start; + } posix; + #endif +} _sapp_tc_timestamp_t; + +_SOKOL_PRIVATE int64_t _sapp_tc_int64_muldiv(int64_t value, int64_t numer, int64_t denom) { + int64_t q = value / denom; + int64_t r = value % denom; + return q * numer + r * numer / denom; +} + +_SOKOL_PRIVATE void _sapp_tc_timestamp_init(_sapp_tc_timestamp_t* ts) { + #if defined(_SAPP_APPLE) + mach_timebase_info(&ts->mach.timebase); + ts->mach.start = mach_absolute_time(); + #elif defined(_SAPP_EMSCRIPTEN) + (void)ts; + #elif defined(_SAPP_WIN32) + QueryPerformanceFrequency(&ts->win.freq); + QueryPerformanceCounter(&ts->win.start); + #else + struct timespec tspec; + clock_gettime(_SAPP_CLOCK_MONOTONIC, &tspec); + ts->posix.start = (uint64_t)tspec.tv_sec*1000000000 + (uint64_t)tspec.tv_nsec; + #endif +} + +_SOKOL_PRIVATE double _sapp_tc_timestamp_now(_sapp_tc_timestamp_t* ts) { + #if defined(_SAPP_APPLE) + const uint64_t traw = mach_absolute_time() - ts->mach.start; + const uint64_t now = (uint64_t) _sapp_tc_int64_muldiv((int64_t)traw, (int64_t)ts->mach.timebase.numer, (int64_t)ts->mach.timebase.denom); + return (double)now / 1000000000.0; + #elif defined(_SAPP_EMSCRIPTEN) + (void)ts; + SOKOL_ASSERT(false); + return 0.0; + #elif defined(_SAPP_WIN32) + LARGE_INTEGER qpc; + QueryPerformanceCounter(&qpc); + const uint64_t now = (uint64_t)_sapp_tc_int64_muldiv(qpc.QuadPart - ts->win.start.QuadPart, 1000000000, ts->win.freq.QuadPart); + return (double)now / 1000000000.0; + #else + struct timespec tspec; + clock_gettime(_SAPP_CLOCK_MONOTONIC, &tspec); + const uint64_t now = ((uint64_t)tspec.tv_sec*1000000000 + (uint64_t)tspec.tv_nsec) - ts->posix.start; + return (double)now / 1000000000.0; + #endif +} + +typedef struct { + _sapp_tc_timestamp_t timestamp; + double dt_min; // config: min clamp value for unfiltered time delta (seconds) + double dt_max; // config: max clamp value for unfiltered time delta (seconds) + double dt_threshold; // config: threshold time delta for 'resetting' filtering (default: 0.004s, 4ms) + double alpha; // config: smoothing constant, lower values smoother, higher values faster response + double last; // last absolute time in seconds + double dt; // unfiltered frame delta in seconds, clamped to dt_min/dt_max + double ema; // intermediate ema-filter result + double smooth_dt; // smoothed frame delta in seconds +} _sapp_tc_timing_t; + +_SOKOL_PRIVATE void _sapp_tc_timing_init(_sapp_tc_timing_t* t) { + _sapp_tc_timestamp_init(&t->timestamp); + t->dt_min = 0.000001; // 1 us + t->dt_max = 0.1; // 100 ms + t->dt_threshold = 0.004; // 4ms + t->alpha = 0.025; + t->dt = 1.0 / 60.0; // a 'likely' non-null value + t->ema = t->dt; + t->smooth_dt = t->dt; +} + +_SOKOL_PRIVATE double _sapp_tc_timing_clamp(_sapp_tc_timing_t* t, double dt) { + SOKOL_ASSERT((t->dt_min > 0.0) && (t->dt_max > 0.0) && (t->dt_max >= t->dt_min)); + if (dt < t->dt_min) { + return t->dt_min; + } else if (dt > t->dt_max) { + return t->dt_max; + } else { + return dt; + } +} + +_SOKOL_PRIVATE void _sapp_tc_timing_delta(_sapp_tc_timing_t* t, double dt) { + // first clamp raw dt against min/max (min avoid division by zero, max + // may avoids glitches and 'death-spirals' during debugging + dt = _sapp_tc_timing_clamp(t, dt); + t->dt = dt; + const double error = fabs(dt - t->smooth_dt); + if (error > t->dt_threshold) { + // 'reset' filter when new delta is outside threshold + t->ema = dt; + t->smooth_dt = dt; + } else { + // simple ema-filter with fixed alpha + t->ema = t->ema + t->alpha * (dt - t->ema); + t->smooth_dt = _sapp_tc_timing_clamp(t, t->ema); + } +} + +_SOKOL_PRIVATE void _sapp_tc_timing_update(_sapp_tc_timing_t* t, double external_now) { + double now; + if (external_now == 0.0) { + now = _sapp_tc_timestamp_now(&t->timestamp); + } else { + now = external_now; + } + if (t->last > 0.0) { + double dt = now - t->last; + _sapp_tc_timing_delta(t, dt); + } + t->last = now; + +} + +_SOKOL_PRIVATE double _sapp_tc_timing_get(_sapp_tc_timing_t* t) { + return t->smooth_dt; +} + +#if defined(_SAPP_IOS) + +@interface _sapp_tc_scene_delegate : NSObject; +@end +@interface _sapp_tc_textfield_dlg : NSObject +- (void)keyboardWasShown:(NSNotification*)notif; +- (void)keyboardWillBeHidden:(NSNotification*)notif; +- (void)keyboardDidChangeFrame:(NSNotification*)notif; +@end + +// Modified by tettou771 for TrussC: custom view controller for runtime orientation control +@interface _sapp_tc_ios_view_ctrl : UIViewController +@end + +#if defined(SOKOL_METAL) + @interface _sapp_tc_ios_view : UIView + - (void)displayLinkFired:(id)sender; + @end +#else + @interface _sapp_tc_ios_view : GLKView + @end +#endif + +typedef struct { + UIWindow* window; + _sapp_tc_ios_view* view; + UITextField* textfield; + _sapp_tc_textfield_dlg* textfield_dlg; + NSUInteger supported_orientations; // UIInterfaceOrientationMask (default: all) + #if defined(SOKOL_METAL) + UIViewController* view_ctrl; + #else + GLKViewController* view_ctrl; + #endif + #if defined(SOKOL_METAL) + struct { + id device; + CAMetalLayer* layer; + CADisplayLink* display_link; + id depth_tex; + id msaa_tex; + struct { + CFTimeInterval timestamp; + CFTimeInterval frame_duration_sec; + } timing; + } mtl; + #else + EAGLContext* eagl_ctx; + #endif + bool suspended; +} _sapp_tc_ios_t; + +#endif // _SAPP_IOS + +typedef struct { + bool enabled; + int buf_size; + char* buffer; +} _sapp_tc_clipboard_t; + +typedef struct { + bool enabled; + int max_files; + int max_path_length; + int num_files; + int buf_size; + char* buffer; +} _sapp_tc_drop_t; + +typedef struct { + float x, y; + float dx, dy; + bool shown; + bool locked; + bool pos_valid; + sapp_mouse_cursor current_cursor; +} _sapp_tc_mouse_t; + +/* per-app state -- an iOS-sized subset of sokol_app.h's _sapp_t with the same + member names, so the lifted implementation code reads unchanged. mouse / + clipboard / drop / html5 / cursor members are inert on iOS (touch-only, no + OS clipboard or dnd surface) but kept so the shared lifted helpers and the + lifted public API compile verbatim. */ +typedef struct { + sapp_desc desc; + bool valid; + bool fullscreen; /* inert: iOS is inherently fullscreen */ + bool first_frame; + bool init_called; + bool cleanup_called; + bool quit_requested; /* inert: no user-driven quit path on iOS */ + bool quit_ordered; + bool event_consumed; + bool html5_ask_leave_site; /* inert */ + bool onscreen_keyboard_shown; + bool skip_present; /* inert: Metal presents via sokol_gfx commit */ + int window_width; + int window_height; + int framebuffer_width; + int framebuffer_height; + int sample_count; + int swap_interval; + float dpi_scale; + uint64_t frame_count; + sapp_event event; + _sapp_tc_mouse_t mouse; + _sapp_tc_clipboard_t clipboard; + _sapp_tc_drop_t drop; + _sapp_tc_timing_t timing; + _sapp_tc_ios_t ios; + char html5_canvas_selector[_SAPP_MAX_TITLE_LENGTH]; + char window_title[_SAPP_MAX_TITLE_LENGTH]; + bool custom_cursor_bound[_SAPP_MOUSECURSOR_NUM]; +} _sapp_tc_t; +static _sapp_tc_t _sapp_tc; +_SOKOL_PRIVATE void _sapp_tc_clear(void* ptr, size_t size) { + SOKOL_ASSERT(ptr && (size > 0)); + memset(ptr, 0, size); +} + +_SOKOL_PRIVATE void* _sapp_tc_malloc(size_t size) { + SOKOL_ASSERT(size > 0); + void* ptr; + if (_sapp_tc.desc.allocator.alloc_fn) { + ptr = _sapp_tc.desc.allocator.alloc_fn(size, _sapp_tc.desc.allocator.user_data); + } else { + ptr = malloc(size); + } + if (0 == ptr) { + _SAPP_PANIC(MALLOC_FAILED); + } + return ptr; +} + +_SOKOL_PRIVATE void* _sapp_tc_malloc_clear(size_t size) { + void* ptr = _sapp_tc_malloc(size); + _sapp_tc_clear(ptr, size); + return ptr; +} + +_SOKOL_PRIVATE void _sapp_tc_free(void* ptr) { + if (_sapp_tc.desc.allocator.free_fn) { + _sapp_tc.desc.allocator.free_fn(ptr, _sapp_tc.desc.allocator.user_data); + } else { + free(ptr); + } +} + +// ██ ██ ███████ ██ ██████ ███████ ██████ ███████ +// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +// ███████ █████ ██ ██████ █████ ██████ ███████ +// ██ ██ ██ ██ ██ ██ ██ ██ ██ +// ██ ██ ███████ ███████ ██ ███████ ██ ██ ███████ +// +// >>helpers + +// round float to int and at least 1 +_SOKOL_PRIVATE int _sapp_tc_roundf_gzero(float f) { + int val = (int)roundf(f); + if (val <= 0) { + val = 1; + } + return val; +} + +_SOKOL_PRIVATE void _sapp_tc_call_init(void) { + if (_sapp_tc.desc.init_cb) { + _sapp_tc.desc.init_cb(); + } else if (_sapp_tc.desc.init_userdata_cb) { + _sapp_tc.desc.init_userdata_cb(_sapp_tc.desc.user_data); + } + _sapp_tc.init_called = true; +} + +_SOKOL_PRIVATE void _sapp_tc_call_frame(void) { + if (_sapp_tc.init_called && !_sapp_tc.cleanup_called) { + if (_sapp_tc.desc.frame_cb) { + _sapp_tc.desc.frame_cb(); + } else if (_sapp_tc.desc.frame_userdata_cb) { + _sapp_tc.desc.frame_userdata_cb(_sapp_tc.desc.user_data); + } + } +} + +_SOKOL_PRIVATE void _sapp_tc_call_cleanup(void) { + if (!_sapp_tc.cleanup_called) { + if (_sapp_tc.desc.cleanup_cb) { + _sapp_tc.desc.cleanup_cb(); + } else if (_sapp_tc.desc.cleanup_userdata_cb) { + _sapp_tc.desc.cleanup_userdata_cb(_sapp_tc.desc.user_data); + } + _sapp_tc.cleanup_called = true; + } +} + +_SOKOL_PRIVATE bool _sapp_tc_call_event(const sapp_event* e) { + if (!_sapp_tc.cleanup_called) { + if (_sapp_tc.desc.event_cb) { + _sapp_tc.desc.event_cb(e); + } else if (_sapp_tc.desc.event_userdata_cb) { + _sapp_tc.desc.event_userdata_cb(e, _sapp_tc.desc.user_data); + } + } + if (_sapp_tc.event_consumed) { + _sapp_tc.event_consumed = false; + return true; + } else { + return false; + } +} + + +_SOKOL_PRIVATE char* _sapp_tc_dropped_file_path_ptr(int index) { + SOKOL_ASSERT(_sapp_tc.drop.buffer); + SOKOL_ASSERT((index >= 0) && (index <= _sapp_tc.drop.max_files)); + int offset = index * _sapp_tc.drop.max_path_length; + SOKOL_ASSERT(offset < _sapp_tc.drop.buf_size); + return &_sapp_tc.drop.buffer[offset]; +} + +/* Copy a string (either zero-terminated or with explicit length) + into a fixed size buffer with guaranteed zero-termination. + + Return false if the string didn't fit into the buffer and had to be clamped. + + FIXME: Currently UTF-8 strings might become invalid if the string + is clamped, because the last zero-byte might be written into + the middle of a multi-byte sequence. +*/ +_SOKOL_PRIVATE bool _sapp_tc_strcpy_range(const char* src, size_t src_len, char* dst, size_t dst_buf_len) { + SOKOL_ASSERT(src && dst && (dst_buf_len > 0)); + if (0 == src_len) { + src_len = dst_buf_len; + } + char* const end = &(dst[dst_buf_len-1]); + char c = 0; + for (size_t i = 0; i < dst_buf_len; i++) { + c = *src; + if (i >= src_len) { + c = 0; + } + if (c != 0) { + src++; + } + *dst++ = c; + } + // truncated? + if (c != 0) { + *end = 0; + return false; + } else { + return true; + } +} + +_SOKOL_PRIVATE bool _sapp_tc_strcpy(const char* src, char* dst, size_t dst_buf_len) { + return _sapp_tc_strcpy_range(src, 0, dst, dst_buf_len); +} + +_SOKOL_PRIVATE sapp_desc _sapp_tc_desc_defaults(const sapp_desc* desc) { + SOKOL_ASSERT((desc->allocator.alloc_fn && desc->allocator.free_fn) || (!desc->allocator.alloc_fn && !desc->allocator.free_fn)); + sapp_desc res = *desc; + res.sample_count = _sapp_tc_def(res.sample_count, 1); + res.swap_interval = _sapp_tc_def(res.swap_interval, 1); + if (0 == res.gl.major_version) { + #if defined(SOKOL_GLCORE) + res.gl.major_version = 4; + #if defined(_SAPP_APPLE) + res.gl.minor_version = 1; + #else + res.gl.minor_version = 3; + #endif + #elif defined(SOKOL_GLES3) + res.gl.major_version = 3; + #if defined(_SAPP_ANDROID) || defined(_SAPP_LINUX) + res.gl.minor_version = 1; + #else + res.gl.minor_version = 0; + #endif + #endif + } + res.html5.canvas_selector = _sapp_tc_def(res.html5.canvas_selector, "#canvas"); + res.clipboard_size = _sapp_tc_def(res.clipboard_size, 8192); + res.max_dropped_files = _sapp_tc_def(res.max_dropped_files, 1); + res.max_dropped_file_path_length = _sapp_tc_def(res.max_dropped_file_path_length, 2048); + res.window_title = _sapp_tc_def(res.window_title, "sokol"); + return res; +} + +_SOKOL_PRIVATE void _sapp_tc_init_state(const sapp_desc* desc) { + SOKOL_ASSERT(desc); + SOKOL_ASSERT(desc->width >= 0); + SOKOL_ASSERT(desc->height >= 0); + SOKOL_ASSERT(desc->sample_count >= 0); + SOKOL_ASSERT(desc->swap_interval >= 0); + SOKOL_ASSERT(desc->clipboard_size >= 0); + SOKOL_ASSERT(desc->max_dropped_files >= 0); + SOKOL_ASSERT(desc->max_dropped_file_path_length >= 0); + _SAPP_CLEAR_ARC_STRUCT(_sapp_tc_t, _sapp_tc); + _sapp_tc.desc = _sapp_tc_desc_defaults(desc); + _sapp_tc.first_frame = true; + // NOTE: _sapp_tc.desc.width/height may be 0! Platform backends need to deal with this + _sapp_tc.window_width = _sapp_tc.desc.width; + _sapp_tc.window_height = _sapp_tc.desc.height; + _sapp_tc.framebuffer_width = _sapp_tc.window_width; + _sapp_tc.framebuffer_height = _sapp_tc.window_height; + _sapp_tc.sample_count = _sapp_tc.desc.sample_count; + _sapp_tc.swap_interval = _sapp_tc.desc.swap_interval; + _sapp_tc_strcpy(_sapp_tc.desc.html5.canvas_selector, _sapp_tc.html5_canvas_selector, sizeof(_sapp_tc.html5_canvas_selector)); + _sapp_tc.desc.html5.canvas_selector = _sapp_tc.html5_canvas_selector; + _sapp_tc.html5_ask_leave_site = _sapp_tc.desc.html5.ask_leave_site; + _sapp_tc.clipboard.enabled = _sapp_tc.desc.enable_clipboard; + if (_sapp_tc.clipboard.enabled) { + _sapp_tc.clipboard.buf_size = _sapp_tc.desc.clipboard_size; + _sapp_tc.clipboard.buffer = (char*) _sapp_tc_malloc_clear((size_t)_sapp_tc.clipboard.buf_size); + } + _sapp_tc.drop.enabled = _sapp_tc.desc.enable_dragndrop; + if (_sapp_tc.drop.enabled) { + _sapp_tc.drop.max_files = _sapp_tc.desc.max_dropped_files; + _sapp_tc.drop.max_path_length = _sapp_tc.desc.max_dropped_file_path_length; + _sapp_tc.drop.buf_size = _sapp_tc.drop.max_files * _sapp_tc.drop.max_path_length; + _sapp_tc.drop.buffer = (char*) _sapp_tc_malloc_clear((size_t)_sapp_tc.drop.buf_size); + } + _sapp_tc_strcpy(_sapp_tc.desc.window_title, _sapp_tc.window_title, sizeof(_sapp_tc.window_title)); + _sapp_tc.desc.window_title = _sapp_tc.window_title; + _sapp_tc.dpi_scale = 1.0f; + _sapp_tc.fullscreen = _sapp_tc.desc.fullscreen; + _sapp_tc.mouse.shown = true; + _sapp_tc_timing_init(&_sapp_tc.timing); +} + +_SOKOL_PRIVATE void _sapp_tc_discard_state(void) { + if (_sapp_tc.clipboard.enabled) { + SOKOL_ASSERT(_sapp_tc.clipboard.buffer); + _sapp_tc_free((void*)_sapp_tc.clipboard.buffer); + } + if (_sapp_tc.drop.enabled) { + SOKOL_ASSERT(_sapp_tc.drop.buffer); + _sapp_tc_free((void*)_sapp_tc.drop.buffer); + } + for (int i = 0; i < _SAPP_MOUSECURSOR_NUM; i++) { + sapp_unbind_mouse_cursor_image((sapp_mouse_cursor) i); + } + _SAPP_CLEAR_ARC_STRUCT(_sapp_tc_t, _sapp_tc); +} + +_SOKOL_PRIVATE void _sapp_tc_init_event(sapp_event_type type) { + _sapp_tc_clear(&_sapp_tc.event, sizeof(_sapp_tc.event)); + _sapp_tc.event.type = type; + _sapp_tc.event.frame_count = _sapp_tc.frame_count; + _sapp_tc.event.mouse_button = SAPP_MOUSEBUTTON_INVALID; + _sapp_tc.event.window_width = _sapp_tc.window_width; + _sapp_tc.event.window_height = _sapp_tc.window_height; + _sapp_tc.event.framebuffer_width = _sapp_tc.framebuffer_width; + _sapp_tc.event.framebuffer_height = _sapp_tc.framebuffer_height; + _sapp_tc.event.mouse_x = _sapp_tc.mouse.x; + _sapp_tc.event.mouse_y = _sapp_tc.mouse.y; + _sapp_tc.event.mouse_dx = _sapp_tc.mouse.dx; + _sapp_tc.event.mouse_dy = _sapp_tc.mouse.dy; +} + +_SOKOL_PRIVATE bool _sapp_tc_events_enabled(void) { + /* only send events when an event callback is set, and the init function was called */ + return (_sapp_tc.desc.event_cb || _sapp_tc.desc.event_userdata_cb) && _sapp_tc.init_called; +} + +_SOKOL_PRIVATE void _sapp_tc_clear_drop_buffer(void) { + if (_sapp_tc.drop.enabled) { + SOKOL_ASSERT(_sapp_tc.drop.buffer); + _sapp_tc_clear(_sapp_tc.drop.buffer, (size_t)_sapp_tc.drop.buf_size); + } +} + +_SOKOL_PRIVATE void _sapp_tc_frame(void) { + if (_sapp_tc.first_frame) { + _sapp_tc.first_frame = false; + _sapp_tc_call_init(); + } + _sapp_tc_call_frame(); + _sapp_tc.frame_count++; +} + +_SOKOL_PRIVATE bool _sapp_tc_image_validate(const sapp_image_desc* desc) { + SOKOL_ASSERT(desc->width > 0); + SOKOL_ASSERT(desc->height > 0); + SOKOL_ASSERT(desc->pixels.ptr != 0); + SOKOL_ASSERT(desc->pixels.size > 0); + const size_t wh_size = (size_t)(desc->width * desc->height) * sizeof(uint32_t); + if (wh_size != desc->pixels.size) { + _SAPP_ERROR(IMAGE_DATA_SIZE_MISMATCH); + return false; + } + return true; +} + +_SOKOL_PRIVATE int _sapp_tc_image_bestmatch(const sapp_image_desc image_descs[], int num_images, int width, int height) { + int least_diff = 0x7FFFFFFF; + int least_index = 0; + for (int i = 0; i < num_images; i++) { + int diff = (image_descs[i].width * image_descs[i].height) - (width * height); + if (diff < 0) { + diff = -diff; + } + if (diff < least_diff) { + least_diff = diff; + least_index = i; + } + } + return least_index; +} + +_SOKOL_PRIVATE int _sapp_tc_icon_num_images(const sapp_icon_desc* desc) { + int index = 0; + for (; index < SAPP_MAX_ICONIMAGES; index++) { + if (0 == desc->images[index].pixels.ptr) { + break; + } + } + return index; +} + +_SOKOL_PRIVATE bool _sapp_tc_validate_icon_desc(const sapp_icon_desc* desc, int num_images) { + SOKOL_ASSERT(num_images <= SAPP_MAX_ICONIMAGES); + for (int i = 0; i < num_images; i++) { + const sapp_image_desc* img_desc = &desc->images[i]; + if (!_sapp_tc_image_validate(img_desc)) { + return false; + } + } + return true; +} +#if defined(_SAPP_IOS) + +_SOKOL_PRIVATE NSInteger _sapp_tc_ios_max_fps(void) { + return _sapp_tc.ios.window.windowScene.screen.maximumFramesPerSecond; +} + +#if defined(SOKOL_METAL) + +_SOKOL_PRIVATE id _sapp_tc_ios_mtl_create_texture(int width, int height, MTLPixelFormat fmt, int sample_count, const char* label) { + MTLTextureDescriptor* mtl_desc = [[MTLTextureDescriptor alloc] init]; + if (sample_count > 1) { + mtl_desc.textureType = MTLTextureType2DMultisample; + } else { + mtl_desc.textureType = MTLTextureType2D; + } + mtl_desc.pixelFormat = fmt; + mtl_desc.width = (NSUInteger)width; + mtl_desc.height = (NSUInteger)height; + mtl_desc.depth = 1; + mtl_desc.mipmapLevelCount = 1; + mtl_desc.arrayLength = 1; + mtl_desc.sampleCount = (NSUInteger)sample_count; + mtl_desc.usage = MTLTextureUsageRenderTarget; + mtl_desc.resourceOptions = MTLResourceStorageModePrivate; + id mtl_tex = [_sapp_tc.ios.mtl.device newTextureWithDescriptor:mtl_desc]; + _SAPP_OBJC_RELEASE(mtl_desc); + #if defined(SOKOL_DEBUG) + if (mtl_tex) { + mtl_tex.label = [NSString stringWithUTF8String:label]; + } + #else + _SOKOL_UNUSED(label); + #endif + return mtl_tex; +} + +_SOKOL_PRIVATE void _sapp_tc_ios_mtl_swapchain_create(int width, int height) { + _sapp_tc.ios.mtl.depth_tex =_sapp_tc_ios_mtl_create_texture(width, height, MTLPixelFormatDepth32Float_Stencil8, _sapp_tc.sample_count, "swapchain_depth_tex"); + if (nil == _sapp_tc.ios.mtl.depth_tex) { + _SAPP_PANIC(METAL_CREATE_SWAPCHAIN_DEPTH_TEXTURE_FAILED); + } + if (_sapp_tc.sample_count > 1) { + _sapp_tc.ios.mtl.msaa_tex = _sapp_tc_ios_mtl_create_texture(width, height, MTLPixelFormatRGB10A2Unorm /* Modified by tettou771 for TrussC: 10-bit color */, _sapp_tc.sample_count, "swapchain_msaa_tex"); + if (nil == _sapp_tc.ios.mtl.msaa_tex) { + _SAPP_PANIC(METAL_CREATE_SWAPCHAIN_MSAA_TEXTURE_FAILED); + } + } +} + +_SOKOL_PRIVATE void _sapp_tc_ios_mtl_swapchain_destroy(void) { + if (_sapp_tc.ios.mtl.depth_tex) { + _SAPP_OBJC_RELEASE(_sapp_tc.ios.mtl.depth_tex); + } + if (_sapp_tc.ios.mtl.msaa_tex) { + _SAPP_OBJC_RELEASE(_sapp_tc.ios.mtl.msaa_tex); + } +} + +_SOKOL_PRIVATE void _sapp_tc_ios_mtl_swapchain_resize(int width, int height) { + _sapp_tc_ios_mtl_swapchain_destroy(); + _sapp_tc_ios_mtl_swapchain_create(width, height); +} + +_SOKOL_PRIVATE id _sapp_tc_ios_mtl_swapchain_next(void) { + id drawable = [_sapp_tc.ios.mtl.layer nextDrawable]; + SOKOL_ASSERT(drawable != nil); + return drawable; +} + +_SOKOL_PRIVATE void _sapp_tc_ios_mtl_timing_init(void) { + _sapp_tc.ios.mtl.timing.timestamp = 0.0; + _sapp_tc.ios.mtl.timing.frame_duration_sec = 1.0 / _sapp_tc_ios_max_fps(); +} + +_SOKOL_PRIVATE void _sapp_tc_ios_mtl_timing_update(void) { + const CFTimeInterval cur_timestamp = _sapp_tc.ios.mtl.display_link.timestamp; + // skip first frame (frame_duration had been initialized to display refresh rate) + if (_sapp_tc.ios.mtl.timing.timestamp > 0.0) { + const double dt = cur_timestamp - _sapp_tc.ios.mtl.timing.timestamp; + _sapp_tc.ios.mtl.timing.frame_duration_sec = _sapp_tc_timing_clamp(&_sapp_tc.timing, dt); + } else { + SOKOL_ASSERT(_sapp_tc.ios.mtl.timing.frame_duration_sec > 0.0); + } + _sapp_tc.ios.mtl.timing.timestamp = cur_timestamp; +} + +_SOKOL_PRIVATE double _sapp_tc_ios_mtl_timing_frame_duration(void) { + SOKOL_ASSERT(_sapp_tc.ios.mtl.timing.frame_duration_sec > 0.0); + return _sapp_tc.ios.mtl.timing.frame_duration_sec; +} + +_SOKOL_PRIVATE void _sapp_tc_ios_mtl_start_display_link(void) { + SOKOL_ASSERT(nil == _sapp_tc.ios.mtl.display_link); + SOKOL_ASSERT(nil != _sapp_tc.ios.view); + _sapp_tc.ios.mtl.display_link = [CADisplayLink displayLinkWithTarget:_sapp_tc.ios.view selector:@selector(displayLinkFired:)]; + const float preferred_fps = _sapp_tc_ios_max_fps() / _sapp_tc.swap_interval; + const CAFrameRateRange frame_rate_range = { preferred_fps, preferred_fps, preferred_fps }; + _sapp_tc.ios.mtl.display_link.preferredFrameRateRange = frame_rate_range; + [_sapp_tc.ios.mtl.display_link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; +} + +_SOKOL_PRIVATE void _sapp_tc_ios_mtl_stop_display_link(void) { + if (nil != _sapp_tc.ios.mtl.display_link) { + [_sapp_tc.ios.mtl.display_link invalidate]; + // NOTE: the run-loop held the only string reference to the display link + _sapp_tc.ios.mtl.display_link = nil; + } +} + +_SOKOL_PRIVATE void _sapp_tc_ios_mtl_init(UIWindowScene* windowScene) { + _sapp_tc.ios.mtl.device = MTLCreateSystemDefaultDevice(); + + _sapp_tc.ios.view = [[_sapp_tc_ios_view alloc] initWithFrame:windowScene.screen.bounds]; + _sapp_tc.ios.view.userInteractionEnabled = YES; + #if !defined(_SAPP_TVOS) + _sapp_tc.ios.view.multipleTouchEnabled = YES; + #endif + _sapp_tc.ios.supported_orientations = UIInterfaceOrientationMaskAll; + + _sapp_tc.ios.mtl.layer = [CAMetalLayer layer]; + _sapp_tc.ios.mtl.layer.device = _sapp_tc.ios.mtl.device; + _sapp_tc.ios.mtl.layer.opaque = true; + _sapp_tc.ios.mtl.layer.framebufferOnly = false /* Modified for TrussC: enable captureWindow() reads (issue #56) */; + _sapp_tc.ios.mtl.layer.pixelFormat = MTLPixelFormatRGB10A2Unorm /* Modified by tettou771 for TrussC: 10-bit color */; + _sapp_tc.ios.mtl.layer.frame = _sapp_tc.ios.view.layer.frame; + + [_sapp_tc.ios.view.layer addSublayer:_sapp_tc.ios.mtl.layer]; + + _sapp_tc.ios.supported_orientations = UIInterfaceOrientationMaskAll; + _sapp_tc.ios.view_ctrl = [[_sapp_tc_ios_view_ctrl alloc] init]; + _sapp_tc.ios.view_ctrl.modalPresentationStyle = UIModalPresentationFullScreen; + _sapp_tc.ios.view_ctrl.view = _sapp_tc.ios.view; + _sapp_tc.ios.window.rootViewController = _sapp_tc.ios.view_ctrl; + + _sapp_tc_ios_mtl_start_display_link(); + _sapp_tc_ios_mtl_timing_init(); +} + +_SOKOL_PRIVATE void _sapp_tc_ios_mtl_discard_state(void) { + _sapp_tc_ios_mtl_stop_display_link(); + _sapp_tc_ios_mtl_swapchain_destroy(); + _SAPP_OBJC_RELEASE(_sapp_tc.ios.mtl.layer); + _SAPP_OBJC_RELEASE(_sapp_tc.ios.view_ctrl); + _SAPP_OBJC_RELEASE(_sapp_tc.ios.mtl.device); +} + +_SOKOL_PRIVATE bool _sapp_tc_ios_mtl_update_framebuffer_dimensions(CGRect screen_rect) { + // Modified by tettou771 for TrussC: use actual drawable dimensions for framebuffer size + // to avoid chicken-egg mismatch between sapp_width()/sapp_height() and Metal render pass dimensions. + _sapp_tc.framebuffer_width = _sapp_tc_roundf_gzero(screen_rect.size.width * _sapp_tc.dpi_scale); + _sapp_tc.framebuffer_height = _sapp_tc_roundf_gzero(screen_rect.size.height * _sapp_tc.dpi_scale); + const CGSize cur_size = _sapp_tc.ios.mtl.layer.drawableSize; + const int cur_width = _sapp_tc_roundf_gzero(cur_size.width); + const int cur_height = _sapp_tc_roundf_gzero(cur_size.height); + const bool dim_changed = (_sapp_tc.framebuffer_width != cur_width) || (_sapp_tc.framebuffer_height != cur_height); + if (dim_changed) { + const CGSize drawable_size = { (CGFloat) _sapp_tc.framebuffer_width, (CGFloat) _sapp_tc.framebuffer_height }; + _sapp_tc.ios.mtl.layer.drawableSize = drawable_size; + _sapp_tc.ios.mtl.layer.frame = screen_rect; + _sapp_tc_ios_mtl_swapchain_resize(_sapp_tc.framebuffer_width, _sapp_tc.framebuffer_height); + } + // Always read back actual drawable dimensions to ensure framebuffer_width/height + // matches the Metal drawable (prevents scissor rect exceeding render pass bounds) + const CGSize actual_size = _sapp_tc.ios.mtl.layer.drawableSize; + _sapp_tc.framebuffer_width = _sapp_tc_roundf_gzero(actual_size.width); + _sapp_tc.framebuffer_height = _sapp_tc_roundf_gzero(actual_size.height); + return dim_changed; +} +#endif + +#if defined(SOKOL_GLES3) +_SOKOL_PRIVATE void _sapp_tc_ios_gles3_init(UIWindowScene* windowScene) { + const CGRect screen_rect = windowScene.screen.bounds; + _sapp_tc.ios.eagl_ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3]; + _sapp_tc.ios.view = [[_sapp_tc_ios_view alloc] initWithFrame:screen_rect]; + _sapp_tc.ios.view.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888; + _sapp_tc.ios.view.drawableDepthFormat = GLKViewDrawableDepthFormat24; + _sapp_tc.ios.view.drawableStencilFormat = GLKViewDrawableStencilFormatNone; + GLKViewDrawableMultisample msaa = _sapp_tc.sample_count > 1 ? GLKViewDrawableMultisample4X : GLKViewDrawableMultisampleNone; + _sapp_tc.ios.view.drawableMultisample = msaa; + _sapp_tc.ios.view.context = _sapp_tc.ios.eagl_ctx; + _sapp_tc.ios.view.enableSetNeedsDisplay = NO; + _sapp_tc.ios.view.userInteractionEnabled = YES; + _sapp_tc.ios.view.multipleTouchEnabled = YES; + // on GLKView, contentScaleFactor appears to work just fine! + if (_sapp_tc.desc.high_dpi) { + _sapp_tc.ios.view.contentScaleFactor = _sapp_tc.dpi_scale; + } else { + _sapp_tc.ios.view.contentScaleFactor = 1.0; + } + _sapp_tc.ios.view_ctrl = [[GLKViewController alloc] init]; + _sapp_tc.ios.view_ctrl.view = _sapp_tc.ios.view; + _sapp_tc.ios.view_ctrl.preferredFramesPerSecond = _sapp_tc_ios_max_fps() / _sapp_tc.swap_interval; + _sapp_tc.ios.window.rootViewController = _sapp_tc.ios.view_ctrl; +} + +_SOKOL_PRIVATE void _sapp_tc_ios_gles3_discard_state(void) { + _SAPP_OBJC_RELEASE(_sapp_tc.ios.view_ctrl); + _SAPP_OBJC_RELEASE(_sapp_tc.ios.eagl_ctx); +} + +_SOKOL_PRIVATE bool _sapp_tc_ios_gles3_update_framebuffer_dimensions(CGRect screen_rect) { + _sapp_tc.framebuffer_width = _sapp_tc_roundf_gzero(screen_rect.size.width * _sapp_tc.dpi_scale); + _sapp_tc.framebuffer_height = _sapp_tc_roundf_gzero(screen_rect.size.height * _sapp_tc.dpi_scale); + int cur_fb_width = _sapp_tc_roundf_gzero(_sapp_tc.ios.view.drawableWidth); + int cur_fb_height = _sapp_tc_roundf_gzero(_sapp_tc.ios.view.drawableHeight); + return (_sapp_tc.framebuffer_width != cur_fb_width) || (_sapp_tc.framebuffer_height != cur_fb_height); +} +#endif + +_SOKOL_PRIVATE void _sapp_tc_ios_discard_state(void) { + // NOTE: it's safe to call [release] on a nil object + _SAPP_OBJC_RELEASE(_sapp_tc.ios.textfield_dlg); + _SAPP_OBJC_RELEASE(_sapp_tc.ios.textfield); + #if defined(SOKOL_METAL) + _sapp_tc_ios_mtl_discard_state(); + #else + _sapp_tc_ios_gles3_discard_state(); + #endif + _SAPP_OBJC_RELEASE(_sapp_tc.ios.view); + _SAPP_OBJC_RELEASE(_sapp_tc.ios.window); +} + +_SOKOL_PRIVATE void _sapp_tc_ios_run(const sapp_desc* desc) { + _sapp_tc_init_state(desc); + static int argc = 1; + static char* argv[] = { (char*)"sokol_app" }; + UIApplicationMain(argc, argv, nil, NSStringFromClass([_sapp_tc_scene_delegate class])); +} + +/* iOS entry function */ +#if !defined(SOKOL_NO_ENTRY) +int main(int argc, char* argv[]) { + sapp_desc desc = sokol_main(argc, argv); + _sapp_tc_ios_run(&desc); + return 0; +} +#endif /* SOKOL_NO_ENTRY */ + +_SOKOL_PRIVATE void _sapp_tc_ios_app_event(sapp_event_type type) { + if (_sapp_tc_events_enabled()) { + _sapp_tc_init_event(type); + _sapp_tc_call_event(&_sapp_tc.event); + } +} + +_SOKOL_PRIVATE void _sapp_tc_tvos_press_event(sapp_event_type type, NSSet* presses) { + if (_sapp_tc_events_enabled()) { + for (UIPress *press in presses) { + sapp_keycode key = SAPP_KEYCODE_INVALID; + switch (press.type) { + case UIPressTypeUpArrow: key = SAPP_KEYCODE_UP; break; + case UIPressTypeDownArrow: key = SAPP_KEYCODE_DOWN; break; + case UIPressTypeLeftArrow: key = SAPP_KEYCODE_LEFT; break; + case UIPressTypeRightArrow: key = SAPP_KEYCODE_RIGHT; break; + case UIPressTypeSelect: key = SAPP_KEYCODE_ENTER; break; + case UIPressTypeMenu: key = SAPP_KEYCODE_MENU; break; + case UIPressTypePlayPause: key = SAPP_KEYCODE_PAUSE; break; + default: break; + } + if (key != SAPP_KEYCODE_INVALID) { + _sapp_tc_init_event(type); + _sapp_tc.event.key_code = key; + _sapp_tc.event.key_repeat = false; + _sapp_tc.event.modifiers = 0; + _sapp_tc_call_event(&_sapp_tc.event); + } + } + } +} + +_SOKOL_PRIVATE void _sapp_tc_ios_touch_event(sapp_event_type type, NSSet* touches, UIEvent* event) { + if (_sapp_tc_events_enabled()) { + _sapp_tc_init_event(type); + NSEnumerator* enumerator = event.allTouches.objectEnumerator; + UITouch* ios_touch; + while ((ios_touch = [enumerator nextObject])) { + if ((_sapp_tc.event.num_touches + 1) < SAPP_MAX_TOUCHPOINTS) { + CGPoint ios_pos = [ios_touch locationInView:_sapp_tc.ios.view]; + sapp_touchpoint* cur_point = &_sapp_tc.event.touches[_sapp_tc.event.num_touches++]; + cur_point->identifier = (uintptr_t) ios_touch; + cur_point->pos_x = ios_pos.x * _sapp_tc.dpi_scale; + cur_point->pos_y = ios_pos.y * _sapp_tc.dpi_scale; + cur_point->changed = [touches containsObject:ios_touch]; + } + } + if (_sapp_tc.event.num_touches > 0) { + _sapp_tc_call_event(&_sapp_tc.event); + } + } +} + +_SOKOL_PRIVATE void _sapp_tc_ios_update_dimensions(void) { + // Modified by tettou771 for TrussC: use view bounds instead of UIScreen.mainScreen.bounds + // (consistent with macOS which uses [view bounds], avoids potential timing issues + // with screen bounds not matching the view's actual layout) + CGRect view_rect = _sapp_tc.ios.view.bounds; + if (view_rect.size.width < 1.0 || view_rect.size.height < 1.0) { + // View not laid out yet, fall back to screen bounds + view_rect = _sapp_tc.ios.window.windowScene.screen.bounds; + } + _sapp_tc.window_width = _sapp_tc_roundf_gzero(view_rect.size.width); + _sapp_tc.window_height = _sapp_tc_roundf_gzero(view_rect.size.height); + #if defined(SOKOL_METAL) + bool dim_changed = _sapp_tc_ios_mtl_update_framebuffer_dimensions(view_rect); + #else + bool dim_changed = _sapp_tc_ios_gles3_update_framebuffer_dimensions(view_rect); + #endif + if (dim_changed && !_sapp_tc.first_frame) { + _sapp_tc_ios_app_event(SAPP_EVENTTYPE_RESIZED); + } +} + +_SOKOL_PRIVATE void _sapp_tc_ios_frame(void) { + _sapp_tc_timing_update(&_sapp_tc.timing, 0.0); + #if defined(SOKOL_METAL) + _sapp_tc_ios_mtl_timing_update(); + #endif + #if defined(_SAPP_ANY_GL) + glGetIntegerv(GL_FRAMEBUFFER_BINDING, (GLint*)&_sapp_tc.gl.framebuffer); + #endif + @autoreleasepool { + _sapp_tc_ios_update_dimensions(); + _sapp_tc_frame(); + } +} + +_SOKOL_PRIVATE void _sapp_tc_ios_show_keyboard(bool shown) { + /* if not happened yet, create an invisible text field */ + if (nil == _sapp_tc.ios.textfield) { + _sapp_tc.ios.textfield_dlg = [[_sapp_tc_textfield_dlg alloc] init]; + _sapp_tc.ios.textfield = [[UITextField alloc] initWithFrame:CGRectMake(10, 10, 100, 50)]; + _sapp_tc.ios.textfield.keyboardType = UIKeyboardTypeDefault; + _sapp_tc.ios.textfield.returnKeyType = UIReturnKeyDefault; + _sapp_tc.ios.textfield.autocapitalizationType = UITextAutocapitalizationTypeNone; + _sapp_tc.ios.textfield.autocorrectionType = UITextAutocorrectionTypeNo; + _sapp_tc.ios.textfield.spellCheckingType = UITextSpellCheckingTypeNo; + _sapp_tc.ios.textfield.hidden = YES; + _sapp_tc.ios.textfield.text = @"x"; + _sapp_tc.ios.textfield.delegate = _sapp_tc.ios.textfield_dlg; + [_sapp_tc.ios.view_ctrl.view addSubview:_sapp_tc.ios.textfield]; + +#if !defined(_SAPP_TVOS) + [[NSNotificationCenter defaultCenter] addObserver:_sapp_tc.ios.textfield_dlg + selector:@selector(keyboardWasShown:) + name:UIKeyboardDidShowNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:_sapp_tc.ios.textfield_dlg + selector:@selector(keyboardWillBeHidden:) + name:UIKeyboardWillHideNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:_sapp_tc.ios.textfield_dlg + selector:@selector(keyboardDidChangeFrame:) + name:UIKeyboardDidChangeFrameNotification object:nil]; +#endif + } + if (shown) { + // setting the text field as first responder brings up the onscreen keyboard + [_sapp_tc.ios.textfield becomeFirstResponder]; + } else { + [_sapp_tc.ios.textfield resignFirstResponder]; + } +} + +@implementation _sapp_tc_scene_delegate +- (UISceneConfiguration*) application:(UIApplication*)application + configurationForConnectingSceneSession:(UISceneSession*)connectingSceneSession + options:(UISceneConnectionOptions*)options +{ + UISceneConfiguration* config = [[UISceneConfiguration alloc] initWithName:@"SokolSceneConfiguration" sessionRole:connectingSceneSession.role]; + config.delegateClass = [_sapp_tc_scene_delegate class]; + return config; +} + +- (void)scene:(UIScene*)scene willConnectToSession:(UISceneSession*)session options:(UISceneConnectionOptions*)connectionOptions { + UIWindowScene* windowScene = (UIWindowScene*)scene; + CGRect screen_rect = windowScene.screen.bounds; + _sapp_tc.ios.window = [[UIWindow alloc] initWithWindowScene:windowScene]; + _sapp_tc.window_width = _sapp_tc_roundf_gzero(screen_rect.size.width); + _sapp_tc.window_height = _sapp_tc_roundf_gzero(screen_rect.size.height); + if (_sapp_tc.desc.high_dpi) { + _sapp_tc.dpi_scale = (float) windowScene.screen.nativeScale; + } else { + _sapp_tc.dpi_scale = 1.0f; + } + _sapp_tc.framebuffer_width = _sapp_tc_roundf_gzero(_sapp_tc.window_width * _sapp_tc.dpi_scale); + _sapp_tc.framebuffer_height = _sapp_tc_roundf_gzero(_sapp_tc.window_height * _sapp_tc.dpi_scale); + #if defined(SOKOL_METAL) + _sapp_tc_ios_mtl_init(windowScene); + #else + _sapp_tc_ios_gles3_init(windowScene); + #endif + [_sapp_tc.ios.window makeKeyAndVisible]; + _sapp_tc.valid = true; +} + +- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions { + return YES; +} + +- (void)sceneWillResignActive:(UIScene*)scene { + if (!_sapp_tc.ios.suspended) { + _sapp_tc.ios.suspended = true; + #if defined(SOKOL_METAL) + if (nil != _sapp_tc.ios.mtl.display_link) { + _sapp_tc.ios.mtl.display_link.paused = YES; + } + #endif + _sapp_tc_ios_app_event(SAPP_EVENTTYPE_SUSPENDED); + } +} + +- (void)sceneDidBecomeActive:(UIScene*)scene { + if (_sapp_tc.ios.suspended) { + _sapp_tc.ios.suspended = false; + #if defined(SOKOL_METAL) + if (nil != _sapp_tc.ios.mtl.display_link) { + _sapp_tc.ios.mtl.display_link.paused = NO; + } + #endif + _sapp_tc_ios_app_event(SAPP_EVENTTYPE_RESUMED); + } +} + +/* NOTE: this method will rarely ever be called, iOS application + which are terminated by the user are usually killed via signal 9 + by the operating system. +*/ +- (void)applicationWillTerminate:(UIApplication *)application { + _SOKOL_UNUSED(application); + _sapp_tc_call_cleanup(); + _sapp_tc_ios_discard_state(); + _sapp_tc_discard_state(); +} +@end + +@implementation _sapp_tc_textfield_dlg +- (void)keyboardWasShown:(NSNotification*)notif { + _sapp_tc.onscreen_keyboard_shown = true; + /* query the keyboard's size, and modify the content view's size */ +#if !defined(_SAPP_TVOS) + if (_sapp_tc.desc.ios.keyboard_resizes_canvas) { + NSDictionary* info = notif.userInfo; + CGFloat kbd_h = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height; + CGRect view_frame = _sapp_tc.ios.window.windowScene.screen.bounds; + view_frame.size.height -= kbd_h; + _sapp_tc.ios.view.frame = view_frame; + } +#endif +} +- (void)keyboardWillBeHidden:(NSNotification*)notif { + _sapp_tc.onscreen_keyboard_shown = false; + if (_sapp_tc.desc.ios.keyboard_resizes_canvas) { + _sapp_tc.ios.view.frame = _sapp_tc.ios.window.windowScene.screen.bounds; + } +} +- (void)keyboardDidChangeFrame:(NSNotification*)notif { + /* this is for the case when the screen rotation changes while the keyboard is open */ +#if !defined(_SAPP_TVOS) + if (_sapp_tc.onscreen_keyboard_shown && _sapp_tc.desc.ios.keyboard_resizes_canvas) { + NSDictionary* info = notif.userInfo; + CGFloat kbd_h = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height; + CGRect view_frame = _sapp_tc.ios.window.windowScene.screen.bounds; + view_frame.size.height -= kbd_h; + _sapp_tc.ios.view.frame = view_frame; + } +#endif +} +- (BOOL)textField:(UITextField*)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString*)string { + if (_sapp_tc_events_enabled()) { + const NSUInteger len = string.length; + if (len > 0) { + for (NSUInteger i = 0; i < len; i++) { + unichar c = [string characterAtIndex:i]; + if (c >= 32) { + /* ignore surrogates for now */ + if ((c < 0xD800) || (c > 0xDFFF)) { + _sapp_tc_init_event(SAPP_EVENTTYPE_CHAR); + _sapp_tc.event.char_code = c; + _sapp_tc_call_event(&_sapp_tc.event); + } + } + if (c <= 32) { + sapp_keycode k = SAPP_KEYCODE_INVALID; + switch (c) { + case 10: k = SAPP_KEYCODE_ENTER; break; + case 32: k = SAPP_KEYCODE_SPACE; break; + default: break; + } + if (k != SAPP_KEYCODE_INVALID) { + _sapp_tc_init_event(SAPP_EVENTTYPE_KEY_DOWN); + _sapp_tc.event.key_code = k; + _sapp_tc_call_event(&_sapp_tc.event); + _sapp_tc_init_event(SAPP_EVENTTYPE_KEY_UP); + _sapp_tc.event.key_code = k; + _sapp_tc_call_event(&_sapp_tc.event); + } + } + } + } else { + // this was a backspace + _sapp_tc_init_event(SAPP_EVENTTYPE_KEY_DOWN); + _sapp_tc.event.key_code = SAPP_KEYCODE_BACKSPACE; + _sapp_tc_call_event(&_sapp_tc.event); + _sapp_tc_init_event(SAPP_EVENTTYPE_KEY_UP); + _sapp_tc.event.key_code = SAPP_KEYCODE_BACKSPACE; + _sapp_tc_call_event(&_sapp_tc.event); + } + } + return NO; +} +@end + +@implementation _sapp_tc_ios_view +#if defined(SOKOL_METAL) +- (void)displayLinkFired:(id)sender { + _SOKOL_UNUSED(sender); + _sapp_tc_ios_frame(); +} +#else +- (void)drawRect:(CGRect)rect { + _SOKOL_UNUSED(rect); + _sapp_tc_ios_frame(); +} +#endif + +- (BOOL)isOpaque { + return YES; +} +- (void)pressesBegan:(NSSet *)presses withEvent:(UIPressesEvent *)event { + _sapp_tc_tvos_press_event(SAPP_EVENTTYPE_KEY_DOWN, presses); +} +- (void)pressesChanged:(NSSet *)presses withEvent:(UIPressesEvent *)event { +} +- (void)pressesEnded:(NSSet *)presses withEvent:(UIPressesEvent *)event { + _sapp_tc_tvos_press_event(SAPP_EVENTTYPE_KEY_UP, presses); +} +- (void)pressesCancelled:(NSSet *)presses withEvent:(UIPressesEvent *)event { + _sapp_tc_tvos_press_event(SAPP_EVENTTYPE_KEY_UP, presses); +} +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent*)event { + _sapp_tc_ios_touch_event(SAPP_EVENTTYPE_TOUCHES_BEGAN, touches, event); +} +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent*)event { + _sapp_tc_ios_touch_event(SAPP_EVENTTYPE_TOUCHES_MOVED, touches, event); +} +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent*)event { + _sapp_tc_ios_touch_event(SAPP_EVENTTYPE_TOUCHES_ENDED, touches, event); +} +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent*)event { + _sapp_tc_ios_touch_event(SAPP_EVENTTYPE_TOUCHES_CANCELLED, touches, event); +} +@end + +// Modified by tettou771 for TrussC: custom view controller for runtime orientation + immersive mode +// Modified by tettou771 for TrussC: non-static so tcPlatform_ios.mm can access +bool _sapp_ios_immersive_mode = false; +@implementation _sapp_tc_ios_view_ctrl +- (UIInterfaceOrientationMask)supportedInterfaceOrientations { + return _sapp_tc.ios.supported_orientations; +} +- (BOOL)prefersStatusBarHidden { + return _sapp_ios_immersive_mode; +} +- (BOOL)prefersHomeIndicatorAutoHidden { + return _sapp_ios_immersive_mode; +} +@end + +#endif /* TARGET_OS_IPHONE */ +/* ---- public API (lifted from sokol_app.h >>public, web-live branches) ---- */ +#if defined(SOKOL_NO_ENTRY) +SOKOL_API_IMPL void sapp_run(const sapp_desc* desc) { + SOKOL_ASSERT(desc); + #if defined(_SAPP_MACOS) + _sapp_tc_macos_run(desc); + #elif defined(_SAPP_IOS) + _sapp_tc_ios_run(desc); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_run(desc); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_run(desc); + #elif defined(_SAPP_LINUX) + _sapp_tc_linux_run(desc); + #else + #error "sapp_run() not supported on this platform" + #endif +} + +/* this is just a stub so the linker doesn't complain */ +sapp_desc sokol_main(int argc, char* argv[]) { + _SOKOL_UNUSED(argc); + _SOKOL_UNUSED(argv); + _SAPP_STRUCT(sapp_desc, desc); + return desc; +} +#else +/* likewise, in normal mode, sapp_run() is just an empty stub */ +SOKOL_API_IMPL void sapp_run(const sapp_desc* desc) { + _SOKOL_UNUSED(desc); +} +#endif + +SOKOL_API_IMPL bool sapp_isvalid(void) { + return _sapp_tc.valid; +} + +SOKOL_API_IMPL void* sapp_userdata(void) { + return _sapp_tc.desc.user_data; +} + +SOKOL_API_IMPL sapp_desc sapp_query_desc(void) { + return _sapp_tc.desc; +} + +SOKOL_API_IMPL uint64_t sapp_frame_count(void) { + return _sapp_tc.frame_count; +} + +SOKOL_API_IMPL double sapp_frame_duration(void) { + #if defined(_SAPP_MACOS) && defined(SOKOL_METAL) + return _sapp_tc_macos_mtl_timing_frame_duration(); + #elif defined(_SAPP_IOS) && defined(SOKOL_METAL) + return _sapp_tc_ios_mtl_timing_frame_duration(); + #else + return _sapp_tc_timing_get(&_sapp_tc.timing); + #endif +} + +SOKOL_API_IMPL double sapp_frame_duration_unfiltered(void) { + return _sapp_tc.timing.dt; +} + +SOKOL_API_IMPL int sapp_width(void) { + return (_sapp_tc.framebuffer_width > 0) ? _sapp_tc.framebuffer_width : 1; +} + +SOKOL_API_IMPL float sapp_widthf(void) { + return (float)sapp_width(); +} + +SOKOL_API_IMPL int sapp_height(void) { + return (_sapp_tc.framebuffer_height > 0) ? _sapp_tc.framebuffer_height : 1; +} + +SOKOL_API_IMPL float sapp_heightf(void) { + return (float)sapp_height(); +} + +SOKOL_API_IMPL sapp_pixel_format sapp_color_format(void) { + #if defined(SOKOL_WGPU) + switch (_sapp_tc.wgpu.render_format) { + case WGPUTextureFormat_RGBA8Unorm: + return SAPP_PIXELFORMAT_RGBA8; + case WGPUTextureFormat_BGRA8Unorm: + return SAPP_PIXELFORMAT_BGRA8; + default: + SOKOL_UNREACHABLE; + return SAPP_PIXELFORMAT_NONE; + } + #elif defined(SOKOL_VULKAN) + switch (_sapp_tc.vk.surface_format.format) { + case VK_FORMAT_R8G8B8A8_UNORM: + return SAPP_PIXELFORMAT_RGBA8; + case VK_FORMAT_B8G8R8A8_UNORM: + return SAPP_PIXELFORMAT_BGRA8; + default: + // FIXME! + SOKOL_UNREACHABLE; + return SAPP_PIXELFORMAT_NONE; + } + #elif defined(SOKOL_METAL) || defined(SOKOL_D3D11) + // Modified by tettou771 for TrussC: report 10-bit color format (RGB10A2) + return SAPP_PIXELFORMAT_RGB10A2; + #else + return SAPP_PIXELFORMAT_RGBA8; + #endif +} + +SOKOL_API_IMPL sapp_pixel_format sapp_depth_format(void) { + return SAPP_PIXELFORMAT_DEPTH_STENCIL; +} + +SOKOL_API_IMPL int sapp_sample_count(void) { + return _sapp_tc.sample_count; +} + +SOKOL_API_IMPL bool sapp_high_dpi(void) { + return _sapp_tc.desc.high_dpi && (_sapp_tc.dpi_scale >= 1.5f); +} + +SOKOL_API_IMPL float sapp_dpi_scale(void) { + return _sapp_tc.dpi_scale; +} + +SOKOL_API_IMPL const void* sapp_egl_get_display(void) { + SOKOL_ASSERT(_sapp_tc.valid); + #if defined(_SAPP_ANDROID) + return _sapp_tc.android.display; + #elif defined(_SAPP_LINUX) && defined(_SAPP_EGL) + return _sapp_tc.egl.display; + #else + return 0; + #endif +} + +SOKOL_API_IMPL const void* sapp_egl_get_context(void) { + SOKOL_ASSERT(_sapp_tc.valid); + #if defined(_SAPP_ANDROID) + return _sapp_tc.android.context; + #elif defined(_SAPP_LINUX) && defined(_SAPP_EGL) + return _sapp_tc.egl.context; + #else + return 0; + #endif +} + +SOKOL_API_IMPL void sapp_show_keyboard(bool show) { + #if defined(_SAPP_IOS) + _sapp_tc_ios_show_keyboard(show); + #elif defined(_SAPP_ANDROID) + _sapp_tc_android_show_keyboard(show); + #else + _SOKOL_UNUSED(show); + #endif +} + +SOKOL_API_IMPL bool sapp_keyboard_shown(void) { + return _sapp_tc.onscreen_keyboard_shown; +} + +SOKOL_API_IMPL bool sapp_is_fullscreen(void) { + return _sapp_tc.fullscreen; +} + +SOKOL_API_IMPL void sapp_toggle_fullscreen(void) { + #if defined(_SAPP_MACOS) + _sapp_tc_macos_toggle_fullscreen(); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_toggle_fullscreen(); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_toggle_fullscreen(); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_toggle_fullscreen(); + #endif +} + +_SOKOL_PRIVATE void _sapp_tc_update_cursor(sapp_mouse_cursor cursor, bool shown) { + #if defined(_SAPP_MACOS) + _sapp_tc_macos_update_cursor(cursor, shown); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_update_cursor(cursor, shown, false); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_update_cursor(cursor, shown); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_update_cursor(cursor, shown); + #endif + _sapp_tc.mouse.current_cursor = cursor; + _sapp_tc.mouse.shown = shown; +} + +/* NOTE that sapp_show_mouse() does not "stack" like the Win32 or macOS API functions! */ +SOKOL_API_IMPL void sapp_show_mouse(bool show) { + if (_sapp_tc.mouse.shown != show) { + _sapp_tc_update_cursor(_sapp_tc.mouse.current_cursor, show); + } +} + +SOKOL_API_IMPL bool sapp_mouse_shown(void) { + return _sapp_tc.mouse.shown; +} + +SOKOL_API_IMPL void sapp_lock_mouse(bool lock) { + #if defined(_SAPP_MACOS) + _sapp_tc_macos_lock_mouse(lock); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_lock_mouse(lock); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_lock_mouse(lock); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_lock_mouse(lock); + #else + _sapp_tc.mouse.locked = lock; + #endif +} + +SOKOL_API_IMPL bool sapp_mouse_locked(void) { + return _sapp_tc.mouse.locked; +} + +SOKOL_API_IMPL void sapp_set_mouse_cursor(sapp_mouse_cursor cursor) { + SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); + if (_sapp_tc.mouse.current_cursor != cursor) { + _sapp_tc_update_cursor(cursor, _sapp_tc.mouse.shown); + } +} + +SOKOL_API_IMPL sapp_mouse_cursor sapp_get_mouse_cursor(void) { + return _sapp_tc.mouse.current_cursor; +} + +SOKOL_API_IMPL sapp_mouse_cursor sapp_bind_mouse_cursor_image(sapp_mouse_cursor cursor, const sapp_image_desc* desc) { + SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); + // NOTE: It seems that for some reason, the hotspot doesn't work if it is one less + // than the dimension of the cursor image (or more), on windows. So for a cursor + // that is 32 by 32 px, a hotspot of x = 30 works, but not x = 31. + // The cursor simply dissapears in such cases. Asserting for all platforms to make + // the behaviour consistent. + SOKOL_ASSERT(desc->cursor_hotspot_x < desc->width - 1 && desc->cursor_hotspot_y < desc->height - 1); + SOKOL_ASSERT(desc->width * desc->height * 4 == (int) desc->pixels.size); + + sapp_unbind_mouse_cursor_image(cursor); + + bool res = false; + #if defined(_SAPP_MACOS) + res = _sapp_tc_macos_make_custom_mouse_cursor(cursor, desc); + #elif defined(_SAPP_EMSCRIPTEN) + res = _sapp_tc_emsc_make_custom_mouse_cursor(cursor, desc); + #elif defined(_SAPP_WIN32) + res = _sapp_tc_win32_make_custom_mouse_cursor(cursor, desc); + #elif defined(_SAPP_LINUX) + res = _sapp_tc_x11_make_custom_mouse_cursor(cursor, desc); + #else + _SOKOL_UNUSED(desc); + #endif + _sapp_tc.custom_cursor_bound[(int)cursor] = res; + + // Update the displayed cursor in case the current cursor is the one we just bound. + if (_sapp_tc.mouse.current_cursor == cursor) { + _sapp_tc_update_cursor(cursor, _sapp_tc.mouse.shown); + } + return cursor; // returning the passed-in cursor puerly for convenience, in case you want to asign the value to a variable. +} + +SOKOL_API_IMPL void sapp_unbind_mouse_cursor_image(sapp_mouse_cursor cursor) { + SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); + if (_sapp_tc.custom_cursor_bound[(int)cursor]) { + // if this is the active cursor, first restore it to its default image, + // this must be done before attempting to destroy any cursor image + // resources which at least on win32 would fail if the cursor is still in use + _sapp_tc.custom_cursor_bound[(int)cursor] = false; + if (_sapp_tc.mouse.current_cursor == cursor) { + _sapp_tc_update_cursor(cursor, _sapp_tc.mouse.shown); + } + #if defined(_SAPP_MACOS) + _sapp_tc_macos_destroy_custom_mouse_cursor(cursor); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_destroy_custom_mouse_cursor(cursor); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_destroy_custom_mouse_cursor(cursor); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_destroy_custom_mouse_cursor(cursor); + #endif + } +} + +SOKOL_API_IMPL void sapp_request_quit(void) { + _sapp_tc.quit_requested = true; +} + +SOKOL_API_IMPL void sapp_cancel_quit(void) { + _sapp_tc.quit_requested = false; +} + +SOKOL_API_IMPL void sapp_quit(void) { + _sapp_tc.quit_ordered = true; +} + +SOKOL_API_IMPL void sapp_consume_event(void) { + _sapp_tc.event_consumed = true; +} + +/* NOTE: on HTML5, sapp_set_clipboard_string() must be called from within event handler! */ +SOKOL_API_IMPL void sapp_set_clipboard_string(const char* str) { + if (!_sapp_tc.clipboard.enabled) { + return; + } + SOKOL_ASSERT(str); + #if defined(_SAPP_MACOS) + _sapp_tc_macos_set_clipboard_string(str); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_set_clipboard_string(str); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_set_clipboard_string(str); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_set_clipboard_string(str); + #else + /* not implemented */ + #endif + _sapp_tc_strcpy(str, _sapp_tc.clipboard.buffer, (size_t)_sapp_tc.clipboard.buf_size); +} + +SOKOL_API_IMPL const char* sapp_get_clipboard_string(void) { + if (!_sapp_tc.clipboard.enabled) { + return ""; + } + #if defined(_SAPP_MACOS) + return _sapp_tc_macos_get_clipboard_string(); + #elif defined(_SAPP_EMSCRIPTEN) + return _sapp_tc.clipboard.buffer; + #elif defined(_SAPP_WIN32) + return _sapp_tc_win32_get_clipboard_string(); + #elif defined(_SAPP_LINUX) + return _sapp_tc_x11_get_clipboard_string(); + #else + /* not implemented */ + return _sapp_tc.clipboard.buffer; + #endif +} + +SOKOL_API_IMPL void sapp_set_window_title(const char* title) { + SOKOL_ASSERT(title); + _sapp_tc_strcpy(title, _sapp_tc.window_title, sizeof(_sapp_tc.window_title)); + #if defined(_SAPP_MACOS) + _sapp_tc_macos_update_window_title(); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_update_window_title(); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_update_window_title(); + #endif +} + +SOKOL_API_IMPL void sapp_set_icon(const sapp_icon_desc* desc) { + SOKOL_ASSERT(desc); + if (desc->sokol_default) { + /* the sokol default-icon generator is not ported: the favicon is the + page's business unless the app provides real icon images */ + return; + } + const int num_images = _sapp_tc_icon_num_images(desc); + if (num_images == 0) { + return; + } + SOKOL_ASSERT((num_images > 0) && (num_images <= SAPP_MAX_ICONIMAGES)); + if (!_sapp_tc_validate_icon_desc(desc, num_images)) { + return; + } + #if defined(_SAPP_MACOS) + _sapp_tc_macos_set_icon(desc, num_images); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_set_icon(desc, num_images); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_set_icon(desc, num_images); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_set_icon(desc, num_images); + #endif +} + +SOKOL_API_IMPL int sapp_get_num_dropped_files(void) { + if (!_sapp_tc.drop.enabled) { + return 0; + } + return _sapp_tc.drop.num_files; +} + +SOKOL_API_IMPL const char* sapp_get_dropped_file_path(int index) { + SOKOL_ASSERT((index >= 0) && (index < _sapp_tc.drop.num_files)); + if (!_sapp_tc.drop.enabled) { + return ""; + } + SOKOL_ASSERT(_sapp_tc.drop.buffer); + if ((index < 0) || (index >= _sapp_tc.drop.max_files)) { + return ""; + } + return (const char*) _sapp_tc_dropped_file_path_ptr(index); +} + +SOKOL_API_IMPL uint32_t sapp_html5_get_dropped_file_size(int index) { + SOKOL_ASSERT((index >= 0) && (index < _sapp_tc.drop.num_files)); + #if defined(_SAPP_EMSCRIPTEN) + if (!_sapp_tc.drop.enabled) { + return 0; + } + return _sapp_tc_js_dropped_file_size(index); + #else + (void)index; + return 0; + #endif +} + +SOKOL_API_IMPL void sapp_html5_fetch_dropped_file(const sapp_html5_fetch_request* request) { + SOKOL_ASSERT(_sapp_tc.drop.enabled); + SOKOL_ASSERT(request); + SOKOL_ASSERT(request->callback); + SOKOL_ASSERT(request->buffer.ptr); + SOKOL_ASSERT(request->buffer.size > 0); + #if defined(_SAPP_EMSCRIPTEN) + const int index = request->dropped_file_index; + sapp_html5_fetch_error error_code = SAPP_HTML5_FETCH_ERROR_NO_ERROR; + if ((index < 0) || (index >= _sapp_tc.drop.num_files)) { + error_code = SAPP_HTML5_FETCH_ERROR_OTHER; + } + if (sapp_html5_get_dropped_file_size(index) > request->buffer.size) { + error_code = SAPP_HTML5_FETCH_ERROR_BUFFER_TOO_SMALL; + } + if (SAPP_HTML5_FETCH_ERROR_NO_ERROR != error_code) { + _sapp_tc_emsc_invoke_fetch_cb(index, + false, // success + (int)error_code, + request->callback, + 0, // fetched_size + (void*)request->buffer.ptr, + request->buffer.size, + request->user_data); + } else { + _sapp_tc_js_fetch_dropped_file(index, + request->callback, + (void*)request->buffer.ptr, + request->buffer.size, + request->user_data); + } + #else + (void)request; + #endif +} + +SOKOL_API_IMPL sapp_environment sapp_get_environment(void) { + SOKOL_ASSERT(_sapp_tc.valid); + _SAPP_STRUCT(sapp_environment, res); + res.defaults.color_format = sapp_color_format(); + res.defaults.depth_format = sapp_depth_format(); + res.defaults.sample_count = sapp_sample_count(); + #if defined(SOKOL_METAL) + #if defined(_SAPP_MACOS) + res.metal.device = (__bridge const void*) _sapp_tc.macos.mtl.device; + #else + res.metal.device = (__bridge const void*) _sapp_tc.ios.mtl.device; + #endif + #endif + #if defined(SOKOL_D3D11) + res.d3d11.device = (const void*) _sapp_tc.d3d11.device; + res.d3d11.device_context = (const void*) _sapp_tc.d3d11.device_context; + #endif + #if defined(SOKOL_WGPU) + res.wgpu.device = (const void*) _sapp_tc.wgpu.device; + #endif + #if defined(SOKOL_VULKAN) + res.vulkan.instance = (const void*) _sapp_tc.vk.instance; + res.vulkan.physical_device = (const void*) _sapp_tc.vk.physical_device; + res.vulkan.device = (const void*) _sapp_tc.vk.device; + res.vulkan.queue = (const void*) _sapp_tc.vk.queue; + res.vulkan.queue_family_index = _sapp_tc.vk.queue_family_index; + #endif + return res; +} + +SOKOL_API_IMPL sapp_swapchain sapp_get_swapchain(void) { + SOKOL_ASSERT(_sapp_tc.valid); + _SAPP_STRUCT(sapp_swapchain, res); + #if defined(SOKOL_METAL) + #if defined(_SAPP_MACOS) + res.metal.current_drawable = (__bridge const void*) _sapp_tc_macos_mtl_swapchain_next(); + res.metal.depth_stencil_texture = (__bridge const void*) _sapp_tc.macos.mtl.depth_tex; + res.metal.msaa_color_texture = (__bridge const void*) _sapp_tc.macos.mtl.msaa_tex; + #else + res.metal.current_drawable = (__bridge const void*) _sapp_tc_ios_mtl_swapchain_next(); + res.metal.depth_stencil_texture = (__bridge const void*) _sapp_tc.ios.mtl.depth_tex; + res.metal.msaa_color_texture = (__bridge const void*) _sapp_tc.ios.mtl.msaa_tex; + #endif + #endif + #if defined(SOKOL_D3D11) + SOKOL_ASSERT(_sapp_tc.d3d11.rtv); + if (_sapp_tc.sample_count > 1) { + SOKOL_ASSERT(_sapp_tc.d3d11.msaa_rtv); + res.d3d11.render_view = (const void*) _sapp_tc.d3d11.msaa_rtv; + res.d3d11.resolve_view = (const void*) _sapp_tc.d3d11.rtv; + } else { + res.d3d11.render_view = (const void*) _sapp_tc.d3d11.rtv; + } + res.d3d11.depth_stencil_view = (const void*) _sapp_tc.d3d11.dsv; + #endif + #if defined(SOKOL_WGPU) + SOKOL_ASSERT(0 == _sapp_tc.wgpu.swapchain_view); + _sapp_tc_wgpu_swapchain_next(); + // FIXME: swapchain_view being null must be allowed and should skip the frame + SOKOL_ASSERT(_sapp_tc.wgpu.swapchain_view); + if (_sapp_tc.sample_count > 1) { + SOKOL_ASSERT(_sapp_tc.wgpu.msaa_view); + res.wgpu.render_view = (const void*) _sapp_tc.wgpu.msaa_view; + res.wgpu.resolve_view = (const void*) _sapp_tc.wgpu.swapchain_view; + } else { + res.wgpu.render_view = (const void*) _sapp_tc.wgpu.swapchain_view; + } + res.wgpu.depth_stencil_view = (const void*) _sapp_tc.wgpu.depth_stencil_view; + #endif + #if defined(SOKOL_VULKAN) + _sapp_tc_vk_swapchain_next(); + // FIXME: swapchain_view being null must be allowed and should skip the frame + uint32_t img_idx = _sapp_tc.vk.cur_swapchain_image_index; + if (_sapp_tc.sample_count > 1) { + SOKOL_ASSERT(_sapp_tc.vk.msaa.img && _sapp_tc.vk.msaa.view); + res.vulkan.render_image = (const void*) _sapp_tc.vk.msaa.img; + res.vulkan.render_view = (const void*) _sapp_tc.vk.msaa.view; + res.vulkan.resolve_image = (const void*) _sapp_tc.vk.swapchain_images[img_idx]; + res.vulkan.resolve_view = (const void*) _sapp_tc.vk.swapchain_views[img_idx]; + } else { + res.vulkan.render_image = (const void*) _sapp_tc.vk.swapchain_images[img_idx]; + res.vulkan.render_view = (const void*) _sapp_tc.vk.swapchain_views[img_idx]; + } + res.vulkan.depth_stencil_image = (const void*) _sapp_tc.vk.depth.img; + res.vulkan.depth_stencil_view = (const void*) _sapp_tc.vk.depth.view; + // NOTE: using the current swapchain image index here is *NOT* a bug! The render_finished_semaphore *must* + // be associated with its swapchain image in case the swapchain implementation doesn't return swapchain images in order + res.vulkan.render_finished_semaphore = _sapp_tc.vk.sync[img_idx].render_finished_sem; + res.vulkan.present_complete_semaphore = _sapp_tc.vk.sync[_sapp_tc.vk.sync_slot].present_complete_sem; + #endif + #if defined(_SAPP_ANY_GL) + res.gl.framebuffer = _sapp_tc.gl.framebuffer; + #endif + res.width = sapp_width(); + res.height = sapp_height(); + res.color_format = sapp_color_format(); + res.depth_format = sapp_depth_format(); + res.sample_count = sapp_sample_count(); + return res; +} + +SOKOL_API_IMPL const void* sapp_macos_get_window(void) { + #if defined(_SAPP_MACOS) + const void* obj = (__bridge const void*) _sapp_tc.macos.window; + SOKOL_ASSERT(obj); + return obj; + #else + return 0; + #endif +} + +SOKOL_API_IMPL const void* sapp_ios_get_window(void) { + #if defined(_SAPP_IOS) + const void* obj = (__bridge const void*) _sapp_tc.ios.window; + SOKOL_ASSERT(obj); + return obj; + #else + return 0; + #endif +} + +// Modified by tettou771 for TrussC: runtime orientation control +SOKOL_API_IMPL void sapp_ios_set_supported_orientations(uint32_t mask) { + #if defined(_SAPP_IOS) + _sapp_tc.ios.supported_orientations = (NSUInteger)mask; + #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 160000 + if (@available(iOS 16.0, *)) { + [_sapp_tc.ios.view_ctrl setNeedsUpdateOfSupportedInterfaceOrientations]; + } + #endif + #else + (void)mask; + #endif +} + +SOKOL_API_IMPL const void* sapp_d3d11_get_swap_chain(void) { + SOKOL_ASSERT(_sapp_tc.valid); +#if defined(SOKOL_D3D11) + return _sapp_tc.d3d11.swap_chain; +#else + return 0; +#endif +} + +SOKOL_API_IMPL const void* sapp_win32_get_hwnd(void) { + SOKOL_ASSERT(_sapp_tc.valid); + #if defined(_SAPP_WIN32) + return _sapp_tc.win32.hwnd; + #else + return 0; + #endif +} + +SOKOL_API_IMPL int sapp_gl_get_major_version(void) { + SOKOL_ASSERT(_sapp_tc.valid); + #if defined(_SAPP_ANY_GL) + return _sapp_tc.desc.gl.major_version; + #else + return 0; + #endif +} + +SOKOL_API_IMPL int sapp_gl_get_minor_version(void) { + SOKOL_ASSERT(_sapp_tc.valid); + #if defined(_SAPP_ANY_GL) + return _sapp_tc.desc.gl.minor_version; + #else + return 0; + #endif +} + +SOKOL_API_IMPL bool sapp_gl_is_gles(void) { + #if defined(SOKOL_GLES3) + return true; + #else + return false; + #endif +} + +SOKOL_API_IMPL const void* sapp_x11_get_window(void) { + #if defined(_SAPP_LINUX) + return (void*)_sapp_tc.x11.window; + #else + return 0; + #endif +} + +SOKOL_API_IMPL const void* sapp_x11_get_display(void) { + #if defined(_SAPP_LINUX) + return (void*)_sapp_tc.x11.display; + #else + return 0; + #endif +} + +SOKOL_API_IMPL const void* sapp_android_get_native_activity(void) { + // NOTE: _sapp_tc.valid is not asserted here because sapp_android_get_native_activity() + // needs to be callable from within sokol_main() (see: https://github.com/floooh/sokol/issues/708) + #if defined(_SAPP_ANDROID) + return (void*)_sapp_tc.android.activity; + #else + return 0; + #endif +} + +SOKOL_API_IMPL void sapp_html5_ask_leave_site(bool ask) { + _sapp_tc.html5_ask_leave_site = ask; +} + +// Modified by tettou771 for TrussC: skip the next present call (fix D3D11 flickering) +SOKOL_API_IMPL void sapp_skip_present(void) { + _sapp_tc.skip_present = true; +} +// end of modification + + + +/* ---- Metal backend getters (real values; iOS is single-window) ----------- */ +#if defined(__cplusplus) +extern "C" { +#endif +const void* sapp_metal_get_device(void) { + return (__bridge const void*)_sapp_tc.ios.mtl.device; +} +const void* sapp_metal_get_current_drawable(void) { + /* acquires lazily, upstream parity -- sokol_gfx's begin-pass is the + acquisition point via sglue_swapchain(); call once per frame */ + return (__bridge const void*)_sapp_tc_ios_mtl_swapchain_next(); +} +const void* sapp_metal_get_depth_stencil_texture(void) { + return (__bridge const void*)_sapp_tc.ios.mtl.depth_tex; +} +const void* sapp_metal_get_msaa_color_texture(void) { + return (__bridge const void*)_sapp_tc.ios.mtl.msaa_tex; +} +const void* sapp_d3d11_get_device(void) { return 0; } +const void* sapp_d3d11_get_device_context(void) { return 0; } + +/* ---- multi-window API: explicit platform gap on iOS ---------------------- + there is exactly one UIWindow bound to the connecting UIWindowScene; + a second window is not representable */ +sapp_window sapp_create_window(const sapp_window_desc* desc) { + _SOKOL_UNUSED(desc); + fprintf(stderr, "sokol_app_tc.h: sapp_create_window() is not supported on iOS (single-window platform)\n"); + sapp_window w = {0}; + return w; +} +void sapp_destroy_window(sapp_window win) { _SOKOL_UNUSED(win); } +bool sapp_window_valid(sapp_window win) { _SOKOL_UNUSED(win); return false; } +int sapp_window_width(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +int sapp_window_height(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +int sapp_window_framebuffer_width(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +int sapp_window_framebuffer_height(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +float sapp_window_dpi_scale(sapp_window win) { _SOKOL_UNUSED(win); return 1.0f; } +int sapp_window_sample_count(sapp_window win) { _SOKOL_UNUSED(win); return 1; } +bool sapp_window_occluded(sapp_window win) { _SOKOL_UNUSED(win); return false; } +void sapp_window_set_title(sapp_window win, const char* title) { _SOKOL_UNUSED(win); _SOKOL_UNUSED(title); } +double sapp_window_frame_duration(sapp_window win) { _SOKOL_UNUSED(win); return 1.0 / 60.0; } +const void* sapp_window_metal_current_drawable(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_metal_depth_stencil_texture(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_metal_msaa_color_texture(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_d3d11_render_view(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_d3d11_resolve_view(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_d3d11_depth_stencil_view(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_macos_get_window(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_win32_get_hwnd(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_x11_get_window(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +uint32_t sapp_window_gl_framebuffer(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +#if defined(__cplusplus) +} /* extern "C" */ +#endif + +#elif defined(_WIN32) +/*== Windows (D3D11) ======================================================== + Implements the public sokol_app.h API on Windows plus the multi-window + API. Structure mirrors sokol_app.h's win32/D3D11 backend (quit dance in + WM_CLOSE, resize coalesced outside WM_SIZE, scancode-indexed keytable, + borderless fullscreen, refcounted mouse capture, TrussC RGB10A2 + + skip_present patches) but replaces the PeekMessage busy-render-loop: + every swapchain is created with FRAME_LATENCY_WAITABLE_OBJECT and the + loop blocks in MsgWaitForMultipleObjectsEx on the per-window waitables, + so each window ticks at its own display's rate and the process sleeps + when nothing is due. */ +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include /* GET_X_LPARAM / GET_Y_LPARAM */ +#include /* DragAcceptFiles / DragQueryFileW */ +#include +#include /* IDXGIFactory2 / IDXGISwapChain2 (frame latency waitable) */ + +#if defined(_MSC_VER) +#pragma comment (lib, "kernel32") +#pragma comment (lib, "user32") +#pragma comment (lib, "shell32") +#pragma comment (lib, "gdi32") +#pragma comment (lib, "dxgi") +#pragma comment (lib, "d3d11") +#endif + +#if defined(SOKOL_APP_IMPL_INCLUDED) +#error "sokol_app_tc.h owns the Windows implementation of the sapp_* API; include sokol_app.h WITHOUT an implementation define in this TU" +#endif + +#include /* freopen_s (console redirection) */ + +#ifndef WM_MOUSEHWHEEL +#define WM_MOUSEHWHEEL (0x020E) +#endif +#ifndef WM_DPICHANGED +#define WM_DPICHANGED (0x02E0) +#endif + +#define _SAPP_TC_MAX_WINDOWS (32) +#define _SAPP_TC_MODAL_TIMER (1) +#define _SAPP_TC_RELEASE(obj) do { if (obj) { (obj)->Release(); (obj) = 0; } } while (0) + +/* EMA-filtered frame timer, port of sokol_app.h's _sapp_timing_* (QPC-based; + Windows has no display-link timestamps, so this is the only dt source) */ +typedef struct { + double last; /* previous sample time, 0 = none yet */ + double ema; + double smooth_dt; + double dt; /* last raw (clamped) delta */ +} _sapp_tc_timing_t; + +typedef struct _sapp_tc_window_t { + uint32_t win_id; + sapp_window_desc desc; + bool is_main; /* window #0: created by sapp_run, drives the app callbacks */ + bool ready; /* creation finished; wndproc may deliver events */ + HWND hwnd; + HMONITOR hmonitor; + DXGI_FORMAT color_fmt; + UINT swapchain_flags; /* MUST be passed unchanged to every ResizeBuffers */ + IDXGISwapChain1* swap_chain; + HANDLE frame_wait; /* frame latency waitable (NULL: timer-paced fallback) */ + bool credit_held; /* a waitable credit was consumed without a Present yet */ + bool waitable_ready; /* the run-loop's MsgWait consumed this window's frame + latency waitable signal (auto-reset): the due-check + must NOT re-wait the same handle (it would miss the + already-consumed signal and stall forever) */ + ID3D11Texture2D* rt; /* swapchain backbuffer */ + ID3D11RenderTargetView* rtv; + ID3D11Texture2D* msaa_rt; /* MSAA color (sample_count > 1; flip model can't MSAA the backbuffer) */ + ID3D11RenderTargetView* msaa_rtv; + ID3D11Texture2D* ds; /* depth-stencil */ + ID3D11DepthStencilView* dsv; + int fb_width, fb_height; /* framebuffer pixels */ + int win_width, win_height; /* logical points */ + float window_scale; /* OS DPI / 96 (physical px per point) */ + float dpi_scale; /* framebuffer px per point (content scale) */ + float mouse_scale; /* client px -> event coordinate (framebuffer px) */ + double refresh_period; /* this window's display refresh period (pacing fallback) */ + double earliest_next; /* QPC seconds; window is not due before this */ + double frame_duration; + _sapp_tc_timing_t timing; + bool occluded; + bool iconified; + bool in_tick; + bool mouse_tracked; /* TrackMouseEvent enter/leave state */ + bool mouse_pos_valid; + float mouse_x, mouse_y; /* last position in event coordinates */ + float mouse_dx, mouse_dy; + uint8_t capture_mask; /* SetCapture refcount by button bit */ + WCHAR surrogate; /* WM_CHAR high-surrogate accumulator */ + RECT stored_window_rect; /* windowed rect for fullscreen restore */ +} _sapp_tc_window_t; + +static struct { + bool keytable_valid; + sapp_keycode keycodes[SAPP_MAX_KEYCODES]; + uint32_t next_id; + _sapp_tc_window_t* windows[_SAPP_TC_MAX_WINDOWS]; + bool wndclass_registered; + double qpc_period; /* seconds per QueryPerformanceCounter tick */ + /* dynamically loaded DPI functions (user32.dll, Win10 1607+) */ + BOOL (WINAPI* SetProcessDpiAwarenessContext)(void*); + UINT (WINAPI* GetDpiForWindow)(HWND); + BOOL (WINAPI* AdjustWindowRectExForDpi)(LPRECT, DWORD, BOOL, DWORD, UINT); + + /* ---- app / main-window state (this header owns sapp_run on Windows) -- */ + struct { + sapp_desc desc; /* sanitized copy; window_title points at the buffer below */ + char window_title[256]; + WCHAR window_title_wide[256]; + bool valid; + bool init_called; + bool cleanup_called; + bool quit_requested; + bool quit_ordered; + bool fullscreen; + bool event_consumed; + bool skip_present; /* one-shot; honored on D3D11 (TrussC flicker patch) */ + bool dpi_aware; + uint64_t frame_count; + _sapp_tc_window_t* main; + ID3D11Device* device; + ID3D11DeviceContext* device_context; + /* mouse cursor */ + bool mouse_shown; + sapp_mouse_cursor current_cursor; + HCURSOR standard_cursors[_SAPP_MOUSECURSOR_NUM]; + HCURSOR custom_cursors[_SAPP_MOUSECURSOR_NUM]; + bool custom_cursor_bound[_SAPP_MOUSECURSOR_NUM]; + /* console */ + bool console_utf8_set; + UINT orig_codepage; + /* clipboard */ + bool clipboard_enabled; + int clipboard_size; + char* clipboard; + /* drag & drop */ + bool drop_enabled; + int drop_max_files; + int drop_max_path_length; + int drop_num_files; + char* drop_buffer; + } app; +} _sapp_tc; + +static void _sapp_tc_win32_tick(_sapp_tc_window_t* w, bool from_modal); +static bool _sapp_tc_win32_update_dimensions(_sapp_tc_window_t* w); +static void _sapp_tc_d3d11_resize(_sapp_tc_window_t* w); +static void _sapp_tc_win32_apply_cursor(sapp_mouse_cursor cursor, bool shown, bool skip_area_test); + +/*-- timing -----------------------------------------------------------------*/ +static double _sapp_tc_now(void) { + LARGE_INTEGER qpc; + QueryPerformanceCounter(&qpc); + return (double)qpc.QuadPart * _sapp_tc.qpc_period; +} + +static void _sapp_tc_timing_reset(_sapp_tc_timing_t* t) { + t->last = 0.0; + t->ema = t->smooth_dt = t->dt = 1.0 / 60.0; +} + +static double _sapp_tc_timing_clamp(double dt) { + if (dt < 0.000001) return 0.000001; + if (dt > 0.1) return 0.1; + return dt; +} + +static void _sapp_tc_timing_update(_sapp_tc_timing_t* t) { + const double now = _sapp_tc_now(); + if (t->last > 0.0) { + double dt = _sapp_tc_timing_clamp(now - t->last); + t->dt = dt; + /* big jump: reset the filter; otherwise EMA-smooth */ + if (fabs(dt - t->smooth_dt) > 0.004) { + t->ema = t->smooth_dt = dt; + } else { + t->ema += 0.025 * (dt - t->ema); + t->smooth_dt = _sapp_tc_timing_clamp(t->ema); + } + } + t->last = now; +} + +/* keycode table lifted verbatim from sokol_app.h's _sapp_win32_init_keytable(); + indexed by the 9-bit Win32 scancode (HIWORD(lParam) & 0x1FF -- the 0x100 + extended bit distinguishes e.g. left/right Ctrl and keypad Enter) */ +static void _sapp_tc_init_keytable(void) { + if (_sapp_tc.keytable_valid) return; + _sapp_tc.keytable_valid = true; + _sapp_tc.keycodes[0x00B] = SAPP_KEYCODE_0; + _sapp_tc.keycodes[0x002] = SAPP_KEYCODE_1; + _sapp_tc.keycodes[0x003] = SAPP_KEYCODE_2; + _sapp_tc.keycodes[0x004] = SAPP_KEYCODE_3; + _sapp_tc.keycodes[0x005] = SAPP_KEYCODE_4; + _sapp_tc.keycodes[0x006] = SAPP_KEYCODE_5; + _sapp_tc.keycodes[0x007] = SAPP_KEYCODE_6; + _sapp_tc.keycodes[0x008] = SAPP_KEYCODE_7; + _sapp_tc.keycodes[0x009] = SAPP_KEYCODE_8; + _sapp_tc.keycodes[0x00A] = SAPP_KEYCODE_9; + _sapp_tc.keycodes[0x01E] = SAPP_KEYCODE_A; + _sapp_tc.keycodes[0x030] = SAPP_KEYCODE_B; + _sapp_tc.keycodes[0x02E] = SAPP_KEYCODE_C; + _sapp_tc.keycodes[0x020] = SAPP_KEYCODE_D; + _sapp_tc.keycodes[0x012] = SAPP_KEYCODE_E; + _sapp_tc.keycodes[0x021] = SAPP_KEYCODE_F; + _sapp_tc.keycodes[0x022] = SAPP_KEYCODE_G; + _sapp_tc.keycodes[0x023] = SAPP_KEYCODE_H; + _sapp_tc.keycodes[0x017] = SAPP_KEYCODE_I; + _sapp_tc.keycodes[0x024] = SAPP_KEYCODE_J; + _sapp_tc.keycodes[0x025] = SAPP_KEYCODE_K; + _sapp_tc.keycodes[0x026] = SAPP_KEYCODE_L; + _sapp_tc.keycodes[0x032] = SAPP_KEYCODE_M; + _sapp_tc.keycodes[0x031] = SAPP_KEYCODE_N; + _sapp_tc.keycodes[0x018] = SAPP_KEYCODE_O; + _sapp_tc.keycodes[0x019] = SAPP_KEYCODE_P; + _sapp_tc.keycodes[0x010] = SAPP_KEYCODE_Q; + _sapp_tc.keycodes[0x013] = SAPP_KEYCODE_R; + _sapp_tc.keycodes[0x01F] = SAPP_KEYCODE_S; + _sapp_tc.keycodes[0x014] = SAPP_KEYCODE_T; + _sapp_tc.keycodes[0x016] = SAPP_KEYCODE_U; + _sapp_tc.keycodes[0x02F] = SAPP_KEYCODE_V; + _sapp_tc.keycodes[0x011] = SAPP_KEYCODE_W; + _sapp_tc.keycodes[0x02D] = SAPP_KEYCODE_X; + _sapp_tc.keycodes[0x015] = SAPP_KEYCODE_Y; + _sapp_tc.keycodes[0x02C] = SAPP_KEYCODE_Z; + _sapp_tc.keycodes[0x028] = SAPP_KEYCODE_APOSTROPHE; + _sapp_tc.keycodes[0x02B] = SAPP_KEYCODE_BACKSLASH; + _sapp_tc.keycodes[0x033] = SAPP_KEYCODE_COMMA; + _sapp_tc.keycodes[0x00D] = SAPP_KEYCODE_EQUAL; + _sapp_tc.keycodes[0x029] = SAPP_KEYCODE_GRAVE_ACCENT; + _sapp_tc.keycodes[0x01A] = SAPP_KEYCODE_LEFT_BRACKET; + _sapp_tc.keycodes[0x00C] = SAPP_KEYCODE_MINUS; + _sapp_tc.keycodes[0x034] = SAPP_KEYCODE_PERIOD; + _sapp_tc.keycodes[0x01B] = SAPP_KEYCODE_RIGHT_BRACKET; + _sapp_tc.keycodes[0x027] = SAPP_KEYCODE_SEMICOLON; + _sapp_tc.keycodes[0x035] = SAPP_KEYCODE_SLASH; + _sapp_tc.keycodes[0x056] = SAPP_KEYCODE_WORLD_2; + _sapp_tc.keycodes[0x00E] = SAPP_KEYCODE_BACKSPACE; + _sapp_tc.keycodes[0x153] = SAPP_KEYCODE_DELETE; + _sapp_tc.keycodes[0x14F] = SAPP_KEYCODE_END; + _sapp_tc.keycodes[0x01C] = SAPP_KEYCODE_ENTER; + _sapp_tc.keycodes[0x001] = SAPP_KEYCODE_ESCAPE; + _sapp_tc.keycodes[0x147] = SAPP_KEYCODE_HOME; + _sapp_tc.keycodes[0x152] = SAPP_KEYCODE_INSERT; + _sapp_tc.keycodes[0x15D] = SAPP_KEYCODE_MENU; + _sapp_tc.keycodes[0x151] = SAPP_KEYCODE_PAGE_DOWN; + _sapp_tc.keycodes[0x149] = SAPP_KEYCODE_PAGE_UP; + _sapp_tc.keycodes[0x045] = SAPP_KEYCODE_PAUSE; + _sapp_tc.keycodes[0x146] = SAPP_KEYCODE_PAUSE; + _sapp_tc.keycodes[0x039] = SAPP_KEYCODE_SPACE; + _sapp_tc.keycodes[0x00F] = SAPP_KEYCODE_TAB; + _sapp_tc.keycodes[0x03A] = SAPP_KEYCODE_CAPS_LOCK; + _sapp_tc.keycodes[0x145] = SAPP_KEYCODE_NUM_LOCK; + _sapp_tc.keycodes[0x046] = SAPP_KEYCODE_SCROLL_LOCK; + _sapp_tc.keycodes[0x03B] = SAPP_KEYCODE_F1; + _sapp_tc.keycodes[0x03C] = SAPP_KEYCODE_F2; + _sapp_tc.keycodes[0x03D] = SAPP_KEYCODE_F3; + _sapp_tc.keycodes[0x03E] = SAPP_KEYCODE_F4; + _sapp_tc.keycodes[0x03F] = SAPP_KEYCODE_F5; + _sapp_tc.keycodes[0x040] = SAPP_KEYCODE_F6; + _sapp_tc.keycodes[0x041] = SAPP_KEYCODE_F7; + _sapp_tc.keycodes[0x042] = SAPP_KEYCODE_F8; + _sapp_tc.keycodes[0x043] = SAPP_KEYCODE_F9; + _sapp_tc.keycodes[0x044] = SAPP_KEYCODE_F10; + _sapp_tc.keycodes[0x057] = SAPP_KEYCODE_F11; + _sapp_tc.keycodes[0x058] = SAPP_KEYCODE_F12; + _sapp_tc.keycodes[0x064] = SAPP_KEYCODE_F13; + _sapp_tc.keycodes[0x065] = SAPP_KEYCODE_F14; + _sapp_tc.keycodes[0x066] = SAPP_KEYCODE_F15; + _sapp_tc.keycodes[0x067] = SAPP_KEYCODE_F16; + _sapp_tc.keycodes[0x068] = SAPP_KEYCODE_F17; + _sapp_tc.keycodes[0x069] = SAPP_KEYCODE_F18; + _sapp_tc.keycodes[0x06A] = SAPP_KEYCODE_F19; + _sapp_tc.keycodes[0x06B] = SAPP_KEYCODE_F20; + _sapp_tc.keycodes[0x06C] = SAPP_KEYCODE_F21; + _sapp_tc.keycodes[0x06D] = SAPP_KEYCODE_F22; + _sapp_tc.keycodes[0x06E] = SAPP_KEYCODE_F23; + _sapp_tc.keycodes[0x076] = SAPP_KEYCODE_F24; + _sapp_tc.keycodes[0x038] = SAPP_KEYCODE_LEFT_ALT; + _sapp_tc.keycodes[0x01D] = SAPP_KEYCODE_LEFT_CONTROL; + _sapp_tc.keycodes[0x02A] = SAPP_KEYCODE_LEFT_SHIFT; + _sapp_tc.keycodes[0x15B] = SAPP_KEYCODE_LEFT_SUPER; + _sapp_tc.keycodes[0x137] = SAPP_KEYCODE_PRINT_SCREEN; + _sapp_tc.keycodes[0x138] = SAPP_KEYCODE_RIGHT_ALT; + _sapp_tc.keycodes[0x11D] = SAPP_KEYCODE_RIGHT_CONTROL; + _sapp_tc.keycodes[0x036] = SAPP_KEYCODE_RIGHT_SHIFT; + _sapp_tc.keycodes[0x136] = SAPP_KEYCODE_RIGHT_SHIFT; + _sapp_tc.keycodes[0x15C] = SAPP_KEYCODE_RIGHT_SUPER; + _sapp_tc.keycodes[0x150] = SAPP_KEYCODE_DOWN; + _sapp_tc.keycodes[0x14B] = SAPP_KEYCODE_LEFT; + _sapp_tc.keycodes[0x14D] = SAPP_KEYCODE_RIGHT; + _sapp_tc.keycodes[0x148] = SAPP_KEYCODE_UP; + _sapp_tc.keycodes[0x052] = SAPP_KEYCODE_KP_0; + _sapp_tc.keycodes[0x04F] = SAPP_KEYCODE_KP_1; + _sapp_tc.keycodes[0x050] = SAPP_KEYCODE_KP_2; + _sapp_tc.keycodes[0x051] = SAPP_KEYCODE_KP_3; + _sapp_tc.keycodes[0x04B] = SAPP_KEYCODE_KP_4; + _sapp_tc.keycodes[0x04C] = SAPP_KEYCODE_KP_5; + _sapp_tc.keycodes[0x04D] = SAPP_KEYCODE_KP_6; + _sapp_tc.keycodes[0x047] = SAPP_KEYCODE_KP_7; + _sapp_tc.keycodes[0x048] = SAPP_KEYCODE_KP_8; + _sapp_tc.keycodes[0x049] = SAPP_KEYCODE_KP_9; + _sapp_tc.keycodes[0x04E] = SAPP_KEYCODE_KP_ADD; + _sapp_tc.keycodes[0x053] = SAPP_KEYCODE_KP_DECIMAL; + _sapp_tc.keycodes[0x135] = SAPP_KEYCODE_KP_DIVIDE; + _sapp_tc.keycodes[0x11C] = SAPP_KEYCODE_KP_ENTER; + _sapp_tc.keycodes[0x037] = SAPP_KEYCODE_KP_MULTIPLY; + _sapp_tc.keycodes[0x04A] = SAPP_KEYCODE_KP_SUBTRACT; +} + +static sapp_keycode _sapp_tc_translate_key(int scancode) { + if ((scancode >= 0) && (scancode < SAPP_MAX_KEYCODES)) { + return _sapp_tc.keycodes[scancode]; + } + return SAPP_KEYCODE_INVALID; +} + +static _sapp_tc_window_t* _sapp_tc_lookup(sapp_window win) { + if (win.id == 0) return 0; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* w = _sapp_tc.windows[i]; + if (w && w->win_id == win.id) return w; + } + return 0; +} + +/*-- UTF-8 <-> UTF-16 -------------------------------------------------------*/ +static bool _sapp_tc_win32_utf8_to_wide(const char* src, wchar_t* dst, int dst_num) { + dst[0] = 0; + if (0 == MultiByteToWideChar(CP_UTF8, 0, src, -1, dst, dst_num)) { + dst[dst_num - 1] = 0; + return false; + } + return true; +} + +static bool _sapp_tc_win32_wide_to_utf8(const wchar_t* src, char* dst, int dst_size) { + dst[0] = 0; + if (0 == WideCharToMultiByte(CP_UTF8, 0, src, -1, dst, dst_size, NULL, NULL)) { + dst[dst_size - 1] = 0; + return false; + } + return true; +} + +/*-- event plumbing ---------------------------------------------------------*/ +static uint32_t _sapp_tc_win32_mods(void) { + uint32_t m = 0; + if (GetKeyState(VK_SHIFT) & 0x8000) m |= SAPP_MODIFIER_SHIFT; + if (GetKeyState(VK_CONTROL) & 0x8000) m |= SAPP_MODIFIER_CTRL; + if (GetKeyState(VK_MENU) & 0x8000) m |= SAPP_MODIFIER_ALT; + if ((GetKeyState(VK_LWIN) | GetKeyState(VK_RWIN)) & 0x8000) m |= SAPP_MODIFIER_SUPER; + const bool swapped = (0 != GetSystemMetrics(SM_SWAPBUTTON)); + if (GetAsyncKeyState(VK_LBUTTON) & 0x8000) m |= swapped ? SAPP_MODIFIER_RMB : SAPP_MODIFIER_LMB; + if (GetAsyncKeyState(VK_RBUTTON) & 0x8000) m |= swapped ? SAPP_MODIFIER_LMB : SAPP_MODIFIER_RMB; + if (GetAsyncKeyState(VK_MBUTTON) & 0x8000) m |= SAPP_MODIFIER_MMB; + return m; +} + +static void _sapp_tc_send(_sapp_tc_window_t* w, sapp_event* ev) { + if (!w->desc.event_cb) return; + ev->frame_count = _sapp_tc.app.frame_count; + ev->window_width = w->win_width; + ev->window_height = w->win_height; + ev->framebuffer_width = w->fb_width; + ev->framebuffer_height = w->fb_height; + sapp_window handle = { w->win_id }; + w->desc.event_cb(ev, handle, w->desc.user_data); +} + +/*-- main window: event routing to the sapp_desc callbacks ------------------*/ +static bool _sapp_tc_app_events_enabled(void) { + /* same gate as sokol_app.h: nothing fires before the first tick's init_cb */ + return _sapp_tc.app.init_called && + (_sapp_tc.app.desc.event_cb || _sapp_tc.app.desc.event_userdata_cb); +} + +static void _sapp_tc_main_event_tramp(const sapp_event* ev, sapp_window win, void* user) { + (void)win; (void)user; + if (!_sapp_tc_app_events_enabled()) return; + _sapp_tc.app.event_consumed = false; + if (_sapp_tc.app.desc.event_cb) { + _sapp_tc.app.desc.event_cb(ev); + } else { + _sapp_tc.app.desc.event_userdata_cb(ev, _sapp_tc.app.desc.user_data); + } +} + +/*-- mouse cursor (app-global, same model as sokol_app.h) -------------------*/ +static void _sapp_tc_win32_init_cursor(sapp_mouse_cursor cursor, DWORD ocr_id) { + HCURSOR c = (HCURSOR)LoadImageW(NULL, MAKEINTRESOURCEW(ocr_id), IMAGE_CURSOR, + 0, 0, LR_DEFAULTSIZE | LR_SHARED); + if (!c) c = LoadCursor(NULL, IDC_ARROW); + _sapp_tc.app.standard_cursors[cursor] = c; +} + +static void _sapp_tc_win32_init_cursors(void) { + /* OCR_* resource ids inlined (OEMRESOURCE may not be defined) */ + _sapp_tc_win32_init_cursor(SAPP_MOUSECURSOR_DEFAULT, 32512); /* OCR_NORMAL */ + _sapp_tc_win32_init_cursor(SAPP_MOUSECURSOR_ARROW, 32512); /* OCR_NORMAL */ + _sapp_tc_win32_init_cursor(SAPP_MOUSECURSOR_IBEAM, 32513); /* OCR_IBEAM */ + _sapp_tc_win32_init_cursor(SAPP_MOUSECURSOR_CROSSHAIR, 32515); /* OCR_CROSS */ + _sapp_tc_win32_init_cursor(SAPP_MOUSECURSOR_POINTING_HAND, 32649); /* OCR_HAND */ + _sapp_tc_win32_init_cursor(SAPP_MOUSECURSOR_RESIZE_EW, 32644); /* OCR_SIZEWE */ + _sapp_tc_win32_init_cursor(SAPP_MOUSECURSOR_RESIZE_NS, 32645); /* OCR_SIZENS */ + _sapp_tc_win32_init_cursor(SAPP_MOUSECURSOR_RESIZE_NWSE, 32642); /* OCR_SIZENWSE */ + _sapp_tc_win32_init_cursor(SAPP_MOUSECURSOR_RESIZE_NESW, 32643); /* OCR_SIZENESW */ + _sapp_tc_win32_init_cursor(SAPP_MOUSECURSOR_RESIZE_ALL, 32646); /* OCR_SIZEALL */ + _sapp_tc_win32_init_cursor(SAPP_MOUSECURSOR_NOT_ALLOWED, 32648); /* OCR_NO */ +} + +static bool _sapp_tc_win32_cursor_in_content_area(void) { + POINT pos; + if (!GetCursorPos(&pos)) return false; + HWND hovered = WindowFromPoint(pos); + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* w = _sapp_tc.windows[i]; + if (!w || w->hwnd != hovered) continue; + RECT area; + if (!GetClientRect(w->hwnd, &area)) return false; + ScreenToClient(w->hwnd, &pos); + return PtInRect(&area, pos) == TRUE; + } + return false; +} + +static void _sapp_tc_win32_apply_cursor(sapp_mouse_cursor cursor, bool shown, bool skip_area_test) { + _sapp_tc.app.current_cursor = cursor; + _sapp_tc.app.mouse_shown = shown; + /* WM_SETCURSOR passes skip_area_test=true (it fires for HTCLIENT only); + the public setters check that the pointer is actually over one of our + client areas before touching the global cursor */ + if (!skip_area_test && !_sapp_tc_win32_cursor_in_content_area()) return; + HCURSOR c = NULL; /* SetCursor(NULL) hides (avoids the ShowCursor counter) */ + if (shown) { + if (_sapp_tc.app.custom_cursor_bound[cursor] && _sapp_tc.app.custom_cursors[cursor]) { + c = _sapp_tc.app.custom_cursors[cursor]; + } else { + c = _sapp_tc.app.standard_cursors[cursor]; + } + if (!c) c = LoadCursor(NULL, IDC_ARROW); + } + SetCursor(c); +} + +static HCURSOR _sapp_tc_win32_create_cursor(const sapp_image_desc* desc) { + BITMAPV5HEADER bi; + memset(&bi, 0, sizeof(bi)); + bi.bV5Size = sizeof(bi); + bi.bV5Width = (LONG)desc->width; + bi.bV5Height = -(LONG)desc->height; /* top-down */ + bi.bV5Planes = 1; + bi.bV5BitCount = 32; + bi.bV5Compression = BI_BITFIELDS; + bi.bV5RedMask = 0x00FF0000; + bi.bV5GreenMask = 0x0000FF00; + bi.bV5BlueMask = 0x000000FF; + bi.bV5AlphaMask = 0xFF000000; + uint8_t* target = 0; + HDC dc = GetDC(NULL); + HBITMAP color = CreateDIBSection(dc, (BITMAPINFO*)&bi, DIB_RGB_COLORS, + (void**)&target, NULL, 0); + ReleaseDC(NULL, dc); + if (!color) return NULL; + HBITMAP mask = CreateBitmap(desc->width, desc->height, 1, 1, NULL); + if (!mask) { DeleteObject(color); return NULL; } + /* RGBA -> BGRA */ + const uint8_t* src = (const uint8_t*)desc->pixels.ptr; + const int num_pixels = desc->width * desc->height; + for (int i = 0; i < num_pixels; i++) { + target[i * 4 + 0] = src[i * 4 + 2]; + target[i * 4 + 1] = src[i * 4 + 1]; + target[i * 4 + 2] = src[i * 4 + 0]; + target[i * 4 + 3] = src[i * 4 + 3]; + } + ICONINFO ii; + memset(&ii, 0, sizeof(ii)); + ii.fIcon = FALSE; /* cursor, not icon (hotspot applies) */ + ii.xHotspot = (DWORD)desc->cursor_hotspot_x; + ii.yHotspot = (DWORD)desc->cursor_hotspot_y; + ii.hbmMask = mask; + ii.hbmColor = color; + HCURSOR c = (HCURSOR)CreateIconIndirect(&ii); + DeleteObject(color); + DeleteObject(mask); + return c; +} + +/*-- DPI / display ----------------------------------------------------------*/ +static void _sapp_tc_win32_init_dpi(void) { + HMODULE user32 = GetModuleHandleW(L"user32.dll"); + if (user32) { + _sapp_tc.SetProcessDpiAwarenessContext = + (BOOL(WINAPI*)(void*))(void*)GetProcAddress(user32, "SetProcessDpiAwarenessContext"); + _sapp_tc.GetDpiForWindow = + (UINT(WINAPI*)(HWND))(void*)GetProcAddress(user32, "GetDpiForWindow"); + _sapp_tc.AdjustWindowRectExForDpi = + (BOOL(WINAPI*)(LPRECT, DWORD, BOOL, DWORD, UINT))(void*)GetProcAddress(user32, "AdjustWindowRectExForDpi"); + } + /* D3D11 backend is always DPI-aware; PER_MONITOR_AWARE_V2 when available + (Win10 1703+; the -4 magic constant avoids requiring a newer SDK) */ + if (_sapp_tc.SetProcessDpiAwarenessContext) { + _sapp_tc.SetProcessDpiAwarenessContext((void*)(intptr_t)-4); + _sapp_tc.app.dpi_aware = true; + } else { + _sapp_tc.app.dpi_aware = (TRUE == SetProcessDPIAware()); + } +} + +/* one dpi ratio source per window: the scale used to size the framebuffer is + the scale reported by sapp_window_dpi_scale() and applied to mouse coords */ +static void _sapp_tc_win32_update_scale(_sapp_tc_window_t* w) { + UINT dpi = 96; + if (_sapp_tc.app.dpi_aware && _sapp_tc.GetDpiForWindow && w->hwnd) { + dpi = _sapp_tc.GetDpiForWindow(w->hwnd); + if (dpi == 0) dpi = 96; + } + w->window_scale = (float)dpi / 96.0f; + const bool high_dpi = !w->desc.no_high_dpi; + w->dpi_scale = high_dpi ? w->window_scale : 1.0f; + w->mouse_scale = high_dpi ? 1.0f : 1.0f / w->window_scale; +} + +static void _sapp_tc_win32_update_refresh(_sapp_tc_window_t* w) { + /* this window's display refresh period: the timer-pacing fallback for + ticks that end without a Present (not the primary pacing source -- + that is the swapchain's frame latency waitable) */ + w->refresh_period = 1.0 / 60.0; + if (!w->hmonitor) return; + MONITORINFOEXW mi; + memset(&mi, 0, sizeof(mi)); + mi.cbSize = sizeof(mi); + if (GetMonitorInfoW(w->hmonitor, (MONITORINFO*)&mi)) { + DEVMODEW dm; + memset(&dm, 0, sizeof(dm)); + dm.dmSize = sizeof(dm); + if (EnumDisplaySettingsW(mi.szDevice, ENUM_CURRENT_SETTINGS, &dm) && + (dm.dmDisplayFrequency > 1)) { + w->refresh_period = 1.0 / (double)dm.dmDisplayFrequency; + } + } +} + +/*-- console (process-global) -----------------------------------------------*/ +static void _sapp_tc_win32_init_console(void) { + const sapp_desc* d = &_sapp_tc.app.desc; + if (d->win32.console_create || d->win32.console_attach) { + BOOL con_valid = FALSE; + if (d->win32.console_attach) { + con_valid = AttachConsole(ATTACH_PARENT_PROCESS); + } + if (!con_valid && d->win32.console_create) { + con_valid = AllocConsole(); + } + if (con_valid) { + FILE* res_o = NULL; + FILE* res_e = NULL; + freopen_s(&res_o, "CON", "w", stdout); + freopen_s(&res_e, "CON", "w", stderr); + } + } + if (d->win32.console_utf8) { + _sapp_tc.app.orig_codepage = GetConsoleOutputCP(); + if (SetConsoleOutputCP(CP_UTF8)) { + _sapp_tc.app.console_utf8_set = true; + } + } +} + +static void _sapp_tc_win32_restore_console(void) { + if (_sapp_tc.app.console_utf8_set) { + SetConsoleOutputCP(_sapp_tc.app.orig_codepage); + } +} + +/*-- D3D11 / DXGI -----------------------------------------------------------*/ +static HRESULT _sapp_tc_d3d11_try_create_device(UINT flags) { + D3D_FEATURE_LEVEL levels[] = { D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0 }; + D3D_FEATURE_LEVEL out_level; + HRESULT hr = D3D11CreateDevice(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, flags, + levels, 2, D3D11_SDK_VERSION, + &_sapp_tc.app.device, &out_level, &_sapp_tc.app.device_context); + if (FAILED(hr)) { + /* 11.1-unaware runtimes report E_INVALIDARG for the 11_1 entry */ + hr = D3D11CreateDevice(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, flags, + &levels[1], 1, D3D11_SDK_VERSION, + &_sapp_tc.app.device, &out_level, &_sapp_tc.app.device_context); + } + return hr; +} + +static bool _sapp_tc_d3d11_create_device(void) { + UINT flags = D3D11_CREATE_DEVICE_SINGLETHREADED | D3D11_CREATE_DEVICE_BGRA_SUPPORT; +#if defined(SOKOL_DEBUG) + flags |= D3D11_CREATE_DEVICE_DEBUG; +#endif + HRESULT hr = _sapp_tc_d3d11_try_create_device(flags); +#if defined(SOKOL_DEBUG) + if (FAILED(hr)) { + /* debug layer not installed: retry without it (upstream behavior) */ + hr = _sapp_tc_d3d11_try_create_device(flags & ~(UINT)D3D11_CREATE_DEVICE_DEBUG); + } +#endif + return SUCCEEDED(hr); +} + +static void _sapp_tc_d3d11_destroy_render_targets(_sapp_tc_window_t* w) { + _SAPP_TC_RELEASE(w->msaa_rtv); + _SAPP_TC_RELEASE(w->msaa_rt); + _SAPP_TC_RELEASE(w->dsv); + _SAPP_TC_RELEASE(w->ds); + _SAPP_TC_RELEASE(w->rtv); + _SAPP_TC_RELEASE(w->rt); +} + +static bool _sapp_tc_d3d11_create_render_targets(_sapp_tc_window_t* w) { + ID3D11Device* dev = _sapp_tc.app.device; + if (!dev || !w->swap_chain) return false; + if (FAILED(w->swap_chain->GetBuffer(0, __uuidof(ID3D11Texture2D), (void**)&w->rt))) return false; + if (FAILED(dev->CreateRenderTargetView((ID3D11Resource*)w->rt, NULL, &w->rtv))) return false; + D3D11_TEXTURE2D_DESC td; + memset(&td, 0, sizeof(td)); + td.Width = (UINT)(w->fb_width > 0 ? w->fb_width : 1); + td.Height = (UINT)(w->fb_height > 0 ? w->fb_height : 1); + td.MipLevels = 1; + td.ArraySize = 1; + td.Usage = D3D11_USAGE_DEFAULT; + td.SampleDesc.Count = (UINT)w->desc.sample_count; + td.SampleDesc.Quality = (w->desc.sample_count > 1) ? (UINT)D3D11_STANDARD_MULTISAMPLE_PATTERN : 0; + if (w->desc.sample_count > 1) { + /* flip-model swapchains are always single-sampled; render into a + separate MSAA texture, sokol_gfx resolves into the backbuffer */ + td.Format = w->color_fmt; + td.BindFlags = D3D11_BIND_RENDER_TARGET; + if (FAILED(dev->CreateTexture2D(&td, NULL, &w->msaa_rt))) return false; + if (FAILED(dev->CreateRenderTargetView((ID3D11Resource*)w->msaa_rt, NULL, &w->msaa_rtv))) return false; + } + td.Format = DXGI_FORMAT_D24_UNORM_S8_UINT; + td.BindFlags = D3D11_BIND_DEPTH_STENCIL; + if (FAILED(dev->CreateTexture2D(&td, NULL, &w->ds))) return false; + if (FAILED(dev->CreateDepthStencilView((ID3D11Resource*)w->ds, NULL, &w->dsv))) return false; + return true; +} + +static void _sapp_tc_d3d11_resize(_sapp_tc_window_t* w) { + if (!w->swap_chain) return; + _sapp_tc_d3d11_destroy_render_targets(w); + /* flip model: EVERY backbuffer reference must be gone before + ResizeBuffers, including an RTV still bound on the shared immediate + context -- otherwise ResizeBuffers fails and the next Present crashes */ + if (_sapp_tc.app.device_context) { + _sapp_tc.app.device_context->OMSetRenderTargets(0, NULL, NULL); + _sapp_tc.app.device_context->Flush(); + } + /* the creation flags (frame latency waitable!) must be passed unchanged + on every resize, or the waitable handle is silently invalidated */ + w->swap_chain->ResizeBuffers(2, (UINT)w->fb_width, (UINT)w->fb_height, + w->color_fmt, w->swapchain_flags); + _sapp_tc_d3d11_create_render_targets(w); +} + +static bool _sapp_tc_d3d11_create_swapchain(_sapp_tc_window_t* w) { + IDXGIDevice1* dxgi_device = 0; + IDXGIAdapter* adapter = 0; + IDXGIFactory2* factory = 0; + if (FAILED(_sapp_tc.app.device->QueryInterface(__uuidof(IDXGIDevice1), (void**)&dxgi_device))) { + return false; + } + bool ok = false; + if (SUCCEEDED(dxgi_device->GetAdapter(&adapter)) && + SUCCEEDED(adapter->GetParent(__uuidof(IDXGIFactory2), (void**)&factory))) { + DXGI_SWAP_CHAIN_DESC1 d; + memset(&d, 0, sizeof(d)); + d.Width = (UINT)(w->fb_width > 0 ? w->fb_width : 1); + d.Height = (UINT)(w->fb_height > 0 ? w->fb_height : 1); + d.Format = w->color_fmt; + d.SampleDesc.Count = 1; /* MSAA lives in a separate render target */ + d.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + d.BufferCount = 2; + d.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; + d.Scaling = DXGI_SCALING_STRETCH; + d.AlphaMode = DXGI_ALPHA_MODE_IGNORE; + d.Flags = DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT; + HRESULT hr = factory->CreateSwapChainForHwnd((IUnknown*)_sapp_tc.app.device, + w->hwnd, &d, NULL, NULL, &w->swap_chain); + if (FAILED(hr)) { + /* no waitable support: plain flip swapchain, timer-paced ticks */ + d.Flags = 0; + hr = factory->CreateSwapChainForHwnd((IUnknown*)_sapp_tc.app.device, + w->hwnd, &d, NULL, NULL, &w->swap_chain); + } + if (SUCCEEDED(hr)) { + w->swapchain_flags = d.Flags; + /* fullscreen is driven here (borderless); kill DXGI's Alt-Enter */ + factory->MakeWindowAssociation(w->hwnd, DXGI_MWA_NO_ALT_ENTER | DXGI_MWA_NO_PRINT_SCREEN); + if (d.Flags & DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT) { + IDXGISwapChain2* sc2 = 0; + if (SUCCEEDED(w->swap_chain->QueryInterface(__uuidof(IDXGISwapChain2), (void**)&sc2))) { + sc2->SetMaximumFrameLatency(1); + w->frame_wait = sc2->GetFrameLatencyWaitableObject(); + sc2->Release(); + } + } + ok = _sapp_tc_d3d11_create_render_targets(w); + } + } + _SAPP_TC_RELEASE(factory); + _SAPP_TC_RELEASE(adapter); + _SAPP_TC_RELEASE(dxgi_device); + return ok; +} + +/* re-sample client size + per-monitor DPI; true when the framebuffer size + changed (drives ResizeBuffers). A 0x0 client area means minimized: report + "no change" and keep the old sizes (sokol_app.h parity, sokol#1465). */ +static bool _sapp_tc_win32_update_dimensions(_sapp_tc_window_t* w) { + RECT rect; + if (!GetClientRect(w->hwnd, &rect)) return false; + const int cw = (int)(rect.right - rect.left); + const int ch = (int)(rect.bottom - rect.top); + if ((cw == 0) || (ch == 0)) return false; + HMONITOR mon = MonitorFromWindow(w->hwnd, MONITOR_DEFAULTTONEAREST); + if (mon != w->hmonitor) { + w->hmonitor = mon; + _sapp_tc_win32_update_refresh(w); + } + _sapp_tc_win32_update_scale(w); + w->win_width = (int)((float)cw / w->window_scale + 0.5f); + w->win_height = (int)((float)ch / w->window_scale + 0.5f); + const bool high_dpi = !w->desc.no_high_dpi; + int fbw = high_dpi ? cw : w->win_width; + int fbh = high_dpi ? ch : w->win_height; + if (fbw <= 0) fbw = 1; + if (fbh <= 0) fbh = 1; + if ((fbw != w->fb_width) || (fbh != w->fb_height)) { + w->fb_width = fbw; + w->fb_height = fbh; + return true; + } + return false; +} + +/*-- input event helpers ----------------------------------------------------*/ +static void _sapp_tc_win32_mouse_update(_sapp_tc_window_t* w, LPARAM lParam) { + /* event coordinates are framebuffer pixels (sokol_app.h convention): + client px when high-dpi, logical points otherwise */ + const float x = (float)GET_X_LPARAM(lParam) * w->mouse_scale; + const float y = (float)GET_Y_LPARAM(lParam) * w->mouse_scale; + if (w->mouse_pos_valid) { + w->mouse_dx = x - w->mouse_x; + w->mouse_dy = y - w->mouse_y; + } else { + w->mouse_dx = 0.0f; + w->mouse_dy = 0.0f; + w->mouse_pos_valid = true; + } + w->mouse_x = x; + w->mouse_y = y; +} + +static void _sapp_tc_win32_mouse_event(_sapp_tc_window_t* w, sapp_event_type type, sapp_mousebutton btn) { + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = type; + e.mouse_button = btn; + e.mouse_x = w->mouse_x; + e.mouse_y = w->mouse_y; + e.mouse_dx = w->mouse_dx; + e.mouse_dy = w->mouse_dy; + e.modifiers = _sapp_tc_win32_mods(); + _sapp_tc_send(w, &e); +} + +static void _sapp_tc_win32_scroll_event(_sapp_tc_window_t* w, float x, float y) { + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_MOUSE_SCROLL; + e.mouse_x = w->mouse_x; + e.mouse_y = w->mouse_y; + e.scroll_x = x; + e.scroll_y = y; + e.modifiers = _sapp_tc_win32_mods(); + _sapp_tc_send(w, &e); +} + +static void _sapp_tc_win32_key_event(_sapp_tc_window_t* w, sapp_event_type type, int scancode, bool repeat) { + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = type; + e.key_code = _sapp_tc_translate_key(scancode); + e.key_repeat = repeat; + e.modifiers = _sapp_tc_win32_mods(); + _sapp_tc_send(w, &e); + /* Ctrl+V paste notification (exact-CTRL match, sokol_app.h parity); the + app reads the clipboard in its handler */ + if ((type == SAPP_EVENTTYPE_KEY_DOWN) && _sapp_tc.app.clipboard_enabled && + (e.modifiers == SAPP_MODIFIER_CTRL) && (e.key_code == SAPP_KEYCODE_V)) { + sapp_event pe; + memset(&pe, 0, sizeof(pe)); + pe.type = SAPP_EVENTTYPE_CLIPBOARD_PASTED; + pe.modifiers = SAPP_MODIFIER_CTRL; + _sapp_tc_send(w, &pe); + } +} + +static void _sapp_tc_win32_char_event(_sapp_tc_window_t* w, uint32_t c, bool repeat) { + /* UTF-16 surrogate pair accumulation (per window) */ + if ((c >= 0xD800) && (c <= 0xDBFF)) { + w->surrogate = (WCHAR)c; + return; + } + if ((c >= 0xDC00) && (c <= 0xDFFF)) { + if (!w->surrogate) return; + c = ((((uint32_t)w->surrogate - 0xD800) << 10) | (c - 0xDC00)) + 0x10000; + w->surrogate = 0; + } + if ((c < 32) || (c == 127)) return; /* control characters */ + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_CHAR; + e.char_code = c; + e.key_repeat = repeat; + e.modifiers = _sapp_tc_win32_mods(); + _sapp_tc_send(w, &e); +} + +static void _sapp_tc_win32_app_event(_sapp_tc_window_t* w, sapp_event_type type) { + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = type; + _sapp_tc_send(w, &e); +} + +/* refcounted capture across buttons: dragging with two buttons held must not + drop capture when the first one is released */ +static void _sapp_tc_win32_capture_mouse(_sapp_tc_window_t* w, uint8_t btn_mask) { + if (w->capture_mask == 0) { + SetCapture(w->hwnd); + } + w->capture_mask |= btn_mask; +} + +static void _sapp_tc_win32_release_mouse(_sapp_tc_window_t* w, uint8_t btn_mask) { + if (w->capture_mask != 0) { + w->capture_mask &= (uint8_t)~btn_mask; + if (w->capture_mask == 0) { + ReleaseCapture(); + } + } +} + +static void _sapp_tc_win32_files_dropped(_sapp_tc_window_t* w, HDROP hdrop) { + if (!_sapp_tc.app.drop_enabled || !_sapp_tc.app.drop_buffer) { + DragFinish(hdrop); + return; + } + const int max_files = _sapp_tc.app.drop_max_files; + const int max_path = _sapp_tc.app.drop_max_path_length; + memset(_sapp_tc.app.drop_buffer, 0, (size_t)max_files * (size_t)max_path); + _sapp_tc.app.drop_num_files = 0; + int num = (int)DragQueryFileW(hdrop, 0xFFFFFFFF, NULL, 0); + if (num > max_files) num = max_files; + bool ok = (num > 0); + WCHAR* wpath = (WCHAR*)calloc((size_t)max_path, sizeof(WCHAR)); + if (!wpath) ok = false; + for (int i = 0; ok && (i < num); i++) { + if (0 == DragQueryFileW(hdrop, (UINT)i, wpath, (UINT)max_path)) { + ok = false; + break; + } + char* dst = _sapp_tc.app.drop_buffer + (size_t)i * (size_t)max_path; + if (!_sapp_tc_win32_wide_to_utf8(wpath, dst, max_path)) { + ok = false; /* path too long: drop everything */ + break; + } + } + POINT pt; + const BOOL in_client = DragQueryPoint(hdrop, &pt); + free(wpath); + DragFinish(hdrop); + if (!ok) { + memset(_sapp_tc.app.drop_buffer, 0, (size_t)max_files * (size_t)max_path); + return; + } + _sapp_tc.app.drop_num_files = num; + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_FILES_DROPPED; + if (in_client) { + e.mouse_x = (float)pt.x * w->mouse_scale; + e.mouse_y = (float)pt.y * w->mouse_scale; + } else { + e.mouse_x = w->mouse_x; + e.mouse_y = w->mouse_y; + } + e.modifiers = _sapp_tc_win32_mods(); + _sapp_tc_send(w, &e); +} + +/*-- fullscreen (main window; borderless style-swap, upstream parity) -------*/ +static void _sapp_tc_win32_set_fullscreen(bool fullscreen, UINT swp_flags) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w) return; + HMONITOR monitor = MonitorFromWindow(w->hwnd, MONITOR_DEFAULTTONEAREST); + MONITORINFO minfo; + memset(&minfo, 0, sizeof(minfo)); + minfo.cbSize = sizeof(minfo); + if (!GetMonitorInfoW(monitor, &minfo)) return; + _sapp_tc.app.fullscreen = fullscreen; + RECT rect; + DWORD style; + if (fullscreen) { + GetWindowRect(w->hwnd, &w->stored_window_rect); + style = WS_POPUP | WS_SYSMENU | WS_VISIBLE; + rect = minfo.rcMonitor; + } else { + style = WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_CAPTION | WS_SYSMENU | + WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SIZEBOX; + rect = w->stored_window_rect; + } + SetWindowLongPtrW(w->hwnd, GWL_STYLE, (LONG_PTR)style); + SetWindowPos(w->hwnd, HWND_TOP, rect.left, rect.top, + rect.right - rect.left, rect.bottom - rect.top, + swp_flags | SWP_FRAMECHANGED); +} + +/*-- per-window tick + pacing ------------------------------------------------ + Primary pacing: the swapchain's frame latency waitable signals when a new + frame can be queued (true per-display vsync). One credit is consumed per + successful zero-timeout wait in the due-check; a Present returns it via + the swapchain. A tick that ends WITHOUT a Present (skip_present, occluded, + minimized) keeps `credit_held` and is re-paced by `earliest_next` -- never + waiting on the waitable again until the credit is spent -- so neither + busy-spinning (waitable stays signaled) nor starvation (credits leak away) + can occur. Windows without a waitable (fallback) are purely timer-paced. */ +static void _sapp_tc_win32_pace(_sapp_tc_window_t* w, double seconds) { + w->earliest_next = _sapp_tc_now() + seconds; +} + +static bool _sapp_tc_win32_window_due(_sapp_tc_window_t* w) { + if (!w || !w->swap_chain || w->in_tick) return false; + if (w->earliest_next > _sapp_tc_now()) return false; + /* Waitable pacing: the frame latency waitable is an auto-reset object that + the run-loop's MsgWaitForMultipleObjectsEx already waits on. That wait + CONSUMES the signal, so we must NOT re-wait the handle here (doing so + missed the consumed signal and stalled the render loop after frame 1). + Instead the run-loop records the consumed signal in waitable_ready. */ + if (w->frame_wait && !w->credit_held) { + if (!w->waitable_ready) return false; + } + return true; +} + +static void _sapp_tc_win32_tick(_sapp_tc_window_t* w, bool from_modal) { + if (w->in_tick || !w->swap_chain) return; + /* the due-check just consumed a waitable credit (unless one was already + held); hold it until a real Present returns it through the swapchain. + Clear waitable_ready: this signal is now spent (the next one comes from + the run-loop's MsgWait after this frame is presented). */ + if (w->frame_wait) { w->credit_held = true; w->waitable_ready = false; } + _sapp_tc_timing_update(&w->timing); + w->frame_duration = w->timing.smooth_dt; + const int swap_interval = (_sapp_tc.app.desc.swap_interval > 0) ? _sapp_tc.app.desc.swap_interval : 1; + + if (w->is_main) { + /* resize is deliberately coalesced here, NOT in WM_SIZE (upstream: + per-WM_SIZE ResizeBuffers blows up memory on some drivers). During + a modal size-move loop the swapchain keeps its size (DXGI stretch) + and one resize lands on the first normal tick after the drag. */ + if (!from_modal && _sapp_tc_win32_update_dimensions(w)) { + _sapp_tc_d3d11_resize(w); + _sapp_tc_win32_app_event(w, SAPP_EVENTTYPE_RESIZED); + } + w->in_tick = true; + if (!_sapp_tc.app.init_called) { + _sapp_tc.app.init_called = true; + if (_sapp_tc.app.desc.init_cb) { + _sapp_tc.app.desc.init_cb(); + } else if (_sapp_tc.app.desc.init_userdata_cb) { + _sapp_tc.app.desc.init_userdata_cb(_sapp_tc.app.desc.user_data); + } + } + if (_sapp_tc.app.desc.frame_cb) { + _sapp_tc.app.desc.frame_cb(); + } else if (_sapp_tc.app.desc.frame_userdata_cb) { + _sapp_tc.app.desc.frame_userdata_cb(_sapp_tc.app.desc.user_data); + } + _sapp_tc.app.frame_count++; + w->in_tick = false; + bool presented = false; + if (_sapp_tc.app.skip_present) { + /* one-shot event-driven present suppression (TrussC patch: keeps + the last image on screen when a frame decides not to draw) */ + _sapp_tc.app.skip_present = false; + } else { + const UINT flags = from_modal ? DXGI_PRESENT_DO_NOT_WAIT : 0; + const HRESULT hr = w->swap_chain->Present((UINT)swap_interval, flags); + presented = SUCCEEDED(hr) && (hr != DXGI_STATUS_OCCLUDED); + } + if (presented) { + w->credit_held = false; + } + if (w->iconified) { + /* minimized: frame_cb keeps running, throttled (upstream Sleep(16) + / mac fallback-timer parity) */ + _sapp_tc_win32_pace(w, 0.0166 * (double)swap_interval); + } else if (presented && w->frame_wait) { + w->earliest_next = 0.0; /* vsync pacing via the waitable */ + } else { + _sapp_tc_win32_pace(w, w->refresh_period * (double)swap_interval); + } + } else { + /* secondary window */ + if (w->iconified) { + _sapp_tc_win32_pace(w, w->refresh_period); + return; + } + if (w->occluded) { + /* cheap visibility poll: a fully covered window costs one + present-test per pace period and never renders or stalls */ + const HRESULT hr = w->swap_chain->Present(0, DXGI_PRESENT_TEST); + if (hr == DXGI_STATUS_OCCLUDED) { + _sapp_tc_win32_pace(w, w->refresh_period); + return; + } + w->occluded = false; + _sapp_tc_win32_app_event(w, SAPP_EVENTTYPE_RESUMED); + } + if (!from_modal && _sapp_tc_win32_update_dimensions(w)) { + _sapp_tc_d3d11_resize(w); + _sapp_tc_win32_app_event(w, SAPP_EVENTTYPE_RESIZED); + } + w->in_tick = true; + if (w->desc.tick_cb) { + sapp_window handle = { w->win_id }; + w->desc.tick_cb(handle, w->desc.user_data); + } + w->in_tick = false; + const UINT flags = from_modal ? DXGI_PRESENT_DO_NOT_WAIT : 0; + const HRESULT hr = w->swap_chain->Present(1, flags); + if (hr == DXGI_STATUS_OCCLUDED) { + w->occluded = true; + _sapp_tc_win32_app_event(w, SAPP_EVENTTYPE_SUSPENDED); + _sapp_tc_win32_pace(w, w->refresh_period); + } else if (SUCCEEDED(hr)) { + w->credit_held = false; + if (w->frame_wait) { + w->earliest_next = 0.0; + } else { + _sapp_tc_win32_pace(w, w->refresh_period); + } + } else { + _sapp_tc_win32_pace(w, w->refresh_period); + } + } +} + +/*-- window procedure --------------------------------------------------------*/ +static LRESULT CALLBACK _sapp_tc_wndproc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { + if (msg == WM_NCCREATE) { + /* stash the window struct passed via CreateWindowExW lpCreateParams */ + const CREATESTRUCTW* cs = (const CREATESTRUCTW*)lParam; + SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR)cs->lpCreateParams); + return DefWindowProcW(hwnd, msg, wParam, lParam); + } + _sapp_tc_window_t* w = (_sapp_tc_window_t*)GetWindowLongPtrW(hwnd, GWLP_USERDATA); + if (!w || !w->ready) { + /* creation-time messages are swallowed (upstream in_create_window) */ + return DefWindowProcW(hwnd, msg, wParam, lParam); + } + switch (msg) { + case WM_CLOSE: + if (w->is_main) { + /* the cancellable quit dance (sokol_app.h parity): sapp_quit() + pre-sets quit_ordered (unvetoable); otherwise QUIT_REQUESTED + is delivered and a handler may sapp_cancel_quit() */ + if (!_sapp_tc.app.quit_ordered) { + _sapp_tc.app.quit_requested = true; + _sapp_tc_win32_app_event(w, SAPP_EVENTTYPE_QUIT_REQUESTED); + if (_sapp_tc.app.quit_requested) { + _sapp_tc.app.quit_ordered = true; + } + } + if (_sapp_tc.app.quit_ordered) { + PostQuitMessage(0); + } + } else { + /* user closed a secondary window: notify; the app accepts by + calling sapp_destroy_window() (w may be freed after this) */ + if (w->desc.close_cb) { + sapp_window handle = { w->win_id }; + w->desc.close_cb(handle, w->desc.user_data); + } + } + return 0; /* never let DefWindowProc destroy the window here */ + case WM_SYSCOMMAND: + switch (wParam & 0xFFF0) { + case SC_SCREENSAVE: + case SC_MONITORPOWER: + if (_sapp_tc.app.fullscreen) { + return 0; /* no screensaver/monitor-off in fullscreen */ + } + break; + case SC_KEYMENU: + return 0; /* don't let Alt freeze the render loop */ + default: + break; + } + break; + case WM_ERASEBKGND: + return 1; + case WM_SIZE: { + /* only iconify tracking here; RESIZED + ResizeBuffers are + coalesced into the tick (upstream parity) */ + const bool iconified = (wParam == SIZE_MINIMIZED); + if (iconified != w->iconified) { + w->iconified = iconified; + _sapp_tc_win32_app_event(w, iconified ? SAPP_EVENTTYPE_ICONIFIED + : SAPP_EVENTTYPE_RESTORED); + if (!iconified) { + w->earliest_next = 0.0; /* resume promptly */ + } + } + break; + } + case WM_SETFOCUS: + _sapp_tc_win32_app_event(w, SAPP_EVENTTYPE_FOCUSED); + break; + case WM_KILLFOCUS: + _sapp_tc_win32_app_event(w, SAPP_EVENTTYPE_UNFOCUSED); + break; + case WM_SETCURSOR: + if (LOWORD(lParam) == HTCLIENT) { + _sapp_tc_win32_apply_cursor(_sapp_tc.app.current_cursor, + _sapp_tc.app.mouse_shown, true); + return TRUE; + } + break; + case WM_DPICHANGED: { + /* per-monitor-v2 only: adopt the OS-proposed rect; scale and + sizes are re-sampled on the next tick */ + const RECT* r = (const RECT*)lParam; + SetWindowPos(hwnd, NULL, r->left, r->top, + r->right - r->left, r->bottom - r->top, + SWP_NOZORDER | SWP_NOACTIVATE); + _sapp_tc_win32_update_refresh(w); + break; + } + case WM_LBUTTONDOWN: + _sapp_tc_win32_mouse_update(w, lParam); + _sapp_tc_win32_mouse_event(w, SAPP_EVENTTYPE_MOUSE_DOWN, SAPP_MOUSEBUTTON_LEFT); + _sapp_tc_win32_capture_mouse(w, (uint8_t)(1 << SAPP_MOUSEBUTTON_LEFT)); + break; + case WM_LBUTTONUP: + _sapp_tc_win32_mouse_update(w, lParam); + _sapp_tc_win32_mouse_event(w, SAPP_EVENTTYPE_MOUSE_UP, SAPP_MOUSEBUTTON_LEFT); + _sapp_tc_win32_release_mouse(w, (uint8_t)(1 << SAPP_MOUSEBUTTON_LEFT)); + break; + case WM_RBUTTONDOWN: + _sapp_tc_win32_mouse_update(w, lParam); + _sapp_tc_win32_mouse_event(w, SAPP_EVENTTYPE_MOUSE_DOWN, SAPP_MOUSEBUTTON_RIGHT); + _sapp_tc_win32_capture_mouse(w, (uint8_t)(1 << SAPP_MOUSEBUTTON_RIGHT)); + break; + case WM_RBUTTONUP: + _sapp_tc_win32_mouse_update(w, lParam); + _sapp_tc_win32_mouse_event(w, SAPP_EVENTTYPE_MOUSE_UP, SAPP_MOUSEBUTTON_RIGHT); + _sapp_tc_win32_release_mouse(w, (uint8_t)(1 << SAPP_MOUSEBUTTON_RIGHT)); + break; + case WM_MBUTTONDOWN: + _sapp_tc_win32_mouse_update(w, lParam); + _sapp_tc_win32_mouse_event(w, SAPP_EVENTTYPE_MOUSE_DOWN, SAPP_MOUSEBUTTON_MIDDLE); + _sapp_tc_win32_capture_mouse(w, (uint8_t)(1 << SAPP_MOUSEBUTTON_MIDDLE)); + break; + case WM_MBUTTONUP: + _sapp_tc_win32_mouse_update(w, lParam); + _sapp_tc_win32_mouse_event(w, SAPP_EVENTTYPE_MOUSE_UP, SAPP_MOUSEBUTTON_MIDDLE); + _sapp_tc_win32_release_mouse(w, (uint8_t)(1 << SAPP_MOUSEBUTTON_MIDDLE)); + break; + case WM_MOUSEMOVE: + _sapp_tc_win32_mouse_update(w, lParam); + if (!w->mouse_tracked) { + w->mouse_tracked = true; + TRACKMOUSEEVENT tme; + memset(&tme, 0, sizeof(tme)); + tme.cbSize = sizeof(tme); + tme.dwFlags = TME_LEAVE; + tme.hwndTrack = w->hwnd; + TrackMouseEvent(&tme); + w->mouse_dx = 0.0f; + w->mouse_dy = 0.0f; + _sapp_tc_win32_mouse_event(w, SAPP_EVENTTYPE_MOUSE_ENTER, SAPP_MOUSEBUTTON_INVALID); + } + _sapp_tc_win32_mouse_event(w, SAPP_EVENTTYPE_MOUSE_MOVE, SAPP_MOUSEBUTTON_INVALID); + break; + case WM_MOUSELEAVE: + w->mouse_dx = 0.0f; + w->mouse_dy = 0.0f; + w->mouse_tracked = false; + _sapp_tc_win32_mouse_event(w, SAPP_EVENTTYPE_MOUSE_LEAVE, SAPP_MOUSEBUTTON_INVALID); + break; + case WM_MOUSEWHEEL: + _sapp_tc_win32_scroll_event(w, 0.0f, + (float)GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA); + break; + case WM_MOUSEHWHEEL: + _sapp_tc_win32_scroll_event(w, + -(float)GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA, 0.0f); + break; + case WM_CHAR: + _sapp_tc_win32_char_event(w, (uint32_t)wParam, 0 != (lParam & 0x40000000)); + break; + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + /* 9-bit scancode incl. the extended bit (distinguishes L/R + Ctrl/Alt and keypad Enter); SYS variants fall through to + DefWindowProc below so Alt+F4 etc. keep working */ + _sapp_tc_win32_key_event(w, SAPP_EVENTTYPE_KEY_DOWN, + (int)(HIWORD(lParam) & 0x1FF), 0 != (lParam & 0x40000000)); + break; + case WM_KEYUP: + case WM_SYSKEYUP: + _sapp_tc_win32_key_event(w, SAPP_EVENTTYPE_KEY_UP, + (int)(HIWORD(lParam) & 0x1FF), false); + break; + case WM_ENTERSIZEMOVE: + /* the modal size-move loop blocks the outer message loop; a + coarse WM_TIMER (~64Hz) keeps all windows rendering meanwhile */ + SetTimer(hwnd, _SAPP_TC_MODAL_TIMER, USER_TIMER_MINIMUM, NULL); + break; + case WM_EXITSIZEMOVE: + KillTimer(hwnd, _SAPP_TC_MODAL_TIMER); + break; + case WM_TIMER: + if (wParam == _SAPP_TC_MODAL_TIMER) { + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* tw = _sapp_tc.windows[i]; + if (!tw) continue; + /* the outer run-loop (which records the waitable signal in + waitable_ready) is blocked in this modal size/move loop, so + poll the frame latency waitable here instead -- otherwise the + due-check never sees a credit and drag/resize stops rendering */ + if (tw->frame_wait && !tw->credit_held && !tw->waitable_ready && + WaitForSingleObject(tw->frame_wait, 0) == WAIT_OBJECT_0) { + tw->waitable_ready = true; + } + if (_sapp_tc_win32_window_due(tw)) { + _sapp_tc_win32_tick(tw, true); + } + } + } + break; + case WM_NCLBUTTONDOWN: + /* grabbing the title bar stalls rendering for ~500ms; posting a + synthetic mouse-move breaks the wait (upstream workaround) */ + if (wParam == HTCAPTION) { + POINT point; + if (GetCursorPos(&point)) { + ScreenToClient(hwnd, &point); + PostMessageW(hwnd, WM_MOUSEMOVE, 0, + (LPARAM)(((uint32_t)point.x) | (((uint32_t)point.y) << 16))); + } + } + break; + case WM_DROPFILES: + _sapp_tc_win32_files_dropped(w, (HDROP)wParam); + break; + default: + break; + } + return DefWindowProcW(hwnd, msg, wParam, lParam); +} + +/*-- window creation / destruction ------------------------------------------*/ +static void _sapp_tc_win32_ensure_wndclass(void) { + if (_sapp_tc.wndclass_registered) return; + _sapp_tc.wndclass_registered = true; + WNDCLASSW wndclassw; + memset(&wndclassw, 0, sizeof(wndclassw)); + wndclassw.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; + wndclassw.lpfnWndProc = _sapp_tc_wndproc; + wndclassw.hInstance = GetModuleHandleW(NULL); + wndclassw.hCursor = LoadCursor(NULL, IDC_ARROW); + wndclassw.hIcon = LoadIcon(NULL, IDI_WINLOGO); + wndclassw.lpszClassName = L"SOKOLAPP_TC"; + RegisterClassW(&wndclassw); +} + +/* create the HWND at the desc's logical size: first against 96 DPI, then -- + per-monitor-v2 -- resize the CLIENT area for the real DPI of the monitor + the window actually landed on (the standard create-then-resize dance) */ +static bool _sapp_tc_win32_create_native_window(_sapp_tc_window_t* w, const wchar_t* title, + DWORD style, DWORD ex_style, int x, int y, bool use_default_size) { + _sapp_tc_win32_ensure_wndclass(); + int outer_w = CW_USEDEFAULT; + int outer_h = CW_USEDEFAULT; + if (!use_default_size) { + RECT rect = { 0, 0, (LONG)w->desc.width, (LONG)w->desc.height }; + AdjustWindowRectEx(&rect, style, FALSE, ex_style); + outer_w = (int)(rect.right - rect.left); + outer_h = (int)(rect.bottom - rect.top); + } + w->hwnd = CreateWindowExW(ex_style, L"SOKOLAPP_TC", title, style, + (x >= 0) ? x : CW_USEDEFAULT, + (y >= 0) ? y : CW_USEDEFAULT, + outer_w, outer_h, + NULL, NULL, GetModuleHandleW(NULL), w); + if (!w->hwnd) return false; + w->hmonitor = MonitorFromWindow(w->hwnd, MONITOR_DEFAULTTONEAREST); + _sapp_tc_win32_update_refresh(w); + _sapp_tc_win32_update_scale(w); + if (!use_default_size && (w->window_scale != 1.0f)) { + const LONG pw = (LONG)((float)w->desc.width * w->window_scale + 0.5f); + const LONG ph = (LONG)((float)w->desc.height * w->window_scale + 0.5f); + RECT rect = { 0, 0, pw, ph }; + if (_sapp_tc.GetDpiForWindow && _sapp_tc.AdjustWindowRectExForDpi) { + _sapp_tc.AdjustWindowRectExForDpi(&rect, style, FALSE, ex_style, + _sapp_tc.GetDpiForWindow(w->hwnd)); + } else { + AdjustWindowRectEx(&rect, style, FALSE, ex_style); + } + SetWindowPos(w->hwnd, NULL, 0, 0, + (int)(rect.right - rect.left), (int)(rect.bottom - rect.top), + SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); + } + _sapp_tc_timing_reset(&w->timing); + return true; +} + +static void _sapp_tc_win32_destroy_window_resources(_sapp_tc_window_t* w) { + _sapp_tc_d3d11_destroy_render_targets(w); + if (w->frame_wait) { + CloseHandle(w->frame_wait); + w->frame_wait = NULL; + } + _SAPP_TC_RELEASE(w->swap_chain); + if (w->hwnd) { + KillTimer(w->hwnd, _SAPP_TC_MODAL_TIMER); + SetWindowLongPtrW(w->hwnd, GWLP_USERDATA, 0); /* wndproc goes inert */ + DestroyWindow(w->hwnd); + w->hwnd = NULL; + } +} + +static bool _sapp_tc_win32_create_main_window(void) { + const sapp_desc* d = &_sapp_tc.app.desc; + int slot = -1; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + if (_sapp_tc.windows[i] == 0) { slot = i; break; } + } + if (slot < 0) return false; + + _sapp_tc_window_t* w = new _sapp_tc_window_t(); + w->win_id = ++_sapp_tc.next_id; + w->is_main = true; + w->color_fmt = DXGI_FORMAT_R10G10B10A2_UNORM; /* TrussC 10-bit output */ + w->window_scale = w->dpi_scale = w->mouse_scale = 1.0f; + w->refresh_period = 1.0 / 60.0; + + /* per-window desc: the app callbacks are reached via the trampoline */ + w->desc.width = d->width; + w->desc.height = d->height; + w->desc.sample_count = d->sample_count; + w->desc.no_high_dpi = !d->high_dpi; + w->desc.event_cb = _sapp_tc_main_event_tramp; + + const DWORD style = WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_CAPTION | WS_SYSMENU | + WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SIZEBOX; + const DWORD ex_style = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE; + /* width/height 0: let the OS pick (CW_USEDEFAULT, upstream parity) */ + const bool use_default_size = (d->width <= 0) || (d->height <= 0); + if (!_sapp_tc_win32_create_native_window(w, _sapp_tc.app.window_title_wide, + style, ex_style, -1, -1, use_default_size)) { + delete w; + return false; + } + _sapp_tc.windows[slot] = w; + _sapp_tc.app.main = w; + w->ready = true; + _sapp_tc_win32_update_dimensions(w); /* silent startup sizing (no RESIZED) */ + if (d->fullscreen) { + _sapp_tc_win32_set_fullscreen(true, SWP_HIDEWINDOW); + _sapp_tc_win32_update_dimensions(w); + } + ShowWindow(w->hwnd, SW_SHOW); + /* drop target is always registered; the handler gates on enable_dragndrop */ + DragAcceptFiles(w->hwnd, TRUE); + if (!_sapp_tc_d3d11_create_swapchain(w)) { + return false; + } + _sapp_tc.app.valid = true; + return true; +} + +/*-- the run loop ------------------------------------------------------------- + Blocks in MsgWaitForMultipleObjectsEx on the due windows' frame latency + waitables (plus a timeout for timer-paced windows), drains ALL pending + messages, then ticks every due window. Replaces upstream's PeekMessage + busy-render-loop: the process sleeps whenever nothing is due. */ +static void _sapp_tc_win32_run_loop(void) { + bool done = false; + while (!done && !_sapp_tc.app.quit_ordered) { + HANDLE handles[_SAPP_TC_MAX_WINDOWS]; + _sapp_tc_window_t* handle_owner[_SAPP_TC_MAX_WINDOWS]; + DWORD num_handles = 0; + const double now = _sapp_tc_now(); + double wake_at = now + 0.1; /* robustness cap; messages wake us anyway */ + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* w = _sapp_tc.windows[i]; + if (!w || !w->swap_chain || w->in_tick) continue; + if (w->earliest_next > now) { + if (w->earliest_next < wake_at) wake_at = w->earliest_next; + } else if (w->frame_wait && !w->credit_held && !w->waitable_ready) { + handle_owner[num_handles] = w; + handles[num_handles++] = w->frame_wait; + } else { + wake_at = now; /* due immediately (timer-paced / credit or signal held) */ + } + } + double timeout_s = wake_at - now; + if (timeout_s < 0.0) timeout_s = 0.0; + DWORD wr = MsgWaitForMultipleObjectsEx(num_handles, handles, + (DWORD)(timeout_s * 1000.0), QS_ALLINPUT, MWMO_INPUTAVAILABLE); + /* If a frame latency waitable satisfied the wait, it was auto-reset here. + Record it so the due-check consumes THIS signal instead of re-waiting + the (now-unsignaled) handle. Only one handle is reported per wait; the + rest stay signaled and are caught on the next loop. */ + if (wr >= WAIT_OBJECT_0 && wr < WAIT_OBJECT_0 + num_handles) { + handle_owner[wr - WAIT_OBJECT_0]->waitable_ready = true; + } + MSG msg; + while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE)) { + if (WM_QUIT == msg.message) { + done = true; + continue; + } + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + if (done) break; + /* tick every due window (re-fetch each slot: a tick or a dispatched + message may have destroyed windows) */ + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* w = _sapp_tc.windows[i]; + if (_sapp_tc_win32_window_due(w)) { + _sapp_tc_win32_tick(w, false); + } + } + /* route programmatic quits through the same WM_CLOSE dance so the + QUIT_REQUESTED semantics stay identical (upstream parity) */ + if (_sapp_tc.app.quit_requested && _sapp_tc.app.main) { + PostMessageW(_sapp_tc.app.main->hwnd, WM_CLOSE, 0, 0); + } + } +} + +/*-- public API ---------------------------------------------------------------*/ +extern "C" { + +sapp_window sapp_create_window(const sapp_window_desc* desc) { + sapp_window invalid = { 0 }; + if (!desc) return invalid; + if (!_sapp_tc.app.valid || !_sapp_tc.app.device) return invalid; + _sapp_tc_init_keytable(); + + int slot = -1; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + if (_sapp_tc.windows[i] == 0) { slot = i; break; } + } + if (slot < 0) return invalid; + + _sapp_tc_window_t* w = new _sapp_tc_window_t(); + w->win_id = ++_sapp_tc.next_id; + w->desc = *desc; + if (w->desc.width <= 0) w->desc.width = 640; + if (w->desc.height <= 0) w->desc.height = 480; + if (w->desc.sample_count <= 0) w->desc.sample_count = 1; + w->color_fmt = DXGI_FORMAT_B8G8R8A8_UNORM; /* secondary windows are BGRA8 (mac parity) */ + w->window_scale = w->dpi_scale = w->mouse_scale = 1.0f; + w->refresh_period = 1.0 / 60.0; + + DWORD style; + if (w->desc.borderless) { + style = WS_POPUP | WS_SYSMENU; + } else { + style = WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_CAPTION | WS_SYSMENU | + WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SIZEBOX; + } + const DWORD ex_style = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE; + WCHAR title[256]; + _sapp_tc_win32_utf8_to_wide(w->desc.title ? w->desc.title : "TrussC", title, 256); + const int px = (desc->x >= 0) ? desc->x : 120; + const int py = (desc->y >= 0) ? desc->y : 120; + if (!_sapp_tc_win32_create_native_window(w, title, style, ex_style, px, py, false)) { + delete w; + return invalid; + } + w->ready = true; + _sapp_tc_win32_update_dimensions(w); + ShowWindow(w->hwnd, SW_SHOW); + DragAcceptFiles(w->hwnd, TRUE); + if (!_sapp_tc_d3d11_create_swapchain(w)) { + _sapp_tc_win32_destroy_window_resources(w); + delete w; + return invalid; + } + _sapp_tc.windows[slot] = w; + sapp_window handle = { w->win_id }; + return handle; +} + +void sapp_destroy_window(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w || w->is_main) return; /* the main window is owned by sapp_run */ + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + if (_sapp_tc.windows[i] == w) { _sapp_tc.windows[i] = 0; break; } + } + w->desc.close_cb = 0; + _sapp_tc_win32_destroy_window_resources(w); + delete w; +} + +bool sapp_window_valid(sapp_window win) { + return _sapp_tc_lookup(win) != 0; +} + +int sapp_window_width(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->win_width : 0; +} + +int sapp_window_height(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->win_height : 0; +} + +int sapp_window_framebuffer_width(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->fb_width : 0; +} + +int sapp_window_framebuffer_height(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->fb_height : 0; +} + +float sapp_window_dpi_scale(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->dpi_scale : 1.0f; +} + +int sapp_window_sample_count(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->desc.sample_count : 1; +} + +bool sapp_window_occluded(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->occluded : false; +} + +void sapp_window_set_title(sapp_window win, const char* title) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w || !w->hwnd || !title) return; + WCHAR wide[256]; + if (_sapp_tc_win32_utf8_to_wide(title, wide, 256)) { + SetWindowTextW(w->hwnd, wide); + } +} + +double sapp_window_frame_duration(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w) return 1.0 / 60.0; + return (w->frame_duration > 0.0) ? w->frame_duration : w->refresh_period; +} + +/* Metal handles do not exist on Windows */ +const void* sapp_window_metal_current_drawable(sapp_window win) { (void)win; return 0; } +const void* sapp_window_metal_depth_stencil_texture(sapp_window win) { (void)win; return 0; } +const void* sapp_window_metal_msaa_color_texture(sapp_window win) { (void)win; return 0; } +const void* sapp_window_macos_get_window(sapp_window win) { (void)win; return 0; } + +const void* sapp_window_d3d11_render_view(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w || !w->in_tick) return 0; + return (w->desc.sample_count > 1) ? (const void*)w->msaa_rtv : (const void*)w->rtv; +} + +const void* sapp_window_d3d11_resolve_view(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w || !w->in_tick || (w->desc.sample_count <= 1)) return 0; + return (const void*)w->rtv; +} + +const void* sapp_window_d3d11_depth_stencil_view(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w || !w->in_tick) return 0; + return (const void*)w->dsv; +} + +const void* sapp_window_win32_get_hwnd(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? (const void*)w->hwnd : 0; +} + +/* GL / X11 handles do not exist on Windows (D3D11 backend) */ +const void* sapp_window_x11_get_window(sapp_window win) { (void)win; return 0; } +uint32_t sapp_window_gl_framebuffer(sapp_window win) { (void)win; return 0; } + +/*-- sokol_app.h public API (Windows implementation lives here) --------------*/ + +void sapp_run(const sapp_desc* desc) { + if (!desc) return; + _sapp_tc.app.desc = *desc; + sapp_desc* d = &_sapp_tc.app.desc; + if (d->sample_count <= 0) d->sample_count = 1; + if (d->swap_interval <= 0) d->swap_interval = 1; + if (d->clipboard_size <= 0) d->clipboard_size = 8192; + if (d->max_dropped_files <= 0) d->max_dropped_files = 1; + if (d->max_dropped_file_path_length <= 0) d->max_dropped_file_path_length = 2048; + strncpy(_sapp_tc.app.window_title, d->window_title ? d->window_title : "sokol", + sizeof(_sapp_tc.app.window_title) - 1); + d->window_title = _sapp_tc.app.window_title; + _sapp_tc_win32_utf8_to_wide(_sapp_tc.app.window_title, + _sapp_tc.app.window_title_wide, + (int)(sizeof(_sapp_tc.app.window_title_wide) / sizeof(WCHAR))); + if (d->enable_clipboard) { + _sapp_tc.app.clipboard_enabled = true; + _sapp_tc.app.clipboard_size = d->clipboard_size; + _sapp_tc.app.clipboard = (char*)calloc(1, (size_t)d->clipboard_size); + } + if (d->enable_dragndrop) { + _sapp_tc.app.drop_enabled = true; + _sapp_tc.app.drop_max_files = d->max_dropped_files; + _sapp_tc.app.drop_max_path_length = d->max_dropped_file_path_length; + _sapp_tc.app.drop_buffer = (char*)calloc( + (size_t)d->max_dropped_files, (size_t)d->max_dropped_file_path_length); + } + _sapp_tc.app.mouse_shown = true; + _sapp_tc.app.current_cursor = SAPP_MOUSECURSOR_DEFAULT; + LARGE_INTEGER freq; + QueryPerformanceFrequency(&freq); + _sapp_tc.qpc_period = 1.0 / (double)freq.QuadPart; + _sapp_tc_win32_init_console(); + _sapp_tc_init_keytable(); + _sapp_tc_win32_init_dpi(); + _sapp_tc_win32_init_cursors(); + + if (_sapp_tc_d3d11_create_device()) { + if (_sapp_tc_win32_create_main_window()) { + _sapp_tc_win32_run_loop(); + } + } + + /* user cleanup runs BEFORE any GPU/window teardown (the app shuts down + sokol_gfx here, which still needs the device) */ + if (!_sapp_tc.app.cleanup_called) { + _sapp_tc.app.cleanup_called = true; + if (_sapp_tc.app.desc.cleanup_cb) { + _sapp_tc.app.desc.cleanup_cb(); + } else if (_sapp_tc.app.desc.cleanup_userdata_cb) { + _sapp_tc.app.desc.cleanup_userdata_cb(_sapp_tc.app.desc.user_data); + } + } + /* destroy any windows the app left open (secondary first, then main) */ + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* w = _sapp_tc.windows[i]; + if (!w || w->is_main) continue; + _sapp_tc.windows[i] = 0; + _sapp_tc_win32_destroy_window_resources(w); + delete w; + } + if (_sapp_tc.app.main) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + if (_sapp_tc.windows[i] == w) { _sapp_tc.windows[i] = 0; break; } + } + _sapp_tc.app.main = 0; + _sapp_tc_win32_destroy_window_resources(w); + delete w; + } + _SAPP_TC_RELEASE(_sapp_tc.app.device_context); + _SAPP_TC_RELEASE(_sapp_tc.app.device); + if (_sapp_tc.wndclass_registered) { + UnregisterClassW(L"SOKOLAPP_TC", GetModuleHandleW(NULL)); + _sapp_tc.wndclass_registered = false; + } + for (int i = 0; i < _SAPP_MOUSECURSOR_NUM; i++) { + if (_sapp_tc.app.custom_cursor_bound[i] && _sapp_tc.app.custom_cursors[i]) { + DestroyIcon((HICON)_sapp_tc.app.custom_cursors[i]); + _sapp_tc.app.custom_cursors[i] = NULL; + _sapp_tc.app.custom_cursor_bound[i] = false; + } + } + _sapp_tc_win32_restore_console(); + if (_sapp_tc.app.clipboard) { free(_sapp_tc.app.clipboard); _sapp_tc.app.clipboard = 0; } + if (_sapp_tc.app.drop_buffer) { free(_sapp_tc.app.drop_buffer); _sapp_tc.app.drop_buffer = 0; } + _sapp_tc.app.valid = false; +} + +bool sapp_isvalid(void) { + return _sapp_tc.app.valid; +} + +int sapp_width(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return (w && w->fb_width > 0) ? w->fb_width : 1; +} + +float sapp_widthf(void) { + return (float)sapp_width(); +} + +int sapp_height(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return (w && w->fb_height > 0) ? w->fb_height : 1; +} + +float sapp_heightf(void) { + return (float)sapp_height(); +} + +int sapp_sample_count(void) { + return _sapp_tc.app.desc.sample_count > 0 ? _sapp_tc.app.desc.sample_count : 1; +} + +bool sapp_high_dpi(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return _sapp_tc.app.desc.high_dpi && w && (w->dpi_scale >= 1.5f); +} + +float sapp_dpi_scale(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return w ? w->dpi_scale : 1.0f; +} + +uint64_t sapp_frame_count(void) { + return _sapp_tc.app.frame_count; +} + +double sapp_frame_duration(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w) return 1.0 / 60.0; + return (w->timing.smooth_dt > 0.0) ? w->timing.smooth_dt : w->refresh_period; +} + +void sapp_request_quit(void) { + _sapp_tc.app.quit_requested = true; +} + +void sapp_cancel_quit(void) { + _sapp_tc.app.quit_requested = false; +} + +void sapp_quit(void) { + _sapp_tc.app.quit_ordered = true; +} + +void sapp_consume_event(void) { + _sapp_tc.app.event_consumed = true; +} + +void sapp_skip_present(void) { + /* one-shot; HONORED on D3D11 (TrussC patch: event-driven present + suppression -- keeps the last image on screen, prevents flicker) */ + _sapp_tc.app.skip_present = true; +} + +void sapp_show_keyboard(bool show) { + (void)show; /* on-screen keyboard: mobile only */ +} + +bool sapp_keyboard_shown(void) { + return false; +} + +void sapp_show_mouse(bool show) { + if (_sapp_tc.app.mouse_shown != show) { + _sapp_tc_win32_apply_cursor(_sapp_tc.app.current_cursor, show, false); + } +} + +bool sapp_mouse_shown(void) { + return _sapp_tc.app.mouse_shown; +} + +void sapp_set_mouse_cursor(sapp_mouse_cursor cursor) { + if ((int)cursor < 0 || (int)cursor >= _SAPP_MOUSECURSOR_NUM) return; + if (cursor != _sapp_tc.app.current_cursor) { + _sapp_tc_win32_apply_cursor(cursor, _sapp_tc.app.mouse_shown, false); + } +} + +sapp_mouse_cursor sapp_get_mouse_cursor(void) { + return _sapp_tc.app.current_cursor; +} + +sapp_mouse_cursor sapp_bind_mouse_cursor_image(sapp_mouse_cursor cursor, const sapp_image_desc* desc) { + if ((int)cursor < 0 || (int)cursor >= _SAPP_MOUSECURSOR_NUM) return cursor; + if (!desc || !desc->pixels.ptr || desc->width <= 0 || desc->height <= 0) return cursor; + if (desc->pixels.size < (size_t)(desc->width * desc->height * 4)) return cursor; + sapp_unbind_mouse_cursor_image(cursor); + HCURSOR c = _sapp_tc_win32_create_cursor(desc); + if (!c) return cursor; + _sapp_tc.app.custom_cursors[cursor] = c; + _sapp_tc.app.custom_cursor_bound[cursor] = true; + if (_sapp_tc.app.current_cursor == cursor) { + _sapp_tc_win32_apply_cursor(cursor, _sapp_tc.app.mouse_shown, false); + } + return cursor; +} + +void sapp_unbind_mouse_cursor_image(sapp_mouse_cursor cursor) { + if ((int)cursor < 0 || (int)cursor >= _SAPP_MOUSECURSOR_NUM) return; + if (!_sapp_tc.app.custom_cursor_bound[cursor]) return; + _sapp_tc.app.custom_cursor_bound[cursor] = false; + HCURSOR c = _sapp_tc.app.custom_cursors[cursor]; + _sapp_tc.app.custom_cursors[cursor] = NULL; + if (_sapp_tc.app.current_cursor == cursor) { + _sapp_tc_win32_apply_cursor(cursor, _sapp_tc.app.mouse_shown, false); + } + if (c) DestroyIcon((HICON)c); +} + +void sapp_set_clipboard_string(const char* str) { + if (!_sapp_tc.app.clipboard_enabled || !_sapp_tc.app.clipboard || !str) return; + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w) return; + const SIZE_T wchar_buf_size = (SIZE_T)_sapp_tc.app.clipboard_size * sizeof(wchar_t); + HANDLE object = GlobalAlloc(GMEM_MOVEABLE, wchar_buf_size); + if (!object) return; + wchar_t* wchar_buf = (wchar_t*)GlobalLock(object); + if (!wchar_buf) { + GlobalFree(object); + return; + } + if (!_sapp_tc_win32_utf8_to_wide(str, wchar_buf, (int)(wchar_buf_size / sizeof(wchar_t)))) { + GlobalUnlock(object); + GlobalFree(object); + return; + } + GlobalUnlock(object); + bool owned = false; + if (OpenClipboard(w->hwnd)) { + EmptyClipboard(); + /* the clipboard takes ownership of the global object on success */ + owned = (NULL != SetClipboardData(CF_UNICODETEXT, object)); + CloseClipboard(); + } + if (!owned) { + GlobalFree(object); + return; + } + strncpy(_sapp_tc.app.clipboard, str, (size_t)_sapp_tc.app.clipboard_size - 1); + _sapp_tc.app.clipboard[_sapp_tc.app.clipboard_size - 1] = 0; +} + +const char* sapp_get_clipboard_string(void) { + if (!_sapp_tc.app.clipboard_enabled || !_sapp_tc.app.clipboard) return ""; + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w) return ""; + _sapp_tc.app.clipboard[0] = 0; + if (!OpenClipboard(w->hwnd)) { + return _sapp_tc.app.clipboard; + } + HANDLE object = GetClipboardData(CF_UNICODETEXT); + if (object) { + const wchar_t* wchar_buf = (const wchar_t*)GlobalLock(object); + if (wchar_buf) { + _sapp_tc_win32_wide_to_utf8(wchar_buf, _sapp_tc.app.clipboard, + _sapp_tc.app.clipboard_size); + GlobalUnlock(object); + } + } + CloseClipboard(); + return _sapp_tc.app.clipboard; +} + +void sapp_set_window_title(const char* str) { + if (!str) return; + strncpy(_sapp_tc.app.window_title, str, sizeof(_sapp_tc.app.window_title) - 1); + _sapp_tc_win32_utf8_to_wide(_sapp_tc.app.window_title, + _sapp_tc.app.window_title_wide, + (int)(sizeof(_sapp_tc.app.window_title_wide) / sizeof(WCHAR))); + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (w && w->hwnd) { + SetWindowTextW(w->hwnd, _sapp_tc.app.window_title_wide); + } +} + +bool sapp_is_fullscreen(void) { + return _sapp_tc.app.fullscreen; +} + +void sapp_toggle_fullscreen(void) { + if (!_sapp_tc.app.main) return; + _sapp_tc_win32_set_fullscreen(!_sapp_tc.app.fullscreen, SWP_SHOWWINDOW); +} + +int sapp_get_num_dropped_files(void) { + return _sapp_tc.app.drop_enabled ? _sapp_tc.app.drop_num_files : 0; +} + +const char* sapp_get_dropped_file_path(int index) { + if (!_sapp_tc.app.drop_enabled || !_sapp_tc.app.drop_buffer) return ""; + if (index < 0 || index >= _sapp_tc.app.drop_num_files) return ""; + return _sapp_tc.app.drop_buffer + (size_t)index * (size_t)_sapp_tc.app.drop_max_path_length; +} + +sapp_pixel_format sapp_color_format(void) { + return SAPP_PIXELFORMAT_RGB10A2; /* TrussC 10-bit output on D3D11 */ +} + +sapp_pixel_format sapp_depth_format(void) { + return SAPP_PIXELFORMAT_DEPTH_STENCIL; +} + +const void* sapp_win32_get_hwnd(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return w ? (const void*)w->hwnd : 0; +} + +const void* sapp_d3d11_get_device(void) { + return (const void*)_sapp_tc.app.device; +} + +const void* sapp_d3d11_get_device_context(void) { + return (const void*)_sapp_tc.app.device_context; +} + +const void* sapp_d3d11_get_swap_chain(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return w ? (const void*)w->swap_chain : 0; +} + +sapp_environment sapp_get_environment(void) { + sapp_environment env; + memset(&env, 0, sizeof(env)); + env.defaults.color_format = sapp_color_format(); + env.defaults.depth_format = sapp_depth_format(); + env.defaults.sample_count = sapp_sample_count(); + env.d3d11.device = (const void*)_sapp_tc.app.device; + env.d3d11.device_context = (const void*)_sapp_tc.app.device_context; + return env; +} + +sapp_swapchain sapp_get_swapchain(void) { + /* unlike Metal there is no per-frame acquire: the flip-model swapchain + rotates internally, the views are created once and reused (resize + recreates them) -- calling this any number of times per frame is safe */ + sapp_swapchain sc; + memset(&sc, 0, sizeof(sc)); + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w) return sc; + sc.width = sapp_width(); + sc.height = sapp_height(); + sc.sample_count = sapp_sample_count(); + sc.color_format = sapp_color_format(); + sc.depth_format = sapp_depth_format(); + if (w->desc.sample_count > 1) { + sc.d3d11.render_view = (const void*)w->msaa_rtv; + sc.d3d11.resolve_view = (const void*)w->rtv; + } else { + sc.d3d11.render_view = (const void*)w->rtv; + sc.d3d11.resolve_view = 0; + } + sc.d3d11.depth_stencil_view = (const void*)w->dsv; + return sc; +} + +} /* extern "C" */ + +#elif defined(__linux__) && !defined(__ANDROID__) && !defined(__EMSCRIPTEN__) +#if defined(SOKOL_GLCORE) +/*== Linux (X11 + GLX, desktop GL core) ===================================== + Implements the public sokol_app.h API on Linux plus the multi-window API. + Structure mirrors sokol_app.h's X11/GLX backend (quit dance on + WM_DELETE_WINDOW, per-frame size polling instead of ConfigureNotify, XKB + physical-key-name keytable + XkbSetDetectableAutoRepeat software repeat, + XLookupString + keysym table CHAR events, CLIPBOARD selection, XDND v5, + EWMH fullscreen, Xcursor cursors, TrussC skip_present patch). + + Frame pacing: ONE GLXContext is shared by all windows, each window has its + own GLXWindow drawable. The MAIN window keeps upstream's model: the run + loop blocks inside its vsync'd glXSwapBuffers (swap interval 1 via + GLX_EXT_swap_control on its drawable) -- that blocking swap IS the pacing. + SECONDARY windows swap with interval 0 (never block; vsyncing two or more + drawables would serialize the loop to refresh/N) and are timer-paced to a + refresh-period estimate measured from the main window's presents. If the + main swap stops blocking (iconified, skip_present, no usable swap-control + extension), the tick self-heals to timer pacing, so the loop can never + busy-spin. + + Unlike upstream, GLX entry points are called directly (TrussC's Linux + build always links libGL for sokol_gfx's GLCORE backend, so the dlopen + dance is unnecessary); only extension functions go through + glXGetProcAddressARB. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +/* GL_GLEXT_PROTOTYPES must be defined before the FIRST include of + in the TU: glext.h is include-guarded, so sokol_gfx.h's own define comes + too late once this header has pulled the GL headers in (upstream + sokol_app.h's Linux impl defines it the same way) */ +#ifndef GL_GLEXT_PROTOTYPES +#define GL_GLEXT_PROTOTYPES +#endif +#include +#include +#include +#include +#include +#include + +#if defined(SOKOL_APP_IMPL_INCLUDED) +#error "sokol_app_tc.h owns the Linux implementation of the sapp_* API; include sokol_app.h WITHOUT an implementation define in this TU" +#endif + +#ifndef SOKOL_ASSERT +#include +#define SOKOL_ASSERT(c) assert(c) +#endif + +/* GLX extension constants (present in on any current system; + defined here so an exotic glx.h cannot break the build) */ +#ifndef GLX_CONTEXT_MAJOR_VERSION_ARB +#define GLX_CONTEXT_MAJOR_VERSION_ARB 0x2091 +#endif +#ifndef GLX_CONTEXT_MINOR_VERSION_ARB +#define GLX_CONTEXT_MINOR_VERSION_ARB 0x2092 +#endif +#ifndef GLX_CONTEXT_PROFILE_MASK_ARB +#define GLX_CONTEXT_PROFILE_MASK_ARB 0x9126 +#endif +#ifndef GLX_CONTEXT_CORE_PROFILE_BIT_ARB +#define GLX_CONTEXT_CORE_PROFILE_BIT_ARB 0x00000001 +#endif +#ifndef GLX_CONTEXT_FLAGS_ARB +#define GLX_CONTEXT_FLAGS_ARB 0x2094 +#endif +#ifndef GLX_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB +#define GLX_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB 0x00000002 +#endif +#ifndef GLX_SAMPLES +#define GLX_SAMPLES 0x186A1 +#endif + +#define _SAPP_TC_MAX_WINDOWS (32) +#define _SAPP_X11_MAX_X11_KEYCODES (256) +#define _SAPP_TC_XDND_VERSION (5) + +/* EMA-filtered frame timer, port of sokol_app.h's _sapp_timing_* (X11 has no + display-link timestamps, so this measured filter is the only dt source) */ +typedef struct { + double last; /* previous sample time, 0 = none yet */ + double ema; + double smooth_dt; + double dt; /* last raw (clamped) delta */ +} _sapp_tc_timing_t; + +typedef struct _sapp_tc_window_t { + uint32_t win_id; + sapp_window_desc desc; + bool is_main; /* window #0: created by sapp_run, drives the app callbacks */ + bool ready; /* creation finished; event dispatch may deliver events */ + Window xwin; /* the X11 window XID */ + Colormap colormap; /* per-window (created against the shared visual) */ + GLXWindow glx_win; /* the GLX drawable wrapping xwin */ + int fb_width, fb_height; /* framebuffer pixels (== X11 window pixels) */ + int win_width, win_height; /* logical points */ + float dpi_scale; /* framebuffer px per point (Xft.dpi / 96) */ + double earliest_next; /* CLOCK_MONOTONIC seconds; window is not due before this */ + double last_present; /* time of the previous real present (main pacing probe) */ + double frame_duration; + _sapp_tc_timing_t timing; + bool iconified; /* WM_STATE == IconicState */ + bool occluded; /* VisibilityFullyObscured (secondary windows only) */ + bool in_tick; + bool mouse_pos_valid; + float mouse_x, mouse_y; /* last position in event coordinates (raw px) */ + float mouse_dx, mouse_dy; + uint8_t mouse_buttons; /* held-button bitmask (enter/leave suppression) */ + bool key_repeat[_SAPP_X11_MAX_X11_KEYCODES]; /* software repeat tracking */ +} _sapp_tc_window_t; + +static struct { + bool keytable_valid; + sapp_keycode keycodes[SAPP_MAX_KEYCODES]; + uint32_t next_id; + _sapp_tc_window_t* windows[_SAPP_TC_MAX_WINDOWS]; + + /* ---- X11 process-globals (one server connection for all windows) ----- */ + Display* display; + int screen; + Window root; + float dpi; /* Xft.dpi, 96.0 fallback */ + unsigned char error_code; /* set by the temporary X error handler */ + XErrorHandler prev_error_handler; + Atom UTF8_STRING; + Atom CLIPBOARD; + Atom TARGETS; + Atom WM_PROTOCOLS; + Atom WM_DELETE_WINDOW; + Atom WM_STATE; + Atom NET_WM_NAME; + Atom NET_WM_ICON_NAME; + Atom NET_WM_STATE; + Atom NET_WM_STATE_FULLSCREEN; + Atom MOTIF_WM_HINTS; + Atom SAPP_TC_SELECTION; /* scratch property for clipboard transfers */ + struct { + int version; + Window source; + Atom format; + _sapp_tc_window_t* over; /* which of OUR windows the drag targets */ + Atom XdndAware, XdndEnter, XdndPosition, XdndStatus, XdndActionCopy, + XdndDrop, XdndFinished, XdndSelection, XdndTypeList, text_uri_list; + } xdnd; + Cursor hidden_cursor; + Cursor standard_cursors[_SAPP_MOUSECURSOR_NUM]; + Cursor custom_cursors[_SAPP_MOUSECURSOR_NUM]; + + /* ---- GLX (one shared context; per-window GLXWindow drawables) -------- */ + GLXFBConfig fbconfig; + GLXContext ctx; + uint32_t gl_framebuffer; /* default-FBO id captured after make-current */ + void (*SwapIntervalEXT)(Display*, GLXDrawable, int); /* per-drawable */ + int (*SwapIntervalMESA)(int); /* context-wide */ + GLXContext (*CreateContextAttribsARB)(Display*, GLXFBConfig, GLXContext, Bool, const int*); + double refresh_period; /* estimate from the main window's vsync'd presents */ + + /* ---- app / main-window state (this header owns sapp_run on Linux) ---- */ + struct { + sapp_desc desc; /* sanitized copy; window_title points at the buffer below */ + char window_title[256]; + bool valid; + bool init_called; + bool cleanup_called; + bool quit_requested; + bool quit_ordered; + bool fullscreen; + bool event_consumed; + bool skip_present; /* one-shot; honored on glXSwapBuffers (TrussC flicker patch) */ + uint64_t frame_count; + _sapp_tc_window_t* main; + /* mouse cursor */ + bool mouse_shown; + sapp_mouse_cursor current_cursor; + bool custom_cursor_bound[_SAPP_MOUSECURSOR_NUM]; + /* clipboard */ + bool clipboard_enabled; + int clipboard_size; + char* clipboard; + /* drag & drop */ + bool drop_enabled; + int drop_max_files; + int drop_max_path_length; + int drop_num_files; + char* drop_buffer; + } app; +} _sapp_tc; + +static void _sapp_tc_x11_tick(_sapp_tc_window_t* w); +static bool _sapp_tc_x11_update_dimensions(_sapp_tc_window_t* w); +static void _sapp_tc_x11_apply_cursor(void); + +/*-- timing -----------------------------------------------------------------*/ +static double _sapp_tc_now(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec * 1.0e-9; +} + +static void _sapp_tc_timing_reset(_sapp_tc_timing_t* t) { + t->last = 0.0; + t->ema = t->smooth_dt = t->dt = 1.0 / 60.0; +} + +static double _sapp_tc_timing_clamp(double dt) { + if (dt < 0.000001) return 0.000001; + if (dt > 0.1) return 0.1; + return dt; +} + +static void _sapp_tc_timing_update(_sapp_tc_timing_t* t) { + const double now = _sapp_tc_now(); + if (t->last > 0.0) { + double dt = _sapp_tc_timing_clamp(now - t->last); + t->dt = dt; + /* big jump: reset the filter; otherwise EMA-smooth */ + if (fabs(dt - t->smooth_dt) > 0.004) { + t->ema = t->smooth_dt = dt; + } else { + t->ema += 0.025 * (dt - t->ema); + t->smooth_dt = _sapp_tc_timing_clamp(t->ema); + } + } + t->last = now; +} + +/*-- window lookup ----------------------------------------------------------*/ +static _sapp_tc_window_t* _sapp_tc_lookup(sapp_window win) { + if (win.id == 0) return 0; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* w = _sapp_tc.windows[i]; + if (w && w->win_id == win.id) return w; + } + return 0; +} + +static _sapp_tc_window_t* _sapp_tc_lookup_xwin(Window xwin) { + if (!xwin) return 0; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* w = _sapp_tc.windows[i]; + if (w && w->ready && w->xwin == xwin) return w; + } + return 0; +} + +/*-- event plumbing ---------------------------------------------------------*/ +static void _sapp_tc_send(_sapp_tc_window_t* w, sapp_event* ev) { + if (!w->desc.event_cb) return; + ev->frame_count = _sapp_tc.app.frame_count; + ev->window_width = w->win_width; + ev->window_height = w->win_height; + ev->framebuffer_width = w->fb_width; + ev->framebuffer_height = w->fb_height; + sapp_window handle = { w->win_id }; + w->desc.event_cb(ev, handle, w->desc.user_data); +} + +/*-- main window: event routing to the sapp_desc callbacks ------------------*/ +static bool _sapp_tc_app_events_enabled(void) { + /* same gate as sokol_app.h: nothing fires before the first tick's init_cb */ + return _sapp_tc.app.init_called && + (_sapp_tc.app.desc.event_cb || _sapp_tc.app.desc.event_userdata_cb); +} + +static void _sapp_tc_main_event_tramp(const sapp_event* ev, sapp_window win, void* user) { + (void)win; (void)user; + if (!_sapp_tc_app_events_enabled()) return; + _sapp_tc.app.event_consumed = false; + if (_sapp_tc.app.desc.event_cb) { + _sapp_tc.app.desc.event_cb(ev); + } else { + _sapp_tc.app.desc.event_userdata_cb(ev, _sapp_tc.app.desc.user_data); + } +} + +/* see GLFW's xkb_unicode.c */ +static const struct _sapp_tc_x11_codepair { + uint16_t keysym; + uint16_t ucs; +} _sapp_tc_x11_keysymtab[] = { + { 0x01a1, 0x0104 }, + { 0x01a2, 0x02d8 }, + { 0x01a3, 0x0141 }, + { 0x01a5, 0x013d }, + { 0x01a6, 0x015a }, + { 0x01a9, 0x0160 }, + { 0x01aa, 0x015e }, + { 0x01ab, 0x0164 }, + { 0x01ac, 0x0179 }, + { 0x01ae, 0x017d }, + { 0x01af, 0x017b }, + { 0x01b1, 0x0105 }, + { 0x01b2, 0x02db }, + { 0x01b3, 0x0142 }, + { 0x01b5, 0x013e }, + { 0x01b6, 0x015b }, + { 0x01b7, 0x02c7 }, + { 0x01b9, 0x0161 }, + { 0x01ba, 0x015f }, + { 0x01bb, 0x0165 }, + { 0x01bc, 0x017a }, + { 0x01bd, 0x02dd }, + { 0x01be, 0x017e }, + { 0x01bf, 0x017c }, + { 0x01c0, 0x0154 }, + { 0x01c3, 0x0102 }, + { 0x01c5, 0x0139 }, + { 0x01c6, 0x0106 }, + { 0x01c8, 0x010c }, + { 0x01ca, 0x0118 }, + { 0x01cc, 0x011a }, + { 0x01cf, 0x010e }, + { 0x01d0, 0x0110 }, + { 0x01d1, 0x0143 }, + { 0x01d2, 0x0147 }, + { 0x01d5, 0x0150 }, + { 0x01d8, 0x0158 }, + { 0x01d9, 0x016e }, + { 0x01db, 0x0170 }, + { 0x01de, 0x0162 }, + { 0x01e0, 0x0155 }, + { 0x01e3, 0x0103 }, + { 0x01e5, 0x013a }, + { 0x01e6, 0x0107 }, + { 0x01e8, 0x010d }, + { 0x01ea, 0x0119 }, + { 0x01ec, 0x011b }, + { 0x01ef, 0x010f }, + { 0x01f0, 0x0111 }, + { 0x01f1, 0x0144 }, + { 0x01f2, 0x0148 }, + { 0x01f5, 0x0151 }, + { 0x01f8, 0x0159 }, + { 0x01f9, 0x016f }, + { 0x01fb, 0x0171 }, + { 0x01fe, 0x0163 }, + { 0x01ff, 0x02d9 }, + { 0x02a1, 0x0126 }, + { 0x02a6, 0x0124 }, + { 0x02a9, 0x0130 }, + { 0x02ab, 0x011e }, + { 0x02ac, 0x0134 }, + { 0x02b1, 0x0127 }, + { 0x02b6, 0x0125 }, + { 0x02b9, 0x0131 }, + { 0x02bb, 0x011f }, + { 0x02bc, 0x0135 }, + { 0x02c5, 0x010a }, + { 0x02c6, 0x0108 }, + { 0x02d5, 0x0120 }, + { 0x02d8, 0x011c }, + { 0x02dd, 0x016c }, + { 0x02de, 0x015c }, + { 0x02e5, 0x010b }, + { 0x02e6, 0x0109 }, + { 0x02f5, 0x0121 }, + { 0x02f8, 0x011d }, + { 0x02fd, 0x016d }, + { 0x02fe, 0x015d }, + { 0x03a2, 0x0138 }, + { 0x03a3, 0x0156 }, + { 0x03a5, 0x0128 }, + { 0x03a6, 0x013b }, + { 0x03aa, 0x0112 }, + { 0x03ab, 0x0122 }, + { 0x03ac, 0x0166 }, + { 0x03b3, 0x0157 }, + { 0x03b5, 0x0129 }, + { 0x03b6, 0x013c }, + { 0x03ba, 0x0113 }, + { 0x03bb, 0x0123 }, + { 0x03bc, 0x0167 }, + { 0x03bd, 0x014a }, + { 0x03bf, 0x014b }, + { 0x03c0, 0x0100 }, + { 0x03c7, 0x012e }, + { 0x03cc, 0x0116 }, + { 0x03cf, 0x012a }, + { 0x03d1, 0x0145 }, + { 0x03d2, 0x014c }, + { 0x03d3, 0x0136 }, + { 0x03d9, 0x0172 }, + { 0x03dd, 0x0168 }, + { 0x03de, 0x016a }, + { 0x03e0, 0x0101 }, + { 0x03e7, 0x012f }, + { 0x03ec, 0x0117 }, + { 0x03ef, 0x012b }, + { 0x03f1, 0x0146 }, + { 0x03f2, 0x014d }, + { 0x03f3, 0x0137 }, + { 0x03f9, 0x0173 }, + { 0x03fd, 0x0169 }, + { 0x03fe, 0x016b }, + { 0x047e, 0x203e }, + { 0x04a1, 0x3002 }, + { 0x04a2, 0x300c }, + { 0x04a3, 0x300d }, + { 0x04a4, 0x3001 }, + { 0x04a5, 0x30fb }, + { 0x04a6, 0x30f2 }, + { 0x04a7, 0x30a1 }, + { 0x04a8, 0x30a3 }, + { 0x04a9, 0x30a5 }, + { 0x04aa, 0x30a7 }, + { 0x04ab, 0x30a9 }, + { 0x04ac, 0x30e3 }, + { 0x04ad, 0x30e5 }, + { 0x04ae, 0x30e7 }, + { 0x04af, 0x30c3 }, + { 0x04b0, 0x30fc }, + { 0x04b1, 0x30a2 }, + { 0x04b2, 0x30a4 }, + { 0x04b3, 0x30a6 }, + { 0x04b4, 0x30a8 }, + { 0x04b5, 0x30aa }, + { 0x04b6, 0x30ab }, + { 0x04b7, 0x30ad }, + { 0x04b8, 0x30af }, + { 0x04b9, 0x30b1 }, + { 0x04ba, 0x30b3 }, + { 0x04bb, 0x30b5 }, + { 0x04bc, 0x30b7 }, + { 0x04bd, 0x30b9 }, + { 0x04be, 0x30bb }, + { 0x04bf, 0x30bd }, + { 0x04c0, 0x30bf }, + { 0x04c1, 0x30c1 }, + { 0x04c2, 0x30c4 }, + { 0x04c3, 0x30c6 }, + { 0x04c4, 0x30c8 }, + { 0x04c5, 0x30ca }, + { 0x04c6, 0x30cb }, + { 0x04c7, 0x30cc }, + { 0x04c8, 0x30cd }, + { 0x04c9, 0x30ce }, + { 0x04ca, 0x30cf }, + { 0x04cb, 0x30d2 }, + { 0x04cc, 0x30d5 }, + { 0x04cd, 0x30d8 }, + { 0x04ce, 0x30db }, + { 0x04cf, 0x30de }, + { 0x04d0, 0x30df }, + { 0x04d1, 0x30e0 }, + { 0x04d2, 0x30e1 }, + { 0x04d3, 0x30e2 }, + { 0x04d4, 0x30e4 }, + { 0x04d5, 0x30e6 }, + { 0x04d6, 0x30e8 }, + { 0x04d7, 0x30e9 }, + { 0x04d8, 0x30ea }, + { 0x04d9, 0x30eb }, + { 0x04da, 0x30ec }, + { 0x04db, 0x30ed }, + { 0x04dc, 0x30ef }, + { 0x04dd, 0x30f3 }, + { 0x04de, 0x309b }, + { 0x04df, 0x309c }, + { 0x05ac, 0x060c }, + { 0x05bb, 0x061b }, + { 0x05bf, 0x061f }, + { 0x05c1, 0x0621 }, + { 0x05c2, 0x0622 }, + { 0x05c3, 0x0623 }, + { 0x05c4, 0x0624 }, + { 0x05c5, 0x0625 }, + { 0x05c6, 0x0626 }, + { 0x05c7, 0x0627 }, + { 0x05c8, 0x0628 }, + { 0x05c9, 0x0629 }, + { 0x05ca, 0x062a }, + { 0x05cb, 0x062b }, + { 0x05cc, 0x062c }, + { 0x05cd, 0x062d }, + { 0x05ce, 0x062e }, + { 0x05cf, 0x062f }, + { 0x05d0, 0x0630 }, + { 0x05d1, 0x0631 }, + { 0x05d2, 0x0632 }, + { 0x05d3, 0x0633 }, + { 0x05d4, 0x0634 }, + { 0x05d5, 0x0635 }, + { 0x05d6, 0x0636 }, + { 0x05d7, 0x0637 }, + { 0x05d8, 0x0638 }, + { 0x05d9, 0x0639 }, + { 0x05da, 0x063a }, + { 0x05e0, 0x0640 }, + { 0x05e1, 0x0641 }, + { 0x05e2, 0x0642 }, + { 0x05e3, 0x0643 }, + { 0x05e4, 0x0644 }, + { 0x05e5, 0x0645 }, + { 0x05e6, 0x0646 }, + { 0x05e7, 0x0647 }, + { 0x05e8, 0x0648 }, + { 0x05e9, 0x0649 }, + { 0x05ea, 0x064a }, + { 0x05eb, 0x064b }, + { 0x05ec, 0x064c }, + { 0x05ed, 0x064d }, + { 0x05ee, 0x064e }, + { 0x05ef, 0x064f }, + { 0x05f0, 0x0650 }, + { 0x05f1, 0x0651 }, + { 0x05f2, 0x0652 }, + { 0x06a1, 0x0452 }, + { 0x06a2, 0x0453 }, + { 0x06a3, 0x0451 }, + { 0x06a4, 0x0454 }, + { 0x06a5, 0x0455 }, + { 0x06a6, 0x0456 }, + { 0x06a7, 0x0457 }, + { 0x06a8, 0x0458 }, + { 0x06a9, 0x0459 }, + { 0x06aa, 0x045a }, + { 0x06ab, 0x045b }, + { 0x06ac, 0x045c }, + { 0x06ae, 0x045e }, + { 0x06af, 0x045f }, + { 0x06b0, 0x2116 }, + { 0x06b1, 0x0402 }, + { 0x06b2, 0x0403 }, + { 0x06b3, 0x0401 }, + { 0x06b4, 0x0404 }, + { 0x06b5, 0x0405 }, + { 0x06b6, 0x0406 }, + { 0x06b7, 0x0407 }, + { 0x06b8, 0x0408 }, + { 0x06b9, 0x0409 }, + { 0x06ba, 0x040a }, + { 0x06bb, 0x040b }, + { 0x06bc, 0x040c }, + { 0x06be, 0x040e }, + { 0x06bf, 0x040f }, + { 0x06c0, 0x044e }, + { 0x06c1, 0x0430 }, + { 0x06c2, 0x0431 }, + { 0x06c3, 0x0446 }, + { 0x06c4, 0x0434 }, + { 0x06c5, 0x0435 }, + { 0x06c6, 0x0444 }, + { 0x06c7, 0x0433 }, + { 0x06c8, 0x0445 }, + { 0x06c9, 0x0438 }, + { 0x06ca, 0x0439 }, + { 0x06cb, 0x043a }, + { 0x06cc, 0x043b }, + { 0x06cd, 0x043c }, + { 0x06ce, 0x043d }, + { 0x06cf, 0x043e }, + { 0x06d0, 0x043f }, + { 0x06d1, 0x044f }, + { 0x06d2, 0x0440 }, + { 0x06d3, 0x0441 }, + { 0x06d4, 0x0442 }, + { 0x06d5, 0x0443 }, + { 0x06d6, 0x0436 }, + { 0x06d7, 0x0432 }, + { 0x06d8, 0x044c }, + { 0x06d9, 0x044b }, + { 0x06da, 0x0437 }, + { 0x06db, 0x0448 }, + { 0x06dc, 0x044d }, + { 0x06dd, 0x0449 }, + { 0x06de, 0x0447 }, + { 0x06df, 0x044a }, + { 0x06e0, 0x042e }, + { 0x06e1, 0x0410 }, + { 0x06e2, 0x0411 }, + { 0x06e3, 0x0426 }, + { 0x06e4, 0x0414 }, + { 0x06e5, 0x0415 }, + { 0x06e6, 0x0424 }, + { 0x06e7, 0x0413 }, + { 0x06e8, 0x0425 }, + { 0x06e9, 0x0418 }, + { 0x06ea, 0x0419 }, + { 0x06eb, 0x041a }, + { 0x06ec, 0x041b }, + { 0x06ed, 0x041c }, + { 0x06ee, 0x041d }, + { 0x06ef, 0x041e }, + { 0x06f0, 0x041f }, + { 0x06f1, 0x042f }, + { 0x06f2, 0x0420 }, + { 0x06f3, 0x0421 }, + { 0x06f4, 0x0422 }, + { 0x06f5, 0x0423 }, + { 0x06f6, 0x0416 }, + { 0x06f7, 0x0412 }, + { 0x06f8, 0x042c }, + { 0x06f9, 0x042b }, + { 0x06fa, 0x0417 }, + { 0x06fb, 0x0428 }, + { 0x06fc, 0x042d }, + { 0x06fd, 0x0429 }, + { 0x06fe, 0x0427 }, + { 0x06ff, 0x042a }, + { 0x07a1, 0x0386 }, + { 0x07a2, 0x0388 }, + { 0x07a3, 0x0389 }, + { 0x07a4, 0x038a }, + { 0x07a5, 0x03aa }, + { 0x07a7, 0x038c }, + { 0x07a8, 0x038e }, + { 0x07a9, 0x03ab }, + { 0x07ab, 0x038f }, + { 0x07ae, 0x0385 }, + { 0x07af, 0x2015 }, + { 0x07b1, 0x03ac }, + { 0x07b2, 0x03ad }, + { 0x07b3, 0x03ae }, + { 0x07b4, 0x03af }, + { 0x07b5, 0x03ca }, + { 0x07b6, 0x0390 }, + { 0x07b7, 0x03cc }, + { 0x07b8, 0x03cd }, + { 0x07b9, 0x03cb }, + { 0x07ba, 0x03b0 }, + { 0x07bb, 0x03ce }, + { 0x07c1, 0x0391 }, + { 0x07c2, 0x0392 }, + { 0x07c3, 0x0393 }, + { 0x07c4, 0x0394 }, + { 0x07c5, 0x0395 }, + { 0x07c6, 0x0396 }, + { 0x07c7, 0x0397 }, + { 0x07c8, 0x0398 }, + { 0x07c9, 0x0399 }, + { 0x07ca, 0x039a }, + { 0x07cb, 0x039b }, + { 0x07cc, 0x039c }, + { 0x07cd, 0x039d }, + { 0x07ce, 0x039e }, + { 0x07cf, 0x039f }, + { 0x07d0, 0x03a0 }, + { 0x07d1, 0x03a1 }, + { 0x07d2, 0x03a3 }, + { 0x07d4, 0x03a4 }, + { 0x07d5, 0x03a5 }, + { 0x07d6, 0x03a6 }, + { 0x07d7, 0x03a7 }, + { 0x07d8, 0x03a8 }, + { 0x07d9, 0x03a9 }, + { 0x07e1, 0x03b1 }, + { 0x07e2, 0x03b2 }, + { 0x07e3, 0x03b3 }, + { 0x07e4, 0x03b4 }, + { 0x07e5, 0x03b5 }, + { 0x07e6, 0x03b6 }, + { 0x07e7, 0x03b7 }, + { 0x07e8, 0x03b8 }, + { 0x07e9, 0x03b9 }, + { 0x07ea, 0x03ba }, + { 0x07eb, 0x03bb }, + { 0x07ec, 0x03bc }, + { 0x07ed, 0x03bd }, + { 0x07ee, 0x03be }, + { 0x07ef, 0x03bf }, + { 0x07f0, 0x03c0 }, + { 0x07f1, 0x03c1 }, + { 0x07f2, 0x03c3 }, + { 0x07f3, 0x03c2 }, + { 0x07f4, 0x03c4 }, + { 0x07f5, 0x03c5 }, + { 0x07f6, 0x03c6 }, + { 0x07f7, 0x03c7 }, + { 0x07f8, 0x03c8 }, + { 0x07f9, 0x03c9 }, + { 0x08a1, 0x23b7 }, + { 0x08a2, 0x250c }, + { 0x08a3, 0x2500 }, + { 0x08a4, 0x2320 }, + { 0x08a5, 0x2321 }, + { 0x08a6, 0x2502 }, + { 0x08a7, 0x23a1 }, + { 0x08a8, 0x23a3 }, + { 0x08a9, 0x23a4 }, + { 0x08aa, 0x23a6 }, + { 0x08ab, 0x239b }, + { 0x08ac, 0x239d }, + { 0x08ad, 0x239e }, + { 0x08ae, 0x23a0 }, + { 0x08af, 0x23a8 }, + { 0x08b0, 0x23ac }, + { 0x08bc, 0x2264 }, + { 0x08bd, 0x2260 }, + { 0x08be, 0x2265 }, + { 0x08bf, 0x222b }, + { 0x08c0, 0x2234 }, + { 0x08c1, 0x221d }, + { 0x08c2, 0x221e }, + { 0x08c5, 0x2207 }, + { 0x08c8, 0x223c }, + { 0x08c9, 0x2243 }, + { 0x08cd, 0x21d4 }, + { 0x08ce, 0x21d2 }, + { 0x08cf, 0x2261 }, + { 0x08d6, 0x221a }, + { 0x08da, 0x2282 }, + { 0x08db, 0x2283 }, + { 0x08dc, 0x2229 }, + { 0x08dd, 0x222a }, + { 0x08de, 0x2227 }, + { 0x08df, 0x2228 }, + { 0x08ef, 0x2202 }, + { 0x08f6, 0x0192 }, + { 0x08fb, 0x2190 }, + { 0x08fc, 0x2191 }, + { 0x08fd, 0x2192 }, + { 0x08fe, 0x2193 }, + { 0x09e0, 0x25c6 }, + { 0x09e1, 0x2592 }, + { 0x09e2, 0x2409 }, + { 0x09e3, 0x240c }, + { 0x09e4, 0x240d }, + { 0x09e5, 0x240a }, + { 0x09e8, 0x2424 }, + { 0x09e9, 0x240b }, + { 0x09ea, 0x2518 }, + { 0x09eb, 0x2510 }, + { 0x09ec, 0x250c }, + { 0x09ed, 0x2514 }, + { 0x09ee, 0x253c }, + { 0x09ef, 0x23ba }, + { 0x09f0, 0x23bb }, + { 0x09f1, 0x2500 }, + { 0x09f2, 0x23bc }, + { 0x09f3, 0x23bd }, + { 0x09f4, 0x251c }, + { 0x09f5, 0x2524 }, + { 0x09f6, 0x2534 }, + { 0x09f7, 0x252c }, + { 0x09f8, 0x2502 }, + { 0x0aa1, 0x2003 }, + { 0x0aa2, 0x2002 }, + { 0x0aa3, 0x2004 }, + { 0x0aa4, 0x2005 }, + { 0x0aa5, 0x2007 }, + { 0x0aa6, 0x2008 }, + { 0x0aa7, 0x2009 }, + { 0x0aa8, 0x200a }, + { 0x0aa9, 0x2014 }, + { 0x0aaa, 0x2013 }, + { 0x0aae, 0x2026 }, + { 0x0aaf, 0x2025 }, + { 0x0ab0, 0x2153 }, + { 0x0ab1, 0x2154 }, + { 0x0ab2, 0x2155 }, + { 0x0ab3, 0x2156 }, + { 0x0ab4, 0x2157 }, + { 0x0ab5, 0x2158 }, + { 0x0ab6, 0x2159 }, + { 0x0ab7, 0x215a }, + { 0x0ab8, 0x2105 }, + { 0x0abb, 0x2012 }, + { 0x0abc, 0x2329 }, + { 0x0abe, 0x232a }, + { 0x0ac3, 0x215b }, + { 0x0ac4, 0x215c }, + { 0x0ac5, 0x215d }, + { 0x0ac6, 0x215e }, + { 0x0ac9, 0x2122 }, + { 0x0aca, 0x2613 }, + { 0x0acc, 0x25c1 }, + { 0x0acd, 0x25b7 }, + { 0x0ace, 0x25cb }, + { 0x0acf, 0x25af }, + { 0x0ad0, 0x2018 }, + { 0x0ad1, 0x2019 }, + { 0x0ad2, 0x201c }, + { 0x0ad3, 0x201d }, + { 0x0ad4, 0x211e }, + { 0x0ad6, 0x2032 }, + { 0x0ad7, 0x2033 }, + { 0x0ad9, 0x271d }, + { 0x0adb, 0x25ac }, + { 0x0adc, 0x25c0 }, + { 0x0add, 0x25b6 }, + { 0x0ade, 0x25cf }, + { 0x0adf, 0x25ae }, + { 0x0ae0, 0x25e6 }, + { 0x0ae1, 0x25ab }, + { 0x0ae2, 0x25ad }, + { 0x0ae3, 0x25b3 }, + { 0x0ae4, 0x25bd }, + { 0x0ae5, 0x2606 }, + { 0x0ae6, 0x2022 }, + { 0x0ae7, 0x25aa }, + { 0x0ae8, 0x25b2 }, + { 0x0ae9, 0x25bc }, + { 0x0aea, 0x261c }, + { 0x0aeb, 0x261e }, + { 0x0aec, 0x2663 }, + { 0x0aed, 0x2666 }, + { 0x0aee, 0x2665 }, + { 0x0af0, 0x2720 }, + { 0x0af1, 0x2020 }, + { 0x0af2, 0x2021 }, + { 0x0af3, 0x2713 }, + { 0x0af4, 0x2717 }, + { 0x0af5, 0x266f }, + { 0x0af6, 0x266d }, + { 0x0af7, 0x2642 }, + { 0x0af8, 0x2640 }, + { 0x0af9, 0x260e }, + { 0x0afa, 0x2315 }, + { 0x0afb, 0x2117 }, + { 0x0afc, 0x2038 }, + { 0x0afd, 0x201a }, + { 0x0afe, 0x201e }, + { 0x0ba3, 0x003c }, + { 0x0ba6, 0x003e }, + { 0x0ba8, 0x2228 }, + { 0x0ba9, 0x2227 }, + { 0x0bc0, 0x00af }, + { 0x0bc2, 0x22a5 }, + { 0x0bc3, 0x2229 }, + { 0x0bc4, 0x230a }, + { 0x0bc6, 0x005f }, + { 0x0bca, 0x2218 }, + { 0x0bcc, 0x2395 }, + { 0x0bce, 0x22a4 }, + { 0x0bcf, 0x25cb }, + { 0x0bd3, 0x2308 }, + { 0x0bd6, 0x222a }, + { 0x0bd8, 0x2283 }, + { 0x0bda, 0x2282 }, + { 0x0bdc, 0x22a2 }, + { 0x0bfc, 0x22a3 }, + { 0x0cdf, 0x2017 }, + { 0x0ce0, 0x05d0 }, + { 0x0ce1, 0x05d1 }, + { 0x0ce2, 0x05d2 }, + { 0x0ce3, 0x05d3 }, + { 0x0ce4, 0x05d4 }, + { 0x0ce5, 0x05d5 }, + { 0x0ce6, 0x05d6 }, + { 0x0ce7, 0x05d7 }, + { 0x0ce8, 0x05d8 }, + { 0x0ce9, 0x05d9 }, + { 0x0cea, 0x05da }, + { 0x0ceb, 0x05db }, + { 0x0cec, 0x05dc }, + { 0x0ced, 0x05dd }, + { 0x0cee, 0x05de }, + { 0x0cef, 0x05df }, + { 0x0cf0, 0x05e0 }, + { 0x0cf1, 0x05e1 }, + { 0x0cf2, 0x05e2 }, + { 0x0cf3, 0x05e3 }, + { 0x0cf4, 0x05e4 }, + { 0x0cf5, 0x05e5 }, + { 0x0cf6, 0x05e6 }, + { 0x0cf7, 0x05e7 }, + { 0x0cf8, 0x05e8 }, + { 0x0cf9, 0x05e9 }, + { 0x0cfa, 0x05ea }, + { 0x0da1, 0x0e01 }, + { 0x0da2, 0x0e02 }, + { 0x0da3, 0x0e03 }, + { 0x0da4, 0x0e04 }, + { 0x0da5, 0x0e05 }, + { 0x0da6, 0x0e06 }, + { 0x0da7, 0x0e07 }, + { 0x0da8, 0x0e08 }, + { 0x0da9, 0x0e09 }, + { 0x0daa, 0x0e0a }, + { 0x0dab, 0x0e0b }, + { 0x0dac, 0x0e0c }, + { 0x0dad, 0x0e0d }, + { 0x0dae, 0x0e0e }, + { 0x0daf, 0x0e0f }, + { 0x0db0, 0x0e10 }, + { 0x0db1, 0x0e11 }, + { 0x0db2, 0x0e12 }, + { 0x0db3, 0x0e13 }, + { 0x0db4, 0x0e14 }, + { 0x0db5, 0x0e15 }, + { 0x0db6, 0x0e16 }, + { 0x0db7, 0x0e17 }, + { 0x0db8, 0x0e18 }, + { 0x0db9, 0x0e19 }, + { 0x0dba, 0x0e1a }, + { 0x0dbb, 0x0e1b }, + { 0x0dbc, 0x0e1c }, + { 0x0dbd, 0x0e1d }, + { 0x0dbe, 0x0e1e }, + { 0x0dbf, 0x0e1f }, + { 0x0dc0, 0x0e20 }, + { 0x0dc1, 0x0e21 }, + { 0x0dc2, 0x0e22 }, + { 0x0dc3, 0x0e23 }, + { 0x0dc4, 0x0e24 }, + { 0x0dc5, 0x0e25 }, + { 0x0dc6, 0x0e26 }, + { 0x0dc7, 0x0e27 }, + { 0x0dc8, 0x0e28 }, + { 0x0dc9, 0x0e29 }, + { 0x0dca, 0x0e2a }, + { 0x0dcb, 0x0e2b }, + { 0x0dcc, 0x0e2c }, + { 0x0dcd, 0x0e2d }, + { 0x0dce, 0x0e2e }, + { 0x0dcf, 0x0e2f }, + { 0x0dd0, 0x0e30 }, + { 0x0dd1, 0x0e31 }, + { 0x0dd2, 0x0e32 }, + { 0x0dd3, 0x0e33 }, + { 0x0dd4, 0x0e34 }, + { 0x0dd5, 0x0e35 }, + { 0x0dd6, 0x0e36 }, + { 0x0dd7, 0x0e37 }, + { 0x0dd8, 0x0e38 }, + { 0x0dd9, 0x0e39 }, + { 0x0dda, 0x0e3a }, + { 0x0ddf, 0x0e3f }, + { 0x0de0, 0x0e40 }, + { 0x0de1, 0x0e41 }, + { 0x0de2, 0x0e42 }, + { 0x0de3, 0x0e43 }, + { 0x0de4, 0x0e44 }, + { 0x0de5, 0x0e45 }, + { 0x0de6, 0x0e46 }, + { 0x0de7, 0x0e47 }, + { 0x0de8, 0x0e48 }, + { 0x0de9, 0x0e49 }, + { 0x0dea, 0x0e4a }, + { 0x0deb, 0x0e4b }, + { 0x0dec, 0x0e4c }, + { 0x0ded, 0x0e4d }, + { 0x0df0, 0x0e50 }, + { 0x0df1, 0x0e51 }, + { 0x0df2, 0x0e52 }, + { 0x0df3, 0x0e53 }, + { 0x0df4, 0x0e54 }, + { 0x0df5, 0x0e55 }, + { 0x0df6, 0x0e56 }, + { 0x0df7, 0x0e57 }, + { 0x0df8, 0x0e58 }, + { 0x0df9, 0x0e59 }, + { 0x0ea1, 0x3131 }, + { 0x0ea2, 0x3132 }, + { 0x0ea3, 0x3133 }, + { 0x0ea4, 0x3134 }, + { 0x0ea5, 0x3135 }, + { 0x0ea6, 0x3136 }, + { 0x0ea7, 0x3137 }, + { 0x0ea8, 0x3138 }, + { 0x0ea9, 0x3139 }, + { 0x0eaa, 0x313a }, + { 0x0eab, 0x313b }, + { 0x0eac, 0x313c }, + { 0x0ead, 0x313d }, + { 0x0eae, 0x313e }, + { 0x0eaf, 0x313f }, + { 0x0eb0, 0x3140 }, + { 0x0eb1, 0x3141 }, + { 0x0eb2, 0x3142 }, + { 0x0eb3, 0x3143 }, + { 0x0eb4, 0x3144 }, + { 0x0eb5, 0x3145 }, + { 0x0eb6, 0x3146 }, + { 0x0eb7, 0x3147 }, + { 0x0eb8, 0x3148 }, + { 0x0eb9, 0x3149 }, + { 0x0eba, 0x314a }, + { 0x0ebb, 0x314b }, + { 0x0ebc, 0x314c }, + { 0x0ebd, 0x314d }, + { 0x0ebe, 0x314e }, + { 0x0ebf, 0x314f }, + { 0x0ec0, 0x3150 }, + { 0x0ec1, 0x3151 }, + { 0x0ec2, 0x3152 }, + { 0x0ec3, 0x3153 }, + { 0x0ec4, 0x3154 }, + { 0x0ec5, 0x3155 }, + { 0x0ec6, 0x3156 }, + { 0x0ec7, 0x3157 }, + { 0x0ec8, 0x3158 }, + { 0x0ec9, 0x3159 }, + { 0x0eca, 0x315a }, + { 0x0ecb, 0x315b }, + { 0x0ecc, 0x315c }, + { 0x0ecd, 0x315d }, + { 0x0ece, 0x315e }, + { 0x0ecf, 0x315f }, + { 0x0ed0, 0x3160 }, + { 0x0ed1, 0x3161 }, + { 0x0ed2, 0x3162 }, + { 0x0ed3, 0x3163 }, + { 0x0ed4, 0x11a8 }, + { 0x0ed5, 0x11a9 }, + { 0x0ed6, 0x11aa }, + { 0x0ed7, 0x11ab }, + { 0x0ed8, 0x11ac }, + { 0x0ed9, 0x11ad }, + { 0x0eda, 0x11ae }, + { 0x0edb, 0x11af }, + { 0x0edc, 0x11b0 }, + { 0x0edd, 0x11b1 }, + { 0x0ede, 0x11b2 }, + { 0x0edf, 0x11b3 }, + { 0x0ee0, 0x11b4 }, + { 0x0ee1, 0x11b5 }, + { 0x0ee2, 0x11b6 }, + { 0x0ee3, 0x11b7 }, + { 0x0ee4, 0x11b8 }, + { 0x0ee5, 0x11b9 }, + { 0x0ee6, 0x11ba }, + { 0x0ee7, 0x11bb }, + { 0x0ee8, 0x11bc }, + { 0x0ee9, 0x11bd }, + { 0x0eea, 0x11be }, + { 0x0eeb, 0x11bf }, + { 0x0eec, 0x11c0 }, + { 0x0eed, 0x11c1 }, + { 0x0eee, 0x11c2 }, + { 0x0eef, 0x316d }, + { 0x0ef0, 0x3171 }, + { 0x0ef1, 0x3178 }, + { 0x0ef2, 0x317f }, + { 0x0ef3, 0x3181 }, + { 0x0ef4, 0x3184 }, + { 0x0ef5, 0x3186 }, + { 0x0ef6, 0x318d }, + { 0x0ef7, 0x318e }, + { 0x0ef8, 0x11eb }, + { 0x0ef9, 0x11f0 }, + { 0x0efa, 0x11f9 }, + { 0x0eff, 0x20a9 }, + { 0x13a4, 0x20ac }, + { 0x13bc, 0x0152 }, + { 0x13bd, 0x0153 }, + { 0x13be, 0x0178 }, + { 0x20ac, 0x20ac }, + { 0xfe50, '`' }, + { 0xfe51, 0x00b4 }, + { 0xfe52, '^' }, + { 0xfe53, '~' }, + { 0xfe54, 0x00af }, + { 0xfe55, 0x02d8 }, + { 0xfe56, 0x02d9 }, + { 0xfe57, 0x00a8 }, + { 0xfe58, 0x02da }, + { 0xfe59, 0x02dd }, + { 0xfe5a, 0x02c7 }, + { 0xfe5b, 0x00b8 }, + { 0xfe5c, 0x02db }, + { 0xfe5d, 0x037a }, + { 0xfe5e, 0x309b }, + { 0xfe5f, 0x309c }, + { 0xfe63, '/' }, + { 0xfe64, 0x02bc }, + { 0xfe65, 0x02bd }, + { 0xfe66, 0x02f5 }, + { 0xfe67, 0x02f3 }, + { 0xfe68, 0x02cd }, + { 0xfe69, 0xa788 }, + { 0xfe6a, 0x02f7 }, + { 0xfe6e, ',' }, + { 0xfe6f, 0x00a4 }, + { 0xfe80, 'a' }, /* XK_dead_a */ + { 0xfe81, 'A' }, /* XK_dead_A */ + { 0xfe82, 'e' }, /* XK_dead_e */ + { 0xfe83, 'E' }, /* XK_dead_E */ + { 0xfe84, 'i' }, /* XK_dead_i */ + { 0xfe85, 'I' }, /* XK_dead_I */ + { 0xfe86, 'o' }, /* XK_dead_o */ + { 0xfe87, 'O' }, /* XK_dead_O */ + { 0xfe88, 'u' }, /* XK_dead_u */ + { 0xfe89, 'U' }, /* XK_dead_U */ + { 0xfe8a, 0x0259 }, + { 0xfe8b, 0x018f }, + { 0xfe8c, 0x00b5 }, + { 0xfe90, '_' }, + { 0xfe91, 0x02c8 }, + { 0xfe92, 0x02cc }, + { 0xff80 /*XKB_KEY_KP_Space*/, ' ' }, + { 0xff95 /*XKB_KEY_KP_7*/, 0x0037 }, + { 0xff96 /*XKB_KEY_KP_4*/, 0x0034 }, + { 0xff97 /*XKB_KEY_KP_8*/, 0x0038 }, + { 0xff98 /*XKB_KEY_KP_6*/, 0x0036 }, + { 0xff99 /*XKB_KEY_KP_2*/, 0x0032 }, + { 0xff9a /*XKB_KEY_KP_9*/, 0x0039 }, + { 0xff9b /*XKB_KEY_KP_3*/, 0x0033 }, + { 0xff9c /*XKB_KEY_KP_1*/, 0x0031 }, + { 0xff9d /*XKB_KEY_KP_5*/, 0x0035 }, + { 0xff9e /*XKB_KEY_KP_0*/, 0x0030 }, + { 0xffaa /*XKB_KEY_KP_Multiply*/, '*' }, + { 0xffab /*XKB_KEY_KP_Add*/, '+' }, + { 0xffac /*XKB_KEY_KP_Separator*/, ',' }, + { 0xffad /*XKB_KEY_KP_Subtract*/, '-' }, + { 0xffae /*XKB_KEY_KP_Decimal*/, '.' }, + { 0xffaf /*XKB_KEY_KP_Divide*/, '/' }, + { 0xffb0 /*XKB_KEY_KP_0*/, 0x0030 }, + { 0xffb1 /*XKB_KEY_KP_1*/, 0x0031 }, + { 0xffb2 /*XKB_KEY_KP_2*/, 0x0032 }, + { 0xffb3 /*XKB_KEY_KP_3*/, 0x0033 }, + { 0xffb4 /*XKB_KEY_KP_4*/, 0x0034 }, + { 0xffb5 /*XKB_KEY_KP_5*/, 0x0035 }, + { 0xffb6 /*XKB_KEY_KP_6*/, 0x0036 }, + { 0xffb7 /*XKB_KEY_KP_7*/, 0x0037 }, + { 0xffb8 /*XKB_KEY_KP_8*/, 0x0038 }, + { 0xffb9 /*XKB_KEY_KP_9*/, 0x0039 }, + { 0xffbd /*XKB_KEY_KP_Equal*/, '=' } +}; + +// translate the X11 KeySyms for a key to sokol-app key code +// NOTE: this is only used as a fallback, in case the XBK method fails +// it is layout-dependent and will fail partially on most non-US layouts. +// +static sapp_keycode _sapp_tc_x11_translate_keysyms(const KeySym* keysyms, int width) { + if (width > 1) { + switch (keysyms[1]) { + case XK_KP_0: return SAPP_KEYCODE_KP_0; + case XK_KP_1: return SAPP_KEYCODE_KP_1; + case XK_KP_2: return SAPP_KEYCODE_KP_2; + case XK_KP_3: return SAPP_KEYCODE_KP_3; + case XK_KP_4: return SAPP_KEYCODE_KP_4; + case XK_KP_5: return SAPP_KEYCODE_KP_5; + case XK_KP_6: return SAPP_KEYCODE_KP_6; + case XK_KP_7: return SAPP_KEYCODE_KP_7; + case XK_KP_8: return SAPP_KEYCODE_KP_8; + case XK_KP_9: return SAPP_KEYCODE_KP_9; + case XK_KP_Separator: + case XK_KP_Decimal: return SAPP_KEYCODE_KP_DECIMAL; + case XK_KP_Equal: return SAPP_KEYCODE_KP_EQUAL; + case XK_KP_Enter: return SAPP_KEYCODE_KP_ENTER; + default: break; + } + } + + switch (keysyms[0]) { + case XK_Escape: return SAPP_KEYCODE_ESCAPE; + case XK_Tab: return SAPP_KEYCODE_TAB; + case XK_Shift_L: return SAPP_KEYCODE_LEFT_SHIFT; + case XK_Shift_R: return SAPP_KEYCODE_RIGHT_SHIFT; + case XK_Control_L: return SAPP_KEYCODE_LEFT_CONTROL; + case XK_Control_R: return SAPP_KEYCODE_RIGHT_CONTROL; + case XK_Meta_L: + case XK_Alt_L: return SAPP_KEYCODE_LEFT_ALT; + case XK_Mode_switch: // Mapped to Alt_R on many keyboards + case XK_ISO_Level3_Shift: // AltGr on at least some machines + case XK_Meta_R: + case XK_Alt_R: return SAPP_KEYCODE_RIGHT_ALT; + case XK_Super_L: return SAPP_KEYCODE_LEFT_SUPER; + case XK_Super_R: return SAPP_KEYCODE_RIGHT_SUPER; + case XK_Menu: return SAPP_KEYCODE_MENU; + case XK_Num_Lock: return SAPP_KEYCODE_NUM_LOCK; + case XK_Caps_Lock: return SAPP_KEYCODE_CAPS_LOCK; + case XK_Print: return SAPP_KEYCODE_PRINT_SCREEN; + case XK_Scroll_Lock: return SAPP_KEYCODE_SCROLL_LOCK; + case XK_Pause: return SAPP_KEYCODE_PAUSE; + case XK_Delete: return SAPP_KEYCODE_DELETE; + case XK_BackSpace: return SAPP_KEYCODE_BACKSPACE; + case XK_Return: return SAPP_KEYCODE_ENTER; + case XK_Home: return SAPP_KEYCODE_HOME; + case XK_End: return SAPP_KEYCODE_END; + case XK_Page_Up: return SAPP_KEYCODE_PAGE_UP; + case XK_Page_Down: return SAPP_KEYCODE_PAGE_DOWN; + case XK_Insert: return SAPP_KEYCODE_INSERT; + case XK_Left: return SAPP_KEYCODE_LEFT; + case XK_Right: return SAPP_KEYCODE_RIGHT; + case XK_Down: return SAPP_KEYCODE_DOWN; + case XK_Up: return SAPP_KEYCODE_UP; + case XK_F1: return SAPP_KEYCODE_F1; + case XK_F2: return SAPP_KEYCODE_F2; + case XK_F3: return SAPP_KEYCODE_F3; + case XK_F4: return SAPP_KEYCODE_F4; + case XK_F5: return SAPP_KEYCODE_F5; + case XK_F6: return SAPP_KEYCODE_F6; + case XK_F7: return SAPP_KEYCODE_F7; + case XK_F8: return SAPP_KEYCODE_F8; + case XK_F9: return SAPP_KEYCODE_F9; + case XK_F10: return SAPP_KEYCODE_F10; + case XK_F11: return SAPP_KEYCODE_F11; + case XK_F12: return SAPP_KEYCODE_F12; + case XK_F13: return SAPP_KEYCODE_F13; + case XK_F14: return SAPP_KEYCODE_F14; + case XK_F15: return SAPP_KEYCODE_F15; + case XK_F16: return SAPP_KEYCODE_F16; + case XK_F17: return SAPP_KEYCODE_F17; + case XK_F18: return SAPP_KEYCODE_F18; + case XK_F19: return SAPP_KEYCODE_F19; + case XK_F20: return SAPP_KEYCODE_F20; + case XK_F21: return SAPP_KEYCODE_F21; + case XK_F22: return SAPP_KEYCODE_F22; + case XK_F23: return SAPP_KEYCODE_F23; + case XK_F24: return SAPP_KEYCODE_F24; + case XK_F25: return SAPP_KEYCODE_F25; + + // numeric keypad + case XK_KP_Divide: return SAPP_KEYCODE_KP_DIVIDE; + case XK_KP_Multiply: return SAPP_KEYCODE_KP_MULTIPLY; + case XK_KP_Subtract: return SAPP_KEYCODE_KP_SUBTRACT; + case XK_KP_Add: return SAPP_KEYCODE_KP_ADD; + + // these should have been detected in secondary keysym test above! + case XK_KP_Insert: return SAPP_KEYCODE_KP_0; + case XK_KP_End: return SAPP_KEYCODE_KP_1; + case XK_KP_Down: return SAPP_KEYCODE_KP_2; + case XK_KP_Page_Down: return SAPP_KEYCODE_KP_3; + case XK_KP_Left: return SAPP_KEYCODE_KP_4; + case XK_KP_Right: return SAPP_KEYCODE_KP_6; + case XK_KP_Home: return SAPP_KEYCODE_KP_7; + case XK_KP_Up: return SAPP_KEYCODE_KP_8; + case XK_KP_Page_Up: return SAPP_KEYCODE_KP_9; + case XK_KP_Delete: return SAPP_KEYCODE_KP_DECIMAL; + case XK_KP_Equal: return SAPP_KEYCODE_KP_EQUAL; + case XK_KP_Enter: return SAPP_KEYCODE_KP_ENTER; + + // last resort: Check for printable keys (should not happen if the XKB + // extension is available). This will give a layout dependent mapping + // (which is wrong, and we may miss some keys, especially on non-US + // keyboards), but it's better than nothing... + case XK_a: return SAPP_KEYCODE_A; + case XK_b: return SAPP_KEYCODE_B; + case XK_c: return SAPP_KEYCODE_C; + case XK_d: return SAPP_KEYCODE_D; + case XK_e: return SAPP_KEYCODE_E; + case XK_f: return SAPP_KEYCODE_F; + case XK_g: return SAPP_KEYCODE_G; + case XK_h: return SAPP_KEYCODE_H; + case XK_i: return SAPP_KEYCODE_I; + case XK_j: return SAPP_KEYCODE_J; + case XK_k: return SAPP_KEYCODE_K; + case XK_l: return SAPP_KEYCODE_L; + case XK_m: return SAPP_KEYCODE_M; + case XK_n: return SAPP_KEYCODE_N; + case XK_o: return SAPP_KEYCODE_O; + case XK_p: return SAPP_KEYCODE_P; + case XK_q: return SAPP_KEYCODE_Q; + case XK_r: return SAPP_KEYCODE_R; + case XK_s: return SAPP_KEYCODE_S; + case XK_t: return SAPP_KEYCODE_T; + case XK_u: return SAPP_KEYCODE_U; + case XK_v: return SAPP_KEYCODE_V; + case XK_w: return SAPP_KEYCODE_W; + case XK_x: return SAPP_KEYCODE_X; + case XK_y: return SAPP_KEYCODE_Y; + case XK_z: return SAPP_KEYCODE_Z; + case XK_1: return SAPP_KEYCODE_1; + case XK_2: return SAPP_KEYCODE_2; + case XK_3: return SAPP_KEYCODE_3; + case XK_4: return SAPP_KEYCODE_4; + case XK_5: return SAPP_KEYCODE_5; + case XK_6: return SAPP_KEYCODE_6; + case XK_7: return SAPP_KEYCODE_7; + case XK_8: return SAPP_KEYCODE_8; + case XK_9: return SAPP_KEYCODE_9; + case XK_0: return SAPP_KEYCODE_0; + case XK_space: return SAPP_KEYCODE_SPACE; + case XK_minus: return SAPP_KEYCODE_MINUS; + case XK_equal: return SAPP_KEYCODE_EQUAL; + case XK_bracketleft: return SAPP_KEYCODE_LEFT_BRACKET; + case XK_bracketright: return SAPP_KEYCODE_RIGHT_BRACKET; + case XK_backslash: return SAPP_KEYCODE_BACKSLASH; + case XK_semicolon: return SAPP_KEYCODE_SEMICOLON; + case XK_apostrophe: return SAPP_KEYCODE_APOSTROPHE; + case XK_grave: return SAPP_KEYCODE_GRAVE_ACCENT; + case XK_comma: return SAPP_KEYCODE_COMMA; + case XK_period: return SAPP_KEYCODE_PERIOD; + case XK_slash: return SAPP_KEYCODE_SLASH; + case XK_less: return SAPP_KEYCODE_WORLD_1; // At least in some layouts... + default: break; + } + + // no matching translation was found + return SAPP_KEYCODE_INVALID; +} +static void _sapp_tc_x11_init_keytable(void) { + for (int i = 0; i < SAPP_MAX_KEYCODES; i++) { + _sapp_tc.keycodes[i] = SAPP_KEYCODE_INVALID; + } + // use XKB to determine physical key locations independently of the current keyboard layout + XkbDescPtr desc = XkbGetMap(_sapp_tc.display, 0, XkbUseCoreKbd); + SOKOL_ASSERT(desc); + XkbGetNames(_sapp_tc.display, XkbKeyNamesMask | XkbKeyAliasesMask, desc); + + const int scancode_min = desc->min_key_code; + const int scancode_max = desc->max_key_code; + + const struct { sapp_keycode key; const char* name; } keymap[] = { + { SAPP_KEYCODE_GRAVE_ACCENT, "TLDE" }, + { SAPP_KEYCODE_1, "AE01" }, + { SAPP_KEYCODE_2, "AE02" }, + { SAPP_KEYCODE_3, "AE03" }, + { SAPP_KEYCODE_4, "AE04" }, + { SAPP_KEYCODE_5, "AE05" }, + { SAPP_KEYCODE_6, "AE06" }, + { SAPP_KEYCODE_7, "AE07" }, + { SAPP_KEYCODE_8, "AE08" }, + { SAPP_KEYCODE_9, "AE09" }, + { SAPP_KEYCODE_0, "AE10" }, + { SAPP_KEYCODE_MINUS, "AE11" }, + { SAPP_KEYCODE_EQUAL, "AE12" }, + { SAPP_KEYCODE_Q, "AD01" }, + { SAPP_KEYCODE_W, "AD02" }, + { SAPP_KEYCODE_E, "AD03" }, + { SAPP_KEYCODE_R, "AD04" }, + { SAPP_KEYCODE_T, "AD05" }, + { SAPP_KEYCODE_Y, "AD06" }, + { SAPP_KEYCODE_U, "AD07" }, + { SAPP_KEYCODE_I, "AD08" }, + { SAPP_KEYCODE_O, "AD09" }, + { SAPP_KEYCODE_P, "AD10" }, + { SAPP_KEYCODE_LEFT_BRACKET, "AD11" }, + { SAPP_KEYCODE_RIGHT_BRACKET, "AD12" }, + { SAPP_KEYCODE_A, "AC01" }, + { SAPP_KEYCODE_S, "AC02" }, + { SAPP_KEYCODE_D, "AC03" }, + { SAPP_KEYCODE_F, "AC04" }, + { SAPP_KEYCODE_G, "AC05" }, + { SAPP_KEYCODE_H, "AC06" }, + { SAPP_KEYCODE_J, "AC07" }, + { SAPP_KEYCODE_K, "AC08" }, + { SAPP_KEYCODE_L, "AC09" }, + { SAPP_KEYCODE_SEMICOLON, "AC10" }, + { SAPP_KEYCODE_APOSTROPHE, "AC11" }, + { SAPP_KEYCODE_Z, "AB01" }, + { SAPP_KEYCODE_X, "AB02" }, + { SAPP_KEYCODE_C, "AB03" }, + { SAPP_KEYCODE_V, "AB04" }, + { SAPP_KEYCODE_B, "AB05" }, + { SAPP_KEYCODE_N, "AB06" }, + { SAPP_KEYCODE_M, "AB07" }, + { SAPP_KEYCODE_COMMA, "AB08" }, + { SAPP_KEYCODE_PERIOD, "AB09" }, + { SAPP_KEYCODE_SLASH, "AB10" }, + { SAPP_KEYCODE_BACKSLASH, "BKSL" }, + { SAPP_KEYCODE_WORLD_1, "LSGT" }, + { SAPP_KEYCODE_SPACE, "SPCE" }, + { SAPP_KEYCODE_ESCAPE, "ESC" }, + { SAPP_KEYCODE_ENTER, "RTRN" }, + { SAPP_KEYCODE_TAB, "TAB" }, + { SAPP_KEYCODE_BACKSPACE, "BKSP" }, + { SAPP_KEYCODE_INSERT, "INS" }, + { SAPP_KEYCODE_DELETE, "DELE" }, + { SAPP_KEYCODE_RIGHT, "RGHT" }, + { SAPP_KEYCODE_LEFT, "LEFT" }, + { SAPP_KEYCODE_DOWN, "DOWN" }, + { SAPP_KEYCODE_UP, "UP" }, + { SAPP_KEYCODE_PAGE_UP, "PGUP" }, + { SAPP_KEYCODE_PAGE_DOWN, "PGDN" }, + { SAPP_KEYCODE_HOME, "HOME" }, + { SAPP_KEYCODE_END, "END" }, + { SAPP_KEYCODE_CAPS_LOCK, "CAPS" }, + { SAPP_KEYCODE_SCROLL_LOCK, "SCLK" }, + { SAPP_KEYCODE_NUM_LOCK, "NMLK" }, + { SAPP_KEYCODE_PRINT_SCREEN, "PRSC" }, + { SAPP_KEYCODE_PAUSE, "PAUS" }, + { SAPP_KEYCODE_F1, "FK01" }, + { SAPP_KEYCODE_F2, "FK02" }, + { SAPP_KEYCODE_F3, "FK03" }, + { SAPP_KEYCODE_F4, "FK04" }, + { SAPP_KEYCODE_F5, "FK05" }, + { SAPP_KEYCODE_F6, "FK06" }, + { SAPP_KEYCODE_F7, "FK07" }, + { SAPP_KEYCODE_F8, "FK08" }, + { SAPP_KEYCODE_F9, "FK09" }, + { SAPP_KEYCODE_F10, "FK10" }, + { SAPP_KEYCODE_F11, "FK11" }, + { SAPP_KEYCODE_F12, "FK12" }, + { SAPP_KEYCODE_F13, "FK13" }, + { SAPP_KEYCODE_F14, "FK14" }, + { SAPP_KEYCODE_F15, "FK15" }, + { SAPP_KEYCODE_F16, "FK16" }, + { SAPP_KEYCODE_F17, "FK17" }, + { SAPP_KEYCODE_F18, "FK18" }, + { SAPP_KEYCODE_F19, "FK19" }, + { SAPP_KEYCODE_F20, "FK20" }, + { SAPP_KEYCODE_F21, "FK21" }, + { SAPP_KEYCODE_F22, "FK22" }, + { SAPP_KEYCODE_F23, "FK23" }, + { SAPP_KEYCODE_F24, "FK24" }, + { SAPP_KEYCODE_F25, "FK25" }, + { SAPP_KEYCODE_KP_0, "KP0" }, + { SAPP_KEYCODE_KP_1, "KP1" }, + { SAPP_KEYCODE_KP_2, "KP2" }, + { SAPP_KEYCODE_KP_3, "KP3" }, + { SAPP_KEYCODE_KP_4, "KP4" }, + { SAPP_KEYCODE_KP_5, "KP5" }, + { SAPP_KEYCODE_KP_6, "KP6" }, + { SAPP_KEYCODE_KP_7, "KP7" }, + { SAPP_KEYCODE_KP_8, "KP8" }, + { SAPP_KEYCODE_KP_9, "KP9" }, + { SAPP_KEYCODE_KP_DECIMAL, "KPDL" }, + { SAPP_KEYCODE_KP_DIVIDE, "KPDV" }, + { SAPP_KEYCODE_KP_MULTIPLY, "KPMU" }, + { SAPP_KEYCODE_KP_SUBTRACT, "KPSU" }, + { SAPP_KEYCODE_KP_ADD, "KPAD" }, + { SAPP_KEYCODE_KP_ENTER, "KPEN" }, + { SAPP_KEYCODE_KP_EQUAL, "KPEQ" }, + { SAPP_KEYCODE_LEFT_SHIFT, "LFSH" }, + { SAPP_KEYCODE_LEFT_CONTROL, "LCTL" }, + { SAPP_KEYCODE_LEFT_ALT, "LALT" }, + { SAPP_KEYCODE_LEFT_SUPER, "LWIN" }, + { SAPP_KEYCODE_RIGHT_SHIFT, "RTSH" }, + { SAPP_KEYCODE_RIGHT_CONTROL, "RCTL" }, + { SAPP_KEYCODE_RIGHT_ALT, "RALT" }, + { SAPP_KEYCODE_RIGHT_ALT, "LVL3" }, + { SAPP_KEYCODE_RIGHT_ALT, "MDSW" }, + { SAPP_KEYCODE_RIGHT_SUPER, "RWIN" }, + { SAPP_KEYCODE_MENU, "MENU" } + }; + const int num_keymap_items = (int)(sizeof(keymap) / sizeof(keymap[0])); + + // find X11 keycode to sokol-app key code mapping + for (int scancode = scancode_min; scancode <= scancode_max; scancode++) { + sapp_keycode key = SAPP_KEYCODE_INVALID; + for (int i = 0; i < num_keymap_items; i++) { + if (strncmp(desc->names->keys[scancode].name, keymap[i].name, XkbKeyNameLength) == 0) { + key = keymap[i].key; + break; + } + } + + // fall back to key aliases in case the key name did not match + for (int i = 0; i < desc->names->num_key_aliases; i++) { + if (key != SAPP_KEYCODE_INVALID) { + break; + } + if (strncmp(desc->names->key_aliases[i].real, desc->names->keys[scancode].name, XkbKeyNameLength) != 0) { + continue; + } + for (int j = 0; j < num_keymap_items; j++) { + if (strncmp(desc->names->key_aliases[i].alias, keymap[j].name, XkbKeyNameLength) == 0) { + key = keymap[j].key; + break; + } + } + } + _sapp_tc.keycodes[scancode] = key; + } + XkbFreeNames(desc, XkbKeyNamesMask, True); + XkbFreeKeyboard(desc, 0, True); + + int width = 0; + KeySym* keysyms = XGetKeyboardMapping(_sapp_tc.display, scancode_min, scancode_max - scancode_min + 1, &width); + for (int scancode = scancode_min; scancode <= scancode_max; scancode++) { + // translate untranslated key codes using the traditional X11 KeySym lookups + if (_sapp_tc.keycodes[scancode] == SAPP_KEYCODE_INVALID) { + const size_t base = (size_t)((scancode - scancode_min) * width); + _sapp_tc.keycodes[scancode] = _sapp_tc_x11_translate_keysyms(&keysyms[base], width); + } + } + XFree(keysyms); +} +static int32_t _sapp_tc_x11_keysym_to_unicode(KeySym keysym) { + int min = 0; + int max = sizeof(_sapp_tc_x11_keysymtab) / sizeof(struct _sapp_tc_x11_codepair) - 1; + int mid; + + /* First check for Latin-1 characters (1:1 mapping) */ + if ((keysym >= 0x0020 && keysym <= 0x007e) || + (keysym >= 0x00a0 && keysym <= 0x00ff)) + { + return keysym; + } + + /* Also check for directly encoded 24-bit UCS characters */ + if ((keysym & 0xff000000) == 0x01000000) { + return keysym & 0x00ffffff; + } + + /* Binary search in table */ + while (max >= min) { + mid = (min + max) / 2; + if (_sapp_tc_x11_keysymtab[mid].keysym < keysym) { + min = mid + 1; + } else if (_sapp_tc_x11_keysymtab[mid].keysym > keysym) { + max = mid - 1; + } else { + return _sapp_tc_x11_keysymtab[mid].ucs; + } + } + + /* No matching Unicode value found */ + return -1; +} + +/*-- keycode helpers (per-window software key repeat) ------------------------*/ +static sapp_keycode _sapp_tc_translate_key(int scancode) { + if ((scancode >= 0) && (scancode < _SAPP_X11_MAX_X11_KEYCODES)) { + return _sapp_tc.keycodes[scancode]; + } + return SAPP_KEYCODE_INVALID; +} + +static bool _sapp_tc_x11_keypress_repeat(_sapp_tc_window_t* w, int keycode) { + /* XkbSetDetectableAutoRepeat is enabled at startup, so a held key emits + repeated KeyPress without paired KeyRelease; the first press reports + repeat=false, subsequent ones true until release */ + bool repeat = false; + if ((keycode >= 0) && (keycode < _SAPP_X11_MAX_X11_KEYCODES)) { + repeat = w->key_repeat[keycode]; + w->key_repeat[keycode] = true; + } + return repeat; +} + +static void _sapp_tc_x11_keyrelease_repeat(_sapp_tc_window_t* w, int keycode) { + if ((keycode >= 0) && (keycode < _SAPP_X11_MAX_X11_KEYCODES)) { + w->key_repeat[keycode] = false; + } +} + +/*-- X error handling (controlled failure instead of an Xlib abort) ----------*/ +static int _sapp_tc_x11_error_handler(Display* display, XErrorEvent* event) { + (void)display; + _sapp_tc.error_code = event->error_code; + return 0; +} + +static void _sapp_tc_x11_grab_error_handler(void) { + _sapp_tc.error_code = 0; + _sapp_tc.prev_error_handler = XSetErrorHandler(_sapp_tc_x11_error_handler); +} + +static void _sapp_tc_x11_release_error_handler(void) { + XSync(_sapp_tc.display, False); + XSetErrorHandler(_sapp_tc.prev_error_handler); + _sapp_tc.prev_error_handler = 0; +} + +/*-- window properties -------------------------------------------------------*/ +static unsigned long _sapp_tc_x11_get_window_property(Window xwin, Atom property, Atom type, unsigned char** value) { + Atom actual_type; + int actual_format; + unsigned long item_count, bytes_after; + XGetWindowProperty(_sapp_tc.display, xwin, property, + 0, LONG_MAX, False, type, + &actual_type, &actual_format, &item_count, &bytes_after, value); + return item_count; +} + +static int _sapp_tc_x11_get_window_state(_sapp_tc_window_t* w) { + int result = WithdrawnState; + struct { + CARD32 state; + Window icon; + } *state = NULL; + if (_sapp_tc_x11_get_window_property(w->xwin, _sapp_tc.WM_STATE, _sapp_tc.WM_STATE, (unsigned char**)&state) >= 2) { + result = (int)state->state; + } + if (state) { + XFree(state); + } + return result; +} + +static void _sapp_tc_x11_send_event(Window xwin, Atom type, long a, long b, long c, long d, long e) { + XEvent event; + memset(&event, 0, sizeof(event)); + event.type = ClientMessage; + event.xclient.window = xwin; + event.xclient.format = 32; + event.xclient.message_type = type; + event.xclient.data.l[0] = a; + event.xclient.data.l[1] = b; + event.xclient.data.l[2] = c; + event.xclient.data.l[3] = d; + event.xclient.data.l[4] = e; + XSendEvent(_sapp_tc.display, _sapp_tc.root, False, + SubstructureNotifyMask | SubstructureRedirectMask, &event); +} + +/*-- atoms / extensions -------------------------------------------------------*/ +static void _sapp_tc_x11_init_extensions(void) { + Display* d = _sapp_tc.display; + _sapp_tc.UTF8_STRING = XInternAtom(d, "UTF8_STRING", False); + _sapp_tc.CLIPBOARD = XInternAtom(d, "CLIPBOARD", False); + _sapp_tc.TARGETS = XInternAtom(d, "TARGETS", False); + _sapp_tc.WM_PROTOCOLS = XInternAtom(d, "WM_PROTOCOLS", False); + _sapp_tc.WM_DELETE_WINDOW = XInternAtom(d, "WM_DELETE_WINDOW", False); + _sapp_tc.WM_STATE = XInternAtom(d, "WM_STATE", False); + _sapp_tc.NET_WM_NAME = XInternAtom(d, "_NET_WM_NAME", False); + _sapp_tc.NET_WM_ICON_NAME = XInternAtom(d, "_NET_WM_ICON_NAME", False); + _sapp_tc.NET_WM_STATE = XInternAtom(d, "_NET_WM_STATE", False); + _sapp_tc.NET_WM_STATE_FULLSCREEN = XInternAtom(d, "_NET_WM_STATE_FULLSCREEN", False); + _sapp_tc.MOTIF_WM_HINTS = XInternAtom(d, "_MOTIF_WM_HINTS", False); + _sapp_tc.SAPP_TC_SELECTION = XInternAtom(d, "SAPP_TC_SELECTION", False); + if (_sapp_tc.app.drop_enabled) { + _sapp_tc.xdnd.XdndAware = XInternAtom(d, "XdndAware", False); + _sapp_tc.xdnd.XdndEnter = XInternAtom(d, "XdndEnter", False); + _sapp_tc.xdnd.XdndPosition = XInternAtom(d, "XdndPosition", False); + _sapp_tc.xdnd.XdndStatus = XInternAtom(d, "XdndStatus", False); + _sapp_tc.xdnd.XdndActionCopy = XInternAtom(d, "XdndActionCopy", False); + _sapp_tc.xdnd.XdndDrop = XInternAtom(d, "XdndDrop", False); + _sapp_tc.xdnd.XdndFinished = XInternAtom(d, "XdndFinished", False); + _sapp_tc.xdnd.XdndSelection = XInternAtom(d, "XdndSelection", False); + _sapp_tc.xdnd.XdndTypeList = XInternAtom(d, "XdndTypeList", False); + _sapp_tc.xdnd.text_uri_list = XInternAtom(d, "text/uri-list", False); + } +} + +/*-- DPI (Xft.dpi resource, Qt/GTK-compatible; 96 fallback) -------------------*/ +static void _sapp_tc_x11_query_system_dpi(void) { + _sapp_tc.dpi = 96.0f; + const char* rms = XResourceManagerString(_sapp_tc.display); + if (rms) { + XrmDatabase db = XrmGetStringDatabase(rms); + if (db) { + XrmValue value; + char* type = NULL; + if (XrmGetResource(db, "Xft.dpi", "Xft.Dpi", &type, &value)) { + if (type && (0 == strcmp(type, "String")) && value.addr) { + _sapp_tc.dpi = (float)atof(value.addr); + if (_sapp_tc.dpi <= 0.0f) { + _sapp_tc.dpi = 96.0f; + } + } + } + XrmDestroyDatabase(db); + } + } +} + +/*-- mouse cursors (Xcursor themed, X font cursor fallback) -------------------*/ +static void _sapp_tc_x11_create_hidden_cursor(void) { + XcursorImage* img = XcursorImageCreate(16, 16); + if (!img) return; + img->xhot = 0; + img->yhot = 0; + memset(img->pixels, 0, (size_t)(16 * 16) * sizeof(XcursorPixel)); + _sapp_tc.hidden_cursor = XcursorImageLoadCursor(_sapp_tc.display, img); + XcursorImageDestroy(img); +} + +static void _sapp_tc_x11_create_standard_cursor(sapp_mouse_cursor cursor, const char* name, const char* theme, int size, uint32_t fallback_native) { + if (theme) { + XcursorImage* img = XcursorLibraryLoadImage(name, theme, size); + if (img) { + _sapp_tc.standard_cursors[cursor] = XcursorImageLoadCursor(_sapp_tc.display, img); + XcursorImageDestroy(img); + } + } + if ((0 == _sapp_tc.standard_cursors[cursor]) && (0 != fallback_native)) { + _sapp_tc.standard_cursors[cursor] = XCreateFontCursor(_sapp_tc.display, fallback_native); + } +} + +static void _sapp_tc_x11_init_cursors(void) { + const char* theme = XcursorGetTheme(_sapp_tc.display); + const int size = XcursorGetDefaultSize(_sapp_tc.display); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_DEFAULT, "default", theme, size, XC_left_ptr); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_ARROW, "default", theme, size, XC_left_ptr); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_IBEAM, "text", theme, size, XC_xterm); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_CROSSHAIR, "crosshair", theme, size, XC_crosshair); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_POINTING_HAND, "pointer", theme, size, XC_hand2); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_RESIZE_EW, "ew-resize", theme, size, XC_sb_h_double_arrow); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_RESIZE_NS, "ns-resize", theme, size, XC_sb_v_double_arrow); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_RESIZE_NWSE, "nwse-resize", theme, size, 0); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_RESIZE_NESW, "nesw-resize", theme, size, 0); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_RESIZE_ALL, "all-scroll", theme, size, XC_fleur); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_NOT_ALLOWED, "not-allowed", theme, size, 0); + _sapp_tc_x11_create_hidden_cursor(); +} + +static void _sapp_tc_x11_destroy_cursors(void) { + if (_sapp_tc.hidden_cursor) { + XFreeCursor(_sapp_tc.display, _sapp_tc.hidden_cursor); + _sapp_tc.hidden_cursor = 0; + } + for (int i = 0; i < _SAPP_MOUSECURSOR_NUM; i++) { + if (_sapp_tc.standard_cursors[i]) { + XFreeCursor(_sapp_tc.display, _sapp_tc.standard_cursors[i]); + _sapp_tc.standard_cursors[i] = 0; + } + if (_sapp_tc.custom_cursors[i]) { + XFreeCursor(_sapp_tc.display, _sapp_tc.custom_cursors[i]); + _sapp_tc.custom_cursors[i] = 0; + _sapp_tc.app.custom_cursor_bound[i] = false; + } + } +} + +static void _sapp_tc_x11_apply_cursor(void) { + /* app-global cursor (same model as sokol_app.h), applied to every window */ + const sapp_mouse_cursor cur = _sapp_tc.app.current_cursor; + Cursor c = 0; + if (_sapp_tc.app.mouse_shown) { + c = _sapp_tc.app.custom_cursor_bound[cur] ? _sapp_tc.custom_cursors[cur] + : _sapp_tc.standard_cursors[cur]; + } else { + c = _sapp_tc.hidden_cursor; + } + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* w = _sapp_tc.windows[i]; + if (!w || !w->xwin) continue; + if (c) { + XDefineCursor(_sapp_tc.display, w->xwin, c); + } else { + XUndefineCursor(_sapp_tc.display, w->xwin); + } + } + XFlush(_sapp_tc.display); +} + +/*-- title / decorations ------------------------------------------------------*/ +static void _sapp_tc_x11_update_window_title(_sapp_tc_window_t* w, const char* title) { + Xutf8SetWMProperties(_sapp_tc.display, w->xwin, title, title, NULL, 0, NULL, NULL, NULL); + XChangeProperty(_sapp_tc.display, w->xwin, _sapp_tc.NET_WM_NAME, _sapp_tc.UTF8_STRING, + 8, PropModeReplace, (unsigned char*)title, (int)strlen(title)); + XChangeProperty(_sapp_tc.display, w->xwin, _sapp_tc.NET_WM_ICON_NAME, _sapp_tc.UTF8_STRING, + 8, PropModeReplace, (unsigned char*)title, (int)strlen(title)); + XFlush(_sapp_tc.display); +} + +static void _sapp_tc_x11_set_decorated(_sapp_tc_window_t* w, bool decorated) { + struct { + unsigned long flags; + unsigned long functions; + unsigned long decorations; + long input_mode; + unsigned long status; + } hints; + memset(&hints, 0, sizeof(hints)); + hints.flags = (1L << 1); /* MWM_HINTS_DECORATIONS */ + hints.decorations = decorated ? 1 : 0; + XChangeProperty(_sapp_tc.display, w->xwin, _sapp_tc.MOTIF_WM_HINTS, _sapp_tc.MOTIF_WM_HINTS, + 32, PropModeReplace, (unsigned char*)&hints, 5); +} + +/*-- dimensions (polled per tick; ConfigureNotify is deliberately unused) -----*/ +static bool _sapp_tc_x11_update_dimensions(_sapp_tc_window_t* w) { + XWindowAttributes attribs; + memset(&attribs, 0, sizeof(attribs)); + XGetWindowAttributes(_sapp_tc.display, w->xwin, &attribs); + if ((attribs.width <= 0) || (attribs.height <= 0)) { + return false; + } + /* X11 has no separate backing scale: framebuffer == window pixels */ + const bool changed = (attribs.width != w->fb_width) || (attribs.height != w->fb_height); + w->fb_width = attribs.width; + w->fb_height = attribs.height; + const float scale = (w->dpi_scale > 0.0f) ? w->dpi_scale : 1.0f; + w->win_width = (int)((float)attribs.width / scale + 0.5f); + w->win_height = (int)((float)attribs.height / scale + 0.5f); + return changed; +} + +/*-- fullscreen (EWMH _NET_WM_STATE; must be called after XMapWindow) ---------*/ +static void _sapp_tc_x11_set_fullscreen(bool enable) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w) return; + if (_sapp_tc.NET_WM_STATE && _sapp_tc.NET_WM_STATE_FULLSCREEN) { + const long op = enable ? 1 : 0; /* _NET_WM_STATE_ADD : _NET_WM_STATE_REMOVE */ + _sapp_tc_x11_send_event(w->xwin, _sapp_tc.NET_WM_STATE, + op, (long)_sapp_tc.NET_WM_STATE_FULLSCREEN, 0, 1, 0); + } + _sapp_tc.app.fullscreen = enable; + XFlush(_sapp_tc.display); +} + +/*-- synchronous event wait (poll on the connection fd with a deadline) ------*/ +static bool _sapp_tc_x11_wait_for_event(Window xwin, int type, double timeout_sec, XEvent* out_event) { + const double deadline = _sapp_tc_now() + timeout_sec; + for (;;) { + if (XCheckTypedWindowEvent(_sapp_tc.display, xwin, type, out_event)) { + return true; + } + const double remaining = deadline - _sapp_tc_now(); + if (remaining <= 0.0) { + return false; + } + struct pollfd pfd; + pfd.fd = ConnectionNumber(_sapp_tc.display); + pfd.events = POLLIN; + pfd.revents = 0; + poll(&pfd, 1, (int)(remaining * 1000.0) + 1); + } +} + +/*-- clipboard (CLIPBOARD selection, UTF8_STRING only, no INCR) ---------------*/ +static void _sapp_tc_x11_serve_selection_request(XEvent* event) { + XSelectionRequestEvent* req = &event->xselectionrequest; + if (req->selection != _sapp_tc.CLIPBOARD) return; + if (!_sapp_tc.app.clipboard_enabled || !_sapp_tc.app.clipboard) return; + XSelectionEvent reply; + memset(&reply, 0, sizeof(reply)); + reply.type = SelectionNotify; + reply.display = req->display; + reply.requestor = req->requestor; + reply.selection = req->selection; + reply.target = req->target; + reply.property = req->property; + reply.time = req->time; + if (req->target == _sapp_tc.UTF8_STRING) { + XChangeProperty(_sapp_tc.display, req->requestor, req->property, + _sapp_tc.UTF8_STRING, 8, PropModeReplace, + (unsigned char*)_sapp_tc.app.clipboard, + (int)strlen(_sapp_tc.app.clipboard)); + } else if (req->target == _sapp_tc.TARGETS) { + XChangeProperty(_sapp_tc.display, req->requestor, req->property, + XA_ATOM, 32, PropModeReplace, + (unsigned char*)&_sapp_tc.UTF8_STRING, 1); + } else { + reply.property = None; + } + XSendEvent(_sapp_tc.display, req->requestor, False, 0, (XEvent*)&reply); + XFlush(_sapp_tc.display); +} + +/*-- drag & drop (XDND v5, text/uri-list) -------------------------------------*/ +static bool _sapp_tc_x11_parse_dropped_files_list(const char* src) { + SOKOL_ASSERT(src); + SOKOL_ASSERT(_sapp_tc.app.drop_buffer); + + memset(_sapp_tc.app.drop_buffer, 0, (size_t)_sapp_tc.app.drop_max_files * (size_t)_sapp_tc.app.drop_max_path_length); + _sapp_tc.app.drop_num_files = 0; + + /* + src is (potentially percent-encoded) string made of one or multiple paths + separated by \r\n, each path starting with 'file://' + */ + bool err = false; + int src_count = 0; + char src_chr = 0; + char* dst_ptr = _sapp_tc.app.drop_buffer; + const char* dst_end_ptr = dst_ptr + (_sapp_tc.app.drop_max_path_length - 1); // room for terminating 0 + while (0 != (src_chr = *src++)) { + src_count++; + char dst_chr = 0; + /* check leading 'file://' */ + if (src_count <= 7) { + if (((src_count == 1) && (src_chr != 'f')) || + ((src_count == 2) && (src_chr != 'i')) || + ((src_count == 3) && (src_chr != 'l')) || + ((src_count == 4) && (src_chr != 'e')) || + ((src_count == 5) && (src_chr != ':')) || + ((src_count == 6) && (src_chr != '/')) || + ((src_count == 7) && (src_chr != '/'))) + { + /* non-file: URI in drop list: ignore entry */ + err = true; + break; + } + } else if (src_chr == '\r') { + // skip + } else if (src_chr == '\n') { + src_count = 0; + _sapp_tc.app.drop_num_files++; + // too many files is not an error + if (_sapp_tc.app.drop_num_files >= _sapp_tc.app.drop_max_files) { + break; + } + dst_ptr = _sapp_tc.app.drop_buffer + _sapp_tc.app.drop_num_files * _sapp_tc.app.drop_max_path_length; + dst_end_ptr = dst_ptr + (_sapp_tc.app.drop_max_path_length - 1); + } else if ((src_chr == '%') && src[0] && src[1]) { + // a percent-encoded byte (most likely UTF-8 multibyte sequence) + const char digits[3] = { src[0], src[1], 0 }; + src += 2; + dst_chr = (char) strtol(digits, 0, 16); + } else { + dst_chr = src_chr; + } + if (dst_chr) { + // dst_end_ptr already has adjustment for terminating zero + if (dst_ptr < dst_end_ptr) { + *dst_ptr++ = dst_chr; + } else { + /* dropped file path too long: abort parse */ + err = true; + break; + } + } + } + if (err) { + memset(_sapp_tc.app.drop_buffer, 0, (size_t)_sapp_tc.app.drop_max_files * (size_t)_sapp_tc.app.drop_max_path_length); + _sapp_tc.app.drop_num_files = 0; + return false; + } else { + return true; + } +} + +/*-- per-window event senders --------------------------------------------------*/ +static uint32_t _sapp_tc_x11_mods(uint32_t x11_state) { + uint32_t m = 0; + if (x11_state & ShiftMask) m |= SAPP_MODIFIER_SHIFT; + if (x11_state & ControlMask) m |= SAPP_MODIFIER_CTRL; + if (x11_state & Mod1Mask) m |= SAPP_MODIFIER_ALT; + if (x11_state & Mod4Mask) m |= SAPP_MODIFIER_SUPER; + if (x11_state & Button1Mask) m |= SAPP_MODIFIER_LMB; + if (x11_state & Button2Mask) m |= SAPP_MODIFIER_MMB; + if (x11_state & Button3Mask) m |= SAPP_MODIFIER_RMB; + return m; +} + +/* X11 doesn't set a modifier's own bit on the event that presses/releases it; + emulate (key-down ORs the bit in, key-up masks it out; same for buttons) */ +static uint32_t _sapp_tc_x11_key_modifier_bit(sapp_keycode key) { + switch (key) { + case SAPP_KEYCODE_LEFT_SHIFT: + case SAPP_KEYCODE_RIGHT_SHIFT: return SAPP_MODIFIER_SHIFT; + case SAPP_KEYCODE_LEFT_CONTROL: + case SAPP_KEYCODE_RIGHT_CONTROL: return SAPP_MODIFIER_CTRL; + case SAPP_KEYCODE_LEFT_ALT: + case SAPP_KEYCODE_RIGHT_ALT: return SAPP_MODIFIER_ALT; + case SAPP_KEYCODE_LEFT_SUPER: + case SAPP_KEYCODE_RIGHT_SUPER: return SAPP_MODIFIER_SUPER; + default: return 0; + } +} + +static uint32_t _sapp_tc_x11_button_modifier_bit(sapp_mousebutton btn) { + switch (btn) { + case SAPP_MOUSEBUTTON_LEFT: return SAPP_MODIFIER_LMB; + case SAPP_MOUSEBUTTON_RIGHT: return SAPP_MODIFIER_RMB; + case SAPP_MOUSEBUTTON_MIDDLE: return SAPP_MODIFIER_MMB; + default: return 0; + } +} + +static sapp_mousebutton _sapp_tc_x11_translate_button(unsigned int button) { + switch (button) { + case Button1: return SAPP_MOUSEBUTTON_LEFT; + case Button2: return SAPP_MOUSEBUTTON_MIDDLE; + case Button3: return SAPP_MOUSEBUTTON_RIGHT; + default: return SAPP_MOUSEBUTTON_INVALID; + } +} + +static void _sapp_tc_x11_mouse_update(_sapp_tc_window_t* w, int x, int y, bool clear_dxdy) { + /* event coordinates are raw window pixels == framebuffer pixels on X11 */ + const float fx = (float)x; + const float fy = (float)y; + if (clear_dxdy || !w->mouse_pos_valid) { + w->mouse_dx = 0.0f; + w->mouse_dy = 0.0f; + w->mouse_pos_valid = true; + } else { + w->mouse_dx = fx - w->mouse_x; + w->mouse_dy = fy - w->mouse_y; + } + w->mouse_x = fx; + w->mouse_y = fy; +} + +static void _sapp_tc_x11_mouse_event(_sapp_tc_window_t* w, sapp_event_type type, sapp_mousebutton btn, uint32_t mods) { + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = type; + e.mouse_button = btn; + e.mouse_x = w->mouse_x; + e.mouse_y = w->mouse_y; + e.mouse_dx = w->mouse_dx; + e.mouse_dy = w->mouse_dy; + e.modifiers = mods; + _sapp_tc_send(w, &e); +} + +static void _sapp_tc_x11_scroll_event(_sapp_tc_window_t* w, float x, float y, uint32_t mods) { + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_MOUSE_SCROLL; + e.mouse_x = w->mouse_x; + e.mouse_y = w->mouse_y; + e.scroll_x = x; + e.scroll_y = y; + e.modifiers = mods; + _sapp_tc_send(w, &e); +} + +static void _sapp_tc_x11_key_event(_sapp_tc_window_t* w, sapp_event_type type, sapp_keycode key, bool repeat, uint32_t mods) { + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = type; + e.key_code = key; + e.key_repeat = repeat; + e.modifiers = mods; + _sapp_tc_send(w, &e); + /* Ctrl+V paste notification (exact-CTRL match, sokol_app.h parity); the + app reads the clipboard in its handler */ + if ((type == SAPP_EVENTTYPE_KEY_DOWN) && _sapp_tc.app.clipboard_enabled && + (mods == SAPP_MODIFIER_CTRL) && (key == SAPP_KEYCODE_V)) { + sapp_event pe; + memset(&pe, 0, sizeof(pe)); + pe.type = SAPP_EVENTTYPE_CLIPBOARD_PASTED; + pe.modifiers = SAPP_MODIFIER_CTRL; + _sapp_tc_send(w, &pe); + } +} + +static void _sapp_tc_x11_char_event(_sapp_tc_window_t* w, uint32_t c, bool repeat, uint32_t mods) { + if ((c < 32) || (c == 127)) return; /* control characters */ + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_CHAR; + e.char_code = c; + e.key_repeat = repeat; + e.modifiers = mods; + _sapp_tc_send(w, &e); +} + +static void _sapp_tc_x11_app_event(_sapp_tc_window_t* w, sapp_event_type type) { + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = type; + _sapp_tc_send(w, &e); +} + +/*-- XDND handlers -------------------------------------------------------------*/ +static void _sapp_tc_x11_on_selectionnotify(XEvent* event) { + /* XDND drop data arrival (clipboard replies are consumed synchronously by + _sapp_tc_x11_wait_for_event inside sapp_get_clipboard_string) */ + if (!_sapp_tc.app.drop_enabled) return; + if (event->xselection.property != _sapp_tc.xdnd.XdndSelection) return; + char* data = 0; + const uint32_t result = (uint32_t)_sapp_tc_x11_get_window_property( + event->xselection.requestor, + event->xselection.property, + event->xselection.target, + (unsigned char**)&data); + if (result && data) { + if (_sapp_tc_x11_parse_dropped_files_list(data)) { + _sapp_tc_window_t* w = _sapp_tc_lookup_xwin(event->xselection.requestor); + if (!w) w = _sapp_tc.xdnd.over; + if (!w) w = _sapp_tc.app.main; + if (w) { + /* FIXME (upstream parity): no modifier state available here */ + _sapp_tc_x11_app_event(w, SAPP_EVENTTYPE_FILES_DROPPED); + } + } + } + if (_sapp_tc.xdnd.version >= 2) { + XEvent reply; + memset(&reply, 0, sizeof(reply)); + reply.type = ClientMessage; + reply.xclient.window = _sapp_tc.xdnd.source; + reply.xclient.message_type = _sapp_tc.xdnd.XdndFinished; + reply.xclient.format = 32; + reply.xclient.data.l[0] = (long)event->xselection.requestor; + reply.xclient.data.l[1] = (long)result; + reply.xclient.data.l[2] = (long)_sapp_tc.xdnd.XdndActionCopy; + XSendEvent(_sapp_tc.display, _sapp_tc.xdnd.source, False, NoEventMask, &reply); + XFlush(_sapp_tc.display); + } + if (data) { + XFree(data); + } +} + +static void _sapp_tc_x11_on_clientmessage(XEvent* event) { + if (XFilterEvent(event, None)) { + return; + } + _sapp_tc_window_t* w = _sapp_tc_lookup_xwin(event->xclient.window); + if (event->xclient.message_type == _sapp_tc.WM_PROTOCOLS) { + const Atom protocol = (Atom)event->xclient.data.l[0]; + if ((protocol == _sapp_tc.WM_DELETE_WINDOW) && w) { + if (w->is_main) { + /* the cancellable quit dance runs at the loop tail (upstream + parity): QUIT_REQUESTED is delivered there and a handler + may sapp_cancel_quit() */ + _sapp_tc.app.quit_requested = true; + } else if (w->desc.close_cb) { + /* user closed a secondary window: notify; the app accepts by + calling sapp_destroy_window() (w may be freed after this) */ + sapp_window handle = { w->win_id }; + w->desc.close_cb(handle, w->desc.user_data); + } + } + } else if (!_sapp_tc.app.drop_enabled) { + return; + } else if (event->xclient.message_type == _sapp_tc.xdnd.XdndEnter) { + const bool is_list = 0 != (event->xclient.data.l[1] & 1); + _sapp_tc.xdnd.source = (Window)event->xclient.data.l[0]; + _sapp_tc.xdnd.version = (int)(event->xclient.data.l[1] >> 24); + _sapp_tc.xdnd.format = None; + _sapp_tc.xdnd.over = w; + if (_sapp_tc.xdnd.version > _SAPP_TC_XDND_VERSION) { + return; + } + uint32_t count = 0; + Atom* formats = 0; + if (is_list) { + count = (uint32_t)_sapp_tc_x11_get_window_property(_sapp_tc.xdnd.source, + _sapp_tc.xdnd.XdndTypeList, XA_ATOM, (unsigned char**)&formats); + } else { + count = 3; + formats = (Atom*)event->xclient.data.l + 2; + } + for (uint32_t i = 0; i < count; i++) { + if (formats[i] == _sapp_tc.xdnd.text_uri_list) { + _sapp_tc.xdnd.format = _sapp_tc.xdnd.text_uri_list; + break; + } + } + if (is_list && formats) { + XFree(formats); + } + } else if (event->xclient.message_type == _sapp_tc.xdnd.XdndDrop) { + if (_sapp_tc.xdnd.version > _SAPP_TC_XDND_VERSION) { + return; + } + Time time = CurrentTime; + if (_sapp_tc.xdnd.format) { + if (_sapp_tc.xdnd.version >= 1) { + time = (Time)event->xclient.data.l[2]; + } + XConvertSelection(_sapp_tc.display, _sapp_tc.xdnd.XdndSelection, + _sapp_tc.xdnd.format, _sapp_tc.xdnd.XdndSelection, + event->xclient.window, time); + } else if (_sapp_tc.xdnd.version >= 2) { + XEvent reply; + memset(&reply, 0, sizeof(reply)); + reply.type = ClientMessage; + reply.xclient.window = _sapp_tc.xdnd.source; + reply.xclient.message_type = _sapp_tc.xdnd.XdndFinished; + reply.xclient.format = 32; + reply.xclient.data.l[0] = (long)event->xclient.window; + reply.xclient.data.l[1] = 0; /* drag was rejected */ + reply.xclient.data.l[2] = None; + XSendEvent(_sapp_tc.display, _sapp_tc.xdnd.source, False, NoEventMask, &reply); + XFlush(_sapp_tc.display); + } + } else if (event->xclient.message_type == _sapp_tc.xdnd.XdndPosition) { + if (_sapp_tc.xdnd.version > _SAPP_TC_XDND_VERSION) { + return; + } + _sapp_tc.xdnd.over = w; + XEvent reply; + memset(&reply, 0, sizeof(reply)); + reply.type = ClientMessage; + reply.xclient.window = _sapp_tc.xdnd.source; + reply.xclient.message_type = _sapp_tc.xdnd.XdndStatus; + reply.xclient.format = 32; + reply.xclient.data.l[0] = (long)event->xclient.window; + if (_sapp_tc.xdnd.format) { + reply.xclient.data.l[1] = 1; /* accept with no rectangle */ + if (_sapp_tc.xdnd.version >= 2) { + reply.xclient.data.l[4] = (long)_sapp_tc.xdnd.XdndActionCopy; + } + } + XSendEvent(_sapp_tc.display, _sapp_tc.xdnd.source, False, NoEventMask, &reply); + XFlush(_sapp_tc.display); + } +} + +/*-- GLX bring-up ---------------------------------------------------------------*/ +static bool _sapp_tc_glx_ext_supported(const char* ext, const char* extensions) { + if (!ext || !extensions) return false; + const char* start = extensions; + const size_t len = strlen(ext); + for (;;) { + const char* at = strstr(start, ext); + if (!at) return false; + const char* terminator = at + len; + if ((at == extensions || *(at - 1) == ' ') && + (*terminator == ' ' || *terminator == '\0')) { + return true; + } + start = terminator; + } +} + +typedef void (*_sapp_tc_glx_voidfn_t)(void); +static _sapp_tc_glx_voidfn_t _sapp_tc_glx_getprocaddr(const char* name) { + return (_sapp_tc_glx_voidfn_t)glXGetProcAddressARB((const GLubyte*)name); +} + +static bool _sapp_tc_glx_init(void) { + int error_base = 0, event_base = 0; + if (!glXQueryExtension(_sapp_tc.display, &error_base, &event_base)) { + fprintf(stderr, "sokol_app_tc.h: GLX extension not present on the X server\n"); + return false; + } + int major = 0, minor = 0; + if (!glXQueryVersion(_sapp_tc.display, &major, &minor)) { + fprintf(stderr, "sokol_app_tc.h: glXQueryVersion() failed\n"); + return false; + } + if ((major < 1) || ((major == 1) && (minor < 3))) { + fprintf(stderr, "sokol_app_tc.h: GLX version 1.3 or higher required\n"); + return false; + } + const char* exts = glXQueryExtensionsString(_sapp_tc.display, _sapp_tc.screen); + if (_sapp_tc_glx_ext_supported("GLX_EXT_swap_control", exts)) { + _sapp_tc.SwapIntervalEXT = (void (*)(Display*, GLXDrawable, int)) + _sapp_tc_glx_getprocaddr("glXSwapIntervalEXT"); + } + if (_sapp_tc_glx_ext_supported("GLX_MESA_swap_control", exts)) { + _sapp_tc.SwapIntervalMESA = (int (*)(int)) + _sapp_tc_glx_getprocaddr("glXSwapIntervalMESA"); + } + if (_sapp_tc_glx_ext_supported("GLX_ARB_create_context", exts) && + _sapp_tc_glx_ext_supported("GLX_ARB_create_context_profile", exts)) { + _sapp_tc.CreateContextAttribsARB = + (GLXContext (*)(Display*, GLXFBConfig, GLXContext, Bool, const int*)) + _sapp_tc_glx_getprocaddr("glXCreateContextAttribsARB"); + } + if (!_sapp_tc.CreateContextAttribsARB) { + fprintf(stderr, "sokol_app_tc.h: GLX_ARB_create_context(_profile) required\n"); + return false; + } + return true; +} + +static int _sapp_tc_glx_attrib(GLXFBConfig cfg, int attrib) { + int value = 0; + glXGetFBConfigAttrib(_sapp_tc.display, cfg, attrib, &value); + return value; +} + +/* pick ONE fbconfig for the whole app (shared context; every window's visual + must match it): RGBA8, >= depth24/stencil8, doublebuffered, closest MSAA */ +static bool _sapp_tc_glx_choose_fbconfig(void) { + int count = 0; + GLXFBConfig* configs = glXGetFBConfigs(_sapp_tc.display, _sapp_tc.screen, &count); + if (!configs || (count == 0)) { + if (configs) XFree(configs); + return false; + } + const int want_samples = (_sapp_tc.app.desc.sample_count > 1) ? _sapp_tc.app.desc.sample_count : 0; + int best = -1; + int best_score = INT_MAX; + for (int i = 0; i < count; i++) { + if (!(_sapp_tc_glx_attrib(configs[i], GLX_RENDER_TYPE) & GLX_RGBA_BIT)) continue; + if (!(_sapp_tc_glx_attrib(configs[i], GLX_DRAWABLE_TYPE) & GLX_WINDOW_BIT)) continue; + if (!_sapp_tc_glx_attrib(configs[i], GLX_DOUBLEBUFFER)) continue; + if (_sapp_tc_glx_attrib(configs[i], GLX_RED_SIZE) != 8) continue; + if (_sapp_tc_glx_attrib(configs[i], GLX_GREEN_SIZE) != 8) continue; + if (_sapp_tc_glx_attrib(configs[i], GLX_BLUE_SIZE) != 8) continue; + if (_sapp_tc_glx_attrib(configs[i], GLX_ALPHA_SIZE) != 8) continue; + const int depth = _sapp_tc_glx_attrib(configs[i], GLX_DEPTH_SIZE); + const int stencil = _sapp_tc_glx_attrib(configs[i], GLX_STENCIL_SIZE); + if ((depth < 24) || (stencil < 8)) continue; + const int samples = _sapp_tc_glx_attrib(configs[i], GLX_SAMPLES); + int sample_diff = samples - want_samples; + if (sample_diff < 0) sample_diff = -sample_diff; + const int score = sample_diff * 100 + (depth - 24) + (stencil - 8); + if (score < best_score) { + best_score = score; + best = i; + } + } + bool ok = false; + if (best >= 0) { + _sapp_tc.fbconfig = configs[best]; + /* report the ACTUAL sample count (a request the hardware can't satisfy + degrades to the closest supported config) */ + const int got_samples = _sapp_tc_glx_attrib(configs[best], GLX_SAMPLES); + _sapp_tc.app.desc.sample_count = (got_samples > 1) ? got_samples : 1; + ok = true; + } else { + fprintf(stderr, "sokol_app_tc.h: no suitable GLXFBConfig found\n"); + } + XFree(configs); + return ok; +} + +static bool _sapp_tc_glx_create_context(void) { + int gl_major = _sapp_tc.app.desc.gl.major_version; + int gl_minor = _sapp_tc.app.desc.gl.minor_version; + if (gl_major == 0) { + gl_major = 4; /* upstream Linux GLCORE default: 4.3 core */ + gl_minor = 3; + } + const int attribs[] = { + GLX_CONTEXT_MAJOR_VERSION_ARB, gl_major, + GLX_CONTEXT_MINOR_VERSION_ARB, gl_minor, + GLX_CONTEXT_PROFILE_MASK_ARB, GLX_CONTEXT_CORE_PROFILE_BIT_ARB, + GLX_CONTEXT_FLAGS_ARB, GLX_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB, + None + }; + _sapp_tc_x11_grab_error_handler(); + _sapp_tc.ctx = _sapp_tc.CreateContextAttribsARB(_sapp_tc.display, _sapp_tc.fbconfig, + NULL, True, attribs); + _sapp_tc_x11_release_error_handler(); + if (!_sapp_tc.ctx) { + fprintf(stderr, "sokol_app_tc.h: failed to create GL %d.%d core context\n", gl_major, gl_minor); + return false; + } + return true; +} + +static void _sapp_tc_glx_set_swapinterval(_sapp_tc_window_t* w, int interval) { + /* GLX_EXT_swap_control is per-drawable: each window gets its own interval. + The MESA fallback is context-wide -- with more than one window it is set + to 0 (never block) and the main tick self-heals to timer pacing. */ + if (_sapp_tc.SwapIntervalEXT) { + _sapp_tc.SwapIntervalEXT(_sapp_tc.display, w->glx_win, interval); + } else if (_sapp_tc.SwapIntervalMESA) { + _sapp_tc.SwapIntervalMESA(interval); + } +} + +/*-- window creation / destruction ---------------------------------------------*/ +static bool _sapp_tc_x11_create_native_window(_sapp_tc_window_t* w, const char* title, + int width_pt, int height_pt, bool borderless) { + XVisualInfo* vi = glXGetVisualFromFBConfig(_sapp_tc.display, _sapp_tc.fbconfig); + if (!vi) { + return false; + } + w->colormap = XCreateColormap(_sapp_tc.display, _sapp_tc.root, vi->visual, AllocNone); + XSetWindowAttributes wa; + memset(&wa, 0, sizeof(wa)); + const unsigned long wamask = CWBorderPixel | CWColormap | CWEventMask; + wa.colormap = w->colormap; + wa.border_pixel = 0; + wa.event_mask = StructureNotifyMask | KeyPressMask | KeyReleaseMask | + PointerMotionMask | ButtonPressMask | ButtonReleaseMask | + ExposureMask | FocusChangeMask | VisibilityChangeMask | + EnterWindowMask | LeaveWindowMask | PropertyChangeMask; + /* NOTE: sizing uses the same scale that dimension reporting divides by */ + const float scale = (w->dpi_scale > 0.0f) ? w->dpi_scale : 1.0f; + int px_w = (int)((float)width_pt * scale + 0.5f); + int px_h = (int)((float)height_pt * scale + 0.5f); + if (width_pt <= 0) { + px_w = (DisplayWidth(_sapp_tc.display, _sapp_tc.screen) * 4) / 5; + } + if (height_pt <= 0) { + px_h = (DisplayHeight(_sapp_tc.display, _sapp_tc.screen) * 4) / 5; + } + _sapp_tc_x11_grab_error_handler(); + w->xwin = XCreateWindow(_sapp_tc.display, _sapp_tc.root, + 0, 0, (unsigned int)px_w, (unsigned int)px_h, + 0, /* border width */ + vi->depth, /* color depth */ + InputOutput, vi->visual, wamask, &wa); + _sapp_tc_x11_release_error_handler(); + XFree(vi); + if (!w->xwin) { + return false; + } + Atom protocols[] = { _sapp_tc.WM_DELETE_WINDOW }; + XSetWMProtocols(_sapp_tc.display, w->xwin, protocols, 1); + /* NOTE: PPosition and PSize are obsolete and ignored */ + XSizeHints* hints = XAllocSizeHints(); + if (hints) { + hints->flags = PWinGravity; + hints->win_gravity = CenterGravity; + XSetWMNormalHints(_sapp_tc.display, w->xwin, hints); + XFree(hints); + } + if (borderless) { + _sapp_tc_x11_set_decorated(w, false); + } + /* announce XDND support */ + if (_sapp_tc.app.drop_enabled) { + const Atom version = _SAPP_TC_XDND_VERSION; + XChangeProperty(_sapp_tc.display, w->xwin, _sapp_tc.xdnd.XdndAware, XA_ATOM, + 32, PropModeReplace, (unsigned char*)&version, 1); + } + _sapp_tc_x11_update_window_title(w, title ? title : "TrussC"); + /* GLX drawable wrapping the X window (the per-window object; the context + is shared) */ + _sapp_tc_x11_grab_error_handler(); + w->glx_win = glXCreateWindow(_sapp_tc.display, _sapp_tc.fbconfig, w->xwin, NULL); + _sapp_tc_x11_release_error_handler(); + if (!w->glx_win) { + return false; + } + _sapp_tc_timing_reset(&w->timing); + return true; +} + +static void _sapp_tc_x11_show_window(_sapp_tc_window_t* w) { + XMapWindow(_sapp_tc.display, w->xwin); + XEvent dummy; + _sapp_tc_x11_wait_for_event(w->xwin, VisibilityNotify, 0.1, &dummy); + XRaiseWindow(_sapp_tc.display, w->xwin); + XFlush(_sapp_tc.display); +} + +static void _sapp_tc_x11_destroy_window_resources(_sapp_tc_window_t* w) { + if (w->glx_win) { + /* never destroy the drawable that is current */ + if (glXGetCurrentDrawable() == w->glx_win) { + glXMakeCurrent(_sapp_tc.display, None, NULL); + } + glXDestroyWindow(_sapp_tc.display, w->glx_win); + w->glx_win = 0; + } + if (w->xwin) { + XUnmapWindow(_sapp_tc.display, w->xwin); + XDestroyWindow(_sapp_tc.display, w->xwin); + w->xwin = 0; + } + if (w->colormap) { + XFreeColormap(_sapp_tc.display, w->colormap); + w->colormap = 0; + } + XFlush(_sapp_tc.display); +} + +/*-- event dispatch ---------------------------------------------------------------*/ +static void _sapp_tc_x11_process_event(XEvent* ev) { + _sapp_tc_window_t* w = _sapp_tc_lookup_xwin(ev->xany.window); + switch (ev->type) { + case FocusIn: + /* ignore grab-cycle focus notifications (GLFW parity) */ + if (w && (ev->xfocus.mode != NotifyGrab) && (ev->xfocus.mode != NotifyUngrab)) { + _sapp_tc_x11_app_event(w, SAPP_EVENTTYPE_FOCUSED); + } + break; + case FocusOut: + if (w && (ev->xfocus.mode != NotifyGrab) && (ev->xfocus.mode != NotifyUngrab)) { + _sapp_tc_x11_app_event(w, SAPP_EVENTTYPE_UNFOCUSED); + } + break; + case KeyPress: { + if (!w) break; + const int keycode = (int)ev->xkey.keycode; + const sapp_keycode key = _sapp_tc_translate_key(keycode); + const bool repeat = _sapp_tc_x11_keypress_repeat(w, keycode); + uint32_t mods = _sapp_tc_x11_mods(ev->xkey.state); + mods |= _sapp_tc_x11_key_modifier_bit(key); + _sapp_tc_x11_key_event(w, SAPP_EVENTTYPE_KEY_DOWN, key, repeat, mods); + KeySym keysym = 0; + XLookupString(&ev->xkey, NULL, 0, &keysym, NULL); + const int32_t chr = _sapp_tc_x11_keysym_to_unicode(keysym); + if (chr > 0) { + _sapp_tc_x11_char_event(w, (uint32_t)chr, repeat, mods); + } + break; + } + case KeyRelease: { + if (!w) break; + const int keycode = (int)ev->xkey.keycode; + const sapp_keycode key = _sapp_tc_translate_key(keycode); + _sapp_tc_x11_keyrelease_repeat(w, keycode); + uint32_t mods = _sapp_tc_x11_mods(ev->xkey.state); + mods &= ~_sapp_tc_x11_key_modifier_bit(key); + _sapp_tc_x11_key_event(w, SAPP_EVENTTYPE_KEY_UP, key, false, mods); + break; + } + case ButtonPress: { + if (!w) break; + _sapp_tc_x11_mouse_update(w, ev->xbutton.x, ev->xbutton.y, false); + uint32_t mods = _sapp_tc_x11_mods(ev->xbutton.state); + const unsigned int xbtn = ev->xbutton.button; + if (xbtn == Button4) { _sapp_tc_x11_scroll_event(w, 0.0f, 1.0f, mods); } + else if (xbtn == Button5) { _sapp_tc_x11_scroll_event(w, 0.0f, -1.0f, mods); } + else if (xbtn == 6) { _sapp_tc_x11_scroll_event(w, 1.0f, 0.0f, mods); } + else if (xbtn == 7) { _sapp_tc_x11_scroll_event(w, -1.0f, 0.0f, mods); } + else { + const sapp_mousebutton btn = _sapp_tc_x11_translate_button(xbtn); + if (btn != SAPP_MOUSEBUTTON_INVALID) { + mods |= _sapp_tc_x11_button_modifier_bit(btn); + w->mouse_buttons |= (uint8_t)(1 << btn); + _sapp_tc_x11_mouse_event(w, SAPP_EVENTTYPE_MOUSE_DOWN, btn, mods); + } + } + break; + } + case ButtonRelease: { + if (!w) break; + _sapp_tc_x11_mouse_update(w, ev->xbutton.x, ev->xbutton.y, false); + const sapp_mousebutton btn = _sapp_tc_x11_translate_button(ev->xbutton.button); + if (btn != SAPP_MOUSEBUTTON_INVALID) { + uint32_t mods = _sapp_tc_x11_mods(ev->xbutton.state); + mods &= ~_sapp_tc_x11_button_modifier_bit(btn); + w->mouse_buttons &= (uint8_t)~(1 << btn); + _sapp_tc_x11_mouse_event(w, SAPP_EVENTTYPE_MOUSE_UP, btn, mods); + } + break; + } + case EnterNotify: + if (w) { + _sapp_tc_x11_mouse_update(w, ev->xcrossing.x, ev->xcrossing.y, true); + /* suppressed while a button is held (upstream parity) */ + if (w->mouse_buttons == 0) { + _sapp_tc_x11_mouse_event(w, SAPP_EVENTTYPE_MOUSE_ENTER, + SAPP_MOUSEBUTTON_INVALID, _sapp_tc_x11_mods(ev->xcrossing.state)); + } + } + break; + case LeaveNotify: + if (w && (w->mouse_buttons == 0)) { + _sapp_tc_x11_mouse_event(w, SAPP_EVENTTYPE_MOUSE_LEAVE, + SAPP_MOUSEBUTTON_INVALID, _sapp_tc_x11_mods(ev->xcrossing.state)); + } + break; + case MotionNotify: + if (w) { + _sapp_tc_x11_mouse_update(w, ev->xmotion.x, ev->xmotion.y, false); + _sapp_tc_x11_mouse_event(w, SAPP_EVENTTYPE_MOUSE_MOVE, + SAPP_MOUSEBUTTON_INVALID, _sapp_tc_x11_mods(ev->xmotion.state)); + } + break; + case PropertyNotify: + /* iconify/restore detection (there is no map/unmap handling) */ + if (w && (ev->xproperty.state == PropertyNewValue) && + (ev->xproperty.atom == _sapp_tc.WM_STATE)) { + const int state = _sapp_tc_x11_get_window_state(w); + if ((state == IconicState) && !w->iconified) { + w->iconified = true; + _sapp_tc_x11_app_event(w, SAPP_EVENTTYPE_ICONIFIED); + } else if ((state == NormalState) && w->iconified) { + w->iconified = false; + w->earliest_next = 0.0; /* resume promptly */ + _sapp_tc_x11_app_event(w, SAPP_EVENTTYPE_RESTORED); + } + } + break; + case VisibilityNotify: + /* occlusion gate for secondary windows: a fully covered window + stops rendering (the main window keeps upstream's behavior) */ + if (w && !w->is_main) { + const bool obscured = (ev->xvisibility.state == VisibilityFullyObscured); + if (obscured != w->occluded) { + w->occluded = obscured; + _sapp_tc_x11_app_event(w, obscured ? SAPP_EVENTTYPE_SUSPENDED + : SAPP_EVENTTYPE_RESUMED); + if (!obscured) { + w->earliest_next = 0.0; + } + } + } + break; + case SelectionNotify: + _sapp_tc_x11_on_selectionnotify(ev); + break; + case SelectionRequest: + _sapp_tc_x11_serve_selection_request(ev); + break; + case ClientMessage: + _sapp_tc_x11_on_clientmessage(ev); + break; + case DestroyNotify: + /* not a bug (upstream parity) */ + break; + default: + break; + } +} + +/*-- pacing / tick ------------------------------------------------------------------*/ +static void _sapp_tc_x11_pace(_sapp_tc_window_t* w, double seconds) { + w->earliest_next = _sapp_tc_now() + seconds; +} + +/* absolute periodic schedule (timer-paced secondary windows): pacing from the + previous deadline instead of "now" keeps the average rate at the period + despite tick jitter; resync when far off (first tick, after occlusion) */ +static void _sapp_tc_x11_pace_periodic(_sapp_tc_window_t* w, double period) { + const double now = _sapp_tc_now(); + double next = w->earliest_next + period; + if ((next <= now) || (next > now + 2.0 * period)) { + next = now + period; + } + w->earliest_next = next; +} + +static bool _sapp_tc_x11_window_due(_sapp_tc_window_t* w, double now) { + if (!w || !w->ready || !w->glx_win || w->in_tick) return false; + return w->earliest_next <= now; +} + +static void _sapp_tc_x11_tick(_sapp_tc_window_t* w) { + if (w->in_tick || !w->glx_win) return; + _sapp_tc_timing_update(&w->timing); + w->frame_duration = w->timing.smooth_dt; + const int swap_interval = (_sapp_tc.app.desc.swap_interval > 0) ? _sapp_tc.app.desc.swap_interval : 1; + + if (w->is_main) { + /* resize is polled here every frame (upstream parity: the X11 backend + has no ConfigureNotify handler; this poll is the size authority) */ + if (_sapp_tc_x11_update_dimensions(w) && _sapp_tc.app.init_called) { + _sapp_tc_x11_app_event(w, SAPP_EVENTTYPE_RESIZED); + } + glXMakeCurrent(_sapp_tc.display, w->glx_win, _sapp_tc.ctx); + w->in_tick = true; + if (!_sapp_tc.app.init_called) { + _sapp_tc.app.init_called = true; + if (_sapp_tc.app.desc.init_cb) { + _sapp_tc.app.desc.init_cb(); + } else if (_sapp_tc.app.desc.init_userdata_cb) { + _sapp_tc.app.desc.init_userdata_cb(_sapp_tc.app.desc.user_data); + } + } + if (_sapp_tc.app.desc.frame_cb) { + _sapp_tc.app.desc.frame_cb(); + } else if (_sapp_tc.app.desc.frame_userdata_cb) { + _sapp_tc.app.desc.frame_userdata_cb(_sapp_tc.app.desc.user_data); + } + _sapp_tc.app.frame_count++; + w->in_tick = false; + bool presented = false; + if (_sapp_tc.app.skip_present) { + /* one-shot event-driven present suppression (TrussC patch: keeps + the last image on screen when a frame decides not to draw) */ + _sapp_tc.app.skip_present = false; + } else if (!w->iconified) { + /* with swap interval >= 1 this call BLOCKS until vsync -- that + blocking swap is the run loop's pacing (upstream parity) */ + glXSwapBuffers(_sapp_tc.display, w->glx_win); + presented = true; + } + const double now = _sapp_tc_now(); + if (w->iconified) { + /* minimized: frame_cb keeps running, throttled (win32 parity) */ + _sapp_tc_x11_pace(w, 0.0166 * (double)swap_interval); + } else if (presented) { + const double swap_dt = now - w->last_present; + w->last_present = now; + if (swap_dt > 0.002) { + /* the swap blocked (vsync paces the loop); refine the refresh + estimate that paces the secondary windows */ + const double per_frame = swap_dt / (double)swap_interval; + if ((per_frame > (1.0 / 240.0)) && (per_frame < (1.0 / 30.0))) { + _sapp_tc.refresh_period += 0.1 * (per_frame - _sapp_tc.refresh_period); + } + w->earliest_next = 0.0; + } else { + /* the swap returned immediately (unmapped/unredirected window, + MESA context-wide interval 0, driver without vsync): fall + back to timer pacing so the loop can never busy-spin */ + _sapp_tc_x11_pace_periodic(w, _sapp_tc.refresh_period * (double)swap_interval); + } + } else { + _sapp_tc_x11_pace(w, _sapp_tc.refresh_period * (double)swap_interval); + } + } else { + /* secondary window: swap interval 0 (vsyncing N drawables would + serialize the loop to refresh/N), timer-paced to the measured + refresh estimate */ + if (w->iconified || w->occluded) { + _sapp_tc_x11_pace(w, _sapp_tc.refresh_period); + return; + } + if (_sapp_tc_x11_update_dimensions(w)) { + _sapp_tc_x11_app_event(w, SAPP_EVENTTYPE_RESIZED); + } + glXMakeCurrent(_sapp_tc.display, w->glx_win, _sapp_tc.ctx); + w->in_tick = true; + if (w->desc.tick_cb) { + sapp_window handle = { w->win_id }; + w->desc.tick_cb(handle, w->desc.user_data); + } + w->in_tick = false; + glXSwapBuffers(_sapp_tc.display, w->glx_win); + _sapp_tc_x11_pace_periodic(w, _sapp_tc.refresh_period); + } +} + +/*-- the run loop -------------------------------------------------------------------- + While the main window presents normally, its blocking vsync'd swap paces + the loop and the XPending drain runs once per frame (upstream parity). + When every window is timer-paced (minimized, skip_present), the loop + sleeps in poll() on the X connection fd until the next deadline or an + incoming event, so the process never busy-spins. */ +static void _sapp_tc_x11_run_loop(void) { + XFlush(_sapp_tc.display); + while (!_sapp_tc.app.quit_ordered) { + double now = _sapp_tc_now(); + double wake_at = now + 0.1; /* robustness cap */ + bool any_due = false; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* w = _sapp_tc.windows[i]; + if (!w || !w->ready || !w->glx_win || w->in_tick) continue; + if (w->earliest_next > now) { + if (w->earliest_next < wake_at) wake_at = w->earliest_next; + } else { + any_due = true; + } + } + if (!any_due && !XPending(_sapp_tc.display)) { + struct pollfd pfd; + pfd.fd = ConnectionNumber(_sapp_tc.display); + pfd.events = POLLIN; + pfd.revents = 0; + const double timeout_s = wake_at - now; + poll(&pfd, 1, (timeout_s > 0.0) ? (int)(timeout_s * 1000.0) + 1 : 0); + } + /* drain exactly the already-queued events (can't block: count bounded) */ + int count = XPending(_sapp_tc.display); + while (count--) { + XEvent event; + XNextEvent(_sapp_tc.display, &event); + _sapp_tc_x11_process_event(&event); + } + /* tick every due window (re-fetch each slot: a tick or a processed + event may have destroyed windows) */ + now = _sapp_tc_now(); + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* w = _sapp_tc.windows[i]; + if (_sapp_tc_x11_window_due(w, now)) { + _sapp_tc_x11_tick(w); + } + } + XFlush(_sapp_tc.display); + /* the cancellable quit dance (upstream parity): WM_DELETE_WINDOW or + sapp_request_quit() land here; sapp_quit() pre-sets quit_ordered */ + if (_sapp_tc.app.quit_requested && !_sapp_tc.app.quit_ordered) { + if (_sapp_tc.app.main) { + _sapp_tc_x11_app_event(_sapp_tc.app.main, SAPP_EVENTTYPE_QUIT_REQUESTED); + } + if (_sapp_tc.app.quit_requested) { + _sapp_tc.app.quit_ordered = true; + } + } + } +} + +/*-- main window creation --------------------------------------------------------*/ +static bool _sapp_tc_x11_create_main_window(void) { + const sapp_desc* d = &_sapp_tc.app.desc; + int slot = -1; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + if (_sapp_tc.windows[i] == 0) { slot = i; break; } + } + if (slot < 0) return false; + + _sapp_tc_window_t* w = new _sapp_tc_window_t(); + w->win_id = ++_sapp_tc.next_id; + w->is_main = true; + w->dpi_scale = d->high_dpi ? (_sapp_tc.dpi / 96.0f) : 1.0f; + if (w->dpi_scale <= 0.0f) w->dpi_scale = 1.0f; + + /* per-window desc: the app callbacks are reached via the trampoline */ + w->desc.width = d->width; + w->desc.height = d->height; + w->desc.sample_count = d->sample_count; + w->desc.no_high_dpi = !d->high_dpi; + w->desc.event_cb = _sapp_tc_main_event_tramp; + + if (!_sapp_tc_x11_create_native_window(w, _sapp_tc.app.window_title, + d->width, d->height, false)) { + _sapp_tc_x11_destroy_window_resources(w); + delete w; + return false; + } + _sapp_tc.windows[slot] = w; + _sapp_tc.app.main = w; + w->ready = true; + _sapp_tc_x11_update_dimensions(w); /* silent startup sizing (no RESIZED) */ + glXMakeCurrent(_sapp_tc.display, w->glx_win, _sapp_tc.ctx); + { + /* capture the default-framebuffer id sapp_get_swapchain() reports */ + GLint fb = 0; + glGetIntegerv(GL_FRAMEBUFFER_BINDING, &fb); + _sapp_tc.gl_framebuffer = (uint32_t)fb; + } + _sapp_tc_glx_set_swapinterval(w, (d->swap_interval > 0) ? d->swap_interval : 1); + _sapp_tc_x11_show_window(w); + if (d->fullscreen) { + /* must be after XMapWindow */ + _sapp_tc_x11_set_fullscreen(true); + _sapp_tc_x11_update_dimensions(w); + } + _sapp_tc.app.valid = true; + return true; +} + +/*-- public API --------------------------------------------------------------------*/ +extern "C" { + +sapp_window sapp_create_window(const sapp_window_desc* desc) { + sapp_window invalid = { 0 }; + if (!desc) return invalid; + if (!_sapp_tc.app.valid || !_sapp_tc.ctx) return invalid; + + int slot = -1; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + if (_sapp_tc.windows[i] == 0) { slot = i; break; } + } + if (slot < 0) return invalid; + + _sapp_tc_window_t* w = new _sapp_tc_window_t(); + w->win_id = ++_sapp_tc.next_id; + w->desc = *desc; + if (w->desc.width <= 0) w->desc.width = 640; + if (w->desc.height <= 0) w->desc.height = 480; + /* all windows share ONE fbconfig/visual/context: the MSAA sample count is + the one negotiated at startup, not per-window */ + w->desc.sample_count = _sapp_tc.app.desc.sample_count; + w->dpi_scale = w->desc.no_high_dpi ? 1.0f : (_sapp_tc.dpi / 96.0f); + if (w->dpi_scale <= 0.0f) w->dpi_scale = 1.0f; + + if (!_sapp_tc_x11_create_native_window(w, w->desc.title, + w->desc.width, w->desc.height, w->desc.borderless)) { + _sapp_tc_x11_destroy_window_resources(w); + delete w; + return invalid; + } + /* never vsync a second drawable (it would serialize the run loop to + refresh/N); this window is timer-paced instead. Requires a current + context (there always is one while the app runs). With only the + context-wide MESA fallback this turns vsync off for the WHOLE context + and the main tick self-heals to timer pacing. */ + _sapp_tc_glx_set_swapinterval(w, 0); + _sapp_tc.windows[slot] = w; + w->ready = true; + _sapp_tc_x11_update_dimensions(w); + _sapp_tc_x11_show_window(w); + sapp_window handle = { w->win_id }; + return handle; +} + +void sapp_destroy_window(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w || w->is_main) return; /* the main window is owned by sapp_run */ + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + if (_sapp_tc.windows[i] == w) { _sapp_tc.windows[i] = 0; break; } + } + if (_sapp_tc.xdnd.over == w) _sapp_tc.xdnd.over = 0; + w->desc.close_cb = 0; + _sapp_tc_x11_destroy_window_resources(w); + delete w; + /* restore the main window's drawable if the destroy released it */ + if (!glXGetCurrentContext() && _sapp_tc.app.main && _sapp_tc.app.main->glx_win) { + glXMakeCurrent(_sapp_tc.display, _sapp_tc.app.main->glx_win, _sapp_tc.ctx); + } +} + +bool sapp_window_valid(sapp_window win) { + return _sapp_tc_lookup(win) != 0; +} + +int sapp_window_width(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->win_width : 0; +} + +int sapp_window_height(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->win_height : 0; +} + +int sapp_window_framebuffer_width(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->fb_width : 0; +} + +int sapp_window_framebuffer_height(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->fb_height : 0; +} + +float sapp_window_dpi_scale(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->dpi_scale : 1.0f; +} + +int sapp_window_sample_count(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->desc.sample_count : 1; +} + +bool sapp_window_occluded(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->occluded : false; +} + +void sapp_window_set_title(sapp_window win, const char* title) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w || !w->xwin || !title) return; + _sapp_tc_x11_update_window_title(w, title); +} + +double sapp_window_frame_duration(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w) return 1.0 / 60.0; + return (w->frame_duration > 0.0) ? w->frame_duration : _sapp_tc.refresh_period; +} + +/* Metal / D3D11 handles do not exist on Linux */ +const void* sapp_window_metal_current_drawable(sapp_window win) { (void)win; return 0; } +const void* sapp_window_metal_depth_stencil_texture(sapp_window win) { (void)win; return 0; } +const void* sapp_window_metal_msaa_color_texture(sapp_window win) { (void)win; return 0; } +const void* sapp_window_macos_get_window(sapp_window win) { (void)win; return 0; } +const void* sapp_window_d3d11_render_view(sapp_window win) { (void)win; return 0; } +const void* sapp_window_d3d11_resolve_view(sapp_window win) { (void)win; return 0; } +const void* sapp_window_d3d11_depth_stencil_view(sapp_window win) { (void)win; return 0; } +const void* sapp_window_win32_get_hwnd(sapp_window win) { (void)win; return 0; } + +uint32_t sapp_window_gl_framebuffer(sapp_window win) { + /* the GL "swapchain" is just the default framebuffer of whatever drawable + is current -- which is this window's during its tick_cb */ + (void)win; + return _sapp_tc.gl_framebuffer; +} + +const void* sapp_window_x11_get_window(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? (const void*)(uintptr_t)w->xwin : 0; +} + +/*-- sokol_app.h public API (Linux implementation lives here) ---------------------*/ + +void sapp_run(const sapp_desc* desc) { + if (!desc) return; + _sapp_tc.app.desc = *desc; + sapp_desc* d = &_sapp_tc.app.desc; + if (d->sample_count <= 0) d->sample_count = 1; + if (d->swap_interval <= 0) d->swap_interval = 1; + if (d->clipboard_size <= 0) d->clipboard_size = 8192; + if (d->max_dropped_files <= 0) d->max_dropped_files = 1; + if (d->max_dropped_file_path_length <= 0) d->max_dropped_file_path_length = 2048; + strncpy(_sapp_tc.app.window_title, d->window_title ? d->window_title : "sokol", + sizeof(_sapp_tc.app.window_title) - 1); + d->window_title = _sapp_tc.app.window_title; + if (d->enable_clipboard) { + _sapp_tc.app.clipboard_enabled = true; + _sapp_tc.app.clipboard_size = d->clipboard_size; + _sapp_tc.app.clipboard = (char*)calloc(1, (size_t)d->clipboard_size); + } + if (d->enable_dragndrop) { + _sapp_tc.app.drop_enabled = true; + _sapp_tc.app.drop_max_files = d->max_dropped_files; + _sapp_tc.app.drop_max_path_length = d->max_dropped_file_path_length; + _sapp_tc.app.drop_buffer = (char*)calloc( + (size_t)d->max_dropped_files, (size_t)d->max_dropped_file_path_length); + } + _sapp_tc.app.mouse_shown = true; + _sapp_tc.app.current_cursor = SAPP_MOUSECURSOR_DEFAULT; + _sapp_tc.refresh_period = 1.0 / 60.0; + + /* XInitThreads before ANY other Xlib call (TrussC runs off-thread work) */ + XInitThreads(); + XrmInitialize(); + _sapp_tc.display = XOpenDisplay(NULL); + if (!_sapp_tc.display) { + fprintf(stderr, "sokol_app_tc.h: XOpenDisplay() failed (no X server / DISPLAY not set)\n"); + abort(); + } + _sapp_tc.screen = DefaultScreen(_sapp_tc.display); + _sapp_tc.root = DefaultRootWindow(_sapp_tc.display); + _sapp_tc_x11_query_system_dpi(); + _sapp_tc_x11_init_extensions(); + _sapp_tc_x11_init_cursors(); + /* detectable auto-repeat: held keys emit repeated KeyPress WITHOUT paired + KeyRelease -- required by the software key-repeat tracking */ + XkbSetDetectableAutoRepeat(_sapp_tc.display, True, NULL); + _sapp_tc_x11_init_keytable(); + + if (_sapp_tc_glx_init() && _sapp_tc_glx_choose_fbconfig() && _sapp_tc_glx_create_context()) { + if (_sapp_tc_x11_create_main_window()) { + _sapp_tc_x11_run_loop(); + } + } + + /* user cleanup runs BEFORE any GL/window teardown (the app shuts down + sokol_gfx here, which still needs a current context) */ + if (!_sapp_tc.app.cleanup_called) { + _sapp_tc.app.cleanup_called = true; + if (_sapp_tc.app.desc.cleanup_cb) { + _sapp_tc.app.desc.cleanup_cb(); + } else if (_sapp_tc.app.desc.cleanup_userdata_cb) { + _sapp_tc.app.desc.cleanup_userdata_cb(_sapp_tc.app.desc.user_data); + } + } + /* destroy any windows the app left open (secondary first, then main) */ + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* w = _sapp_tc.windows[i]; + if (!w || w->is_main) continue; + _sapp_tc.windows[i] = 0; + _sapp_tc_x11_destroy_window_resources(w); + delete w; + } + if (_sapp_tc.app.main) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + if (_sapp_tc.windows[i] == w) { _sapp_tc.windows[i] = 0; break; } + } + _sapp_tc.app.main = 0; + _sapp_tc_x11_destroy_window_resources(w); + delete w; + } + if (_sapp_tc.ctx) { + glXMakeCurrent(_sapp_tc.display, None, NULL); + glXDestroyContext(_sapp_tc.display, _sapp_tc.ctx); + _sapp_tc.ctx = 0; + } + _sapp_tc_x11_destroy_cursors(); + XCloseDisplay(_sapp_tc.display); + _sapp_tc.display = 0; + if (_sapp_tc.app.clipboard) { free(_sapp_tc.app.clipboard); _sapp_tc.app.clipboard = 0; } + if (_sapp_tc.app.drop_buffer) { free(_sapp_tc.app.drop_buffer); _sapp_tc.app.drop_buffer = 0; } + _sapp_tc.app.valid = false; +} + +bool sapp_isvalid(void) { + return _sapp_tc.app.valid; +} + +int sapp_width(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return (w && w->fb_width > 0) ? w->fb_width : 1; +} + +float sapp_widthf(void) { + return (float)sapp_width(); +} + +int sapp_height(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return (w && w->fb_height > 0) ? w->fb_height : 1; +} + +float sapp_heightf(void) { + return (float)sapp_height(); +} + +int sapp_sample_count(void) { + return _sapp_tc.app.desc.sample_count > 0 ? _sapp_tc.app.desc.sample_count : 1; +} + +bool sapp_high_dpi(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return _sapp_tc.app.desc.high_dpi && w && (w->dpi_scale >= 1.5f); +} + +float sapp_dpi_scale(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return w ? w->dpi_scale : 1.0f; +} + +uint64_t sapp_frame_count(void) { + return _sapp_tc.app.frame_count; +} + +double sapp_frame_duration(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w) return 1.0 / 60.0; + return (w->timing.smooth_dt > 0.0) ? w->timing.smooth_dt : _sapp_tc.refresh_period; +} + +void sapp_request_quit(void) { + _sapp_tc.app.quit_requested = true; +} + +void sapp_cancel_quit(void) { + _sapp_tc.app.quit_requested = false; +} + +void sapp_quit(void) { + _sapp_tc.app.quit_ordered = true; +} + +void sapp_consume_event(void) { + _sapp_tc.app.event_consumed = true; +} + +void sapp_skip_present(void) { + /* one-shot; HONORED on glXSwapBuffers (TrussC patch: event-driven present + suppression -- keeps the last image on screen, prevents flicker) */ + _sapp_tc.app.skip_present = true; +} + +void sapp_show_keyboard(bool show) { + (void)show; /* on-screen keyboard: mobile only */ +} + +bool sapp_keyboard_shown(void) { + return false; +} + +void sapp_show_mouse(bool show) { + if (_sapp_tc.app.mouse_shown != show) { + _sapp_tc.app.mouse_shown = show; + _sapp_tc_x11_apply_cursor(); + } +} + +bool sapp_mouse_shown(void) { + return _sapp_tc.app.mouse_shown; +} + +void sapp_set_mouse_cursor(sapp_mouse_cursor cursor) { + if ((int)cursor < 0 || (int)cursor >= _SAPP_MOUSECURSOR_NUM) return; + if (cursor != _sapp_tc.app.current_cursor) { + _sapp_tc.app.current_cursor = cursor; + _sapp_tc_x11_apply_cursor(); + } +} + +sapp_mouse_cursor sapp_get_mouse_cursor(void) { + return _sapp_tc.app.current_cursor; +} + +sapp_mouse_cursor sapp_bind_mouse_cursor_image(sapp_mouse_cursor cursor, const sapp_image_desc* desc) { + if ((int)cursor < 0 || (int)cursor >= _SAPP_MOUSECURSOR_NUM) return cursor; + if (!desc || !desc->pixels.ptr || desc->width <= 0 || desc->height <= 0) return cursor; + if (desc->pixels.size < (size_t)(desc->width * desc->height * 4)) return cursor; + sapp_unbind_mouse_cursor_image(cursor); + XcursorImage* img = XcursorImageCreate(desc->width, desc->height); + if (!img) return cursor; + img->xhot = (XcursorDim)desc->cursor_hotspot_x; + img->yhot = (XcursorDim)desc->cursor_hotspot_y; + /* Xcursor wants BGRA */ + const size_t num_bytes = (size_t)(desc->width * desc->height) * 4; + for (size_t i = 0; i < num_bytes; i += 4) { + ((uint8_t*)img->pixels)[i + 0] = ((const uint8_t*)desc->pixels.ptr)[i + 2]; + ((uint8_t*)img->pixels)[i + 1] = ((const uint8_t*)desc->pixels.ptr)[i + 1]; + ((uint8_t*)img->pixels)[i + 2] = ((const uint8_t*)desc->pixels.ptr)[i + 0]; + ((uint8_t*)img->pixels)[i + 3] = ((const uint8_t*)desc->pixels.ptr)[i + 3]; + } + Cursor c = XcursorImageLoadCursor(_sapp_tc.display, img); + XcursorImageDestroy(img); + if (!c) return cursor; + _sapp_tc.custom_cursors[cursor] = c; + _sapp_tc.app.custom_cursor_bound[cursor] = true; + if (_sapp_tc.app.current_cursor == cursor) { + _sapp_tc_x11_apply_cursor(); + } + return cursor; +} + +void sapp_unbind_mouse_cursor_image(sapp_mouse_cursor cursor) { + if ((int)cursor < 0 || (int)cursor >= _SAPP_MOUSECURSOR_NUM) return; + if (!_sapp_tc.app.custom_cursor_bound[cursor]) return; + _sapp_tc.app.custom_cursor_bound[cursor] = false; + Cursor c = _sapp_tc.custom_cursors[cursor]; + _sapp_tc.custom_cursors[cursor] = 0; + if (_sapp_tc.app.current_cursor == cursor) { + _sapp_tc_x11_apply_cursor(); + } + if (c) XFreeCursor(_sapp_tc.display, c); +} + +void sapp_set_clipboard_string(const char* str) { + if (!_sapp_tc.app.clipboard_enabled || !_sapp_tc.app.clipboard || !str) return; + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w) return; + if (strlen(str) >= (size_t)_sapp_tc.app.clipboard_size) { + return; /* string too big for the clipboard buffer */ + } + strncpy(_sapp_tc.app.clipboard, str, (size_t)_sapp_tc.app.clipboard_size - 1); + _sapp_tc.app.clipboard[_sapp_tc.app.clipboard_size - 1] = 0; + XSetSelectionOwner(_sapp_tc.display, _sapp_tc.CLIPBOARD, w->xwin, CurrentTime); + if (XGetSelectionOwner(_sapp_tc.display, _sapp_tc.CLIPBOARD) != w->xwin) { + _sapp_tc.app.clipboard[0] = 0; /* failed to become selection owner */ + } +} + +const char* sapp_get_clipboard_string(void) { + if (!_sapp_tc.app.clipboard_enabled || !_sapp_tc.app.clipboard) return ""; + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w) return ""; + if (XGetSelectionOwner(_sapp_tc.display, _sapp_tc.CLIPBOARD) == w->xwin) { + /* we own the selection: skip the X round-trip */ + return _sapp_tc.app.clipboard; + } + _sapp_tc.app.clipboard[0] = 0; + const Atom incremental = XInternAtom(_sapp_tc.display, "INCR", False); + XConvertSelection(_sapp_tc.display, _sapp_tc.CLIPBOARD, _sapp_tc.UTF8_STRING, + _sapp_tc.SAPP_TC_SELECTION, w->xwin, CurrentTime); + XEvent event; + if (!_sapp_tc_x11_wait_for_event(w->xwin, SelectionNotify, 0.1, &event)) { + return _sapp_tc.app.clipboard; + } + if (event.xselection.property == None) { + return _sapp_tc.app.clipboard; + } + char* data = NULL; + Atom actual_type; + int actual_format; + unsigned long item_count, bytes_after; + const int ret = XGetWindowProperty(_sapp_tc.display, + event.xselection.requestor, event.xselection.property, + 0, LONG_MAX, True, _sapp_tc.UTF8_STRING, + &actual_type, &actual_format, &item_count, &bytes_after, + (unsigned char**)&data); + if ((ret != Success) || (data == NULL)) { + if (data) XFree(data); + return _sapp_tc.app.clipboard; + } + /* INCR (incremental transfer) is rejected: pastes beyond the buffer fail */ + if ((actual_type != incremental) && (item_count < (unsigned long)_sapp_tc.app.clipboard_size)) { + strncpy(_sapp_tc.app.clipboard, data, (size_t)_sapp_tc.app.clipboard_size - 1); + _sapp_tc.app.clipboard[_sapp_tc.app.clipboard_size - 1] = 0; + } + XFree(data); + return _sapp_tc.app.clipboard; +} + +void sapp_set_window_title(const char* str) { + if (!str) return; + strncpy(_sapp_tc.app.window_title, str, sizeof(_sapp_tc.app.window_title) - 1); + _sapp_tc.app.window_title[sizeof(_sapp_tc.app.window_title) - 1] = 0; + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (w && w->xwin) { + _sapp_tc_x11_update_window_title(w, _sapp_tc.app.window_title); + } +} + +bool sapp_is_fullscreen(void) { + return _sapp_tc.app.fullscreen; +} + +void sapp_toggle_fullscreen(void) { + if (!_sapp_tc.app.main) return; + _sapp_tc_x11_set_fullscreen(!_sapp_tc.app.fullscreen); + _sapp_tc_x11_update_dimensions(_sapp_tc.app.main); +} + +int sapp_get_num_dropped_files(void) { + return _sapp_tc.app.drop_enabled ? _sapp_tc.app.drop_num_files : 0; +} + +const char* sapp_get_dropped_file_path(int index) { + if (!_sapp_tc.app.drop_enabled || !_sapp_tc.app.drop_buffer) return ""; + if (index < 0 || index >= _sapp_tc.app.drop_num_files) return ""; + return _sapp_tc.app.drop_buffer + (size_t)index * (size_t)_sapp_tc.app.drop_max_path_length; +} + +sapp_pixel_format sapp_color_format(void) { + return SAPP_PIXELFORMAT_RGBA8; /* plain 8-bit RGBA on GL (no 10-bit path) */ +} + +sapp_pixel_format sapp_depth_format(void) { + return SAPP_PIXELFORMAT_DEPTH_STENCIL; +} + +const void* sapp_x11_get_window(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return w ? (const void*)(uintptr_t)w->xwin : 0; +} + +const void* sapp_x11_get_display(void) { + return (const void*)_sapp_tc.display; +} + +/* Metal / D3D11 / win32 / macOS handles do not exist on Linux */ +const void* sapp_metal_get_device(void) { return 0; } +const void* sapp_metal_get_current_drawable(void) { return 0; } +const void* sapp_metal_get_depth_stencil_texture(void) { return 0; } +const void* sapp_metal_get_msaa_color_texture(void) { return 0; } +const void* sapp_macos_get_window(void) { return 0; } +const void* sapp_win32_get_hwnd(void) { return 0; } +const void* sapp_d3d11_get_device(void) { return 0; } +const void* sapp_d3d11_get_device_context(void) { return 0; } +const void* sapp_d3d11_get_swap_chain(void) { return 0; } + +sapp_environment sapp_get_environment(void) { + sapp_environment env; + memset(&env, 0, sizeof(env)); + env.defaults.color_format = sapp_color_format(); + env.defaults.depth_format = sapp_depth_format(); + env.defaults.sample_count = sapp_sample_count(); + /* GL has no device handle: the shared context is current on this thread */ + return env; +} + +sapp_swapchain sapp_get_swapchain(void) { + /* the GL swapchain contract is just the default-framebuffer id: rendering + targets whatever drawable is current (the main window's during its + frame_cb) -- calling this any number of times per frame is safe */ + sapp_swapchain sc; + memset(&sc, 0, sizeof(sc)); + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w) return sc; + sc.width = sapp_width(); + sc.height = sapp_height(); + sc.sample_count = sapp_sample_count(); + sc.color_format = sapp_color_format(); + sc.depth_format = sapp_depth_format(); + sc.gl.framebuffer = _sapp_tc.gl_framebuffer; + return sc; +} + +} /* extern "C" */ +#elif defined(SOKOL_GLES3) +/*== Linux (X11 + EGL, GLES3 -- Raspberry Pi) ================================ + Implements the public sokol_app.h API on GLES3 Linux (TrussC's Raspberry + Pi configuration) plus the multi-window API (as stubs -- this variant is + single-window; sapp_create_window() returns the invalid handle and logs). + + Unlike the GLCORE branch above (a from-scratch multi-window X11+GLX + implementation), this branch is a verbatim lift of upstream sokol_app.h's + single-window X11 backend with the EGL context path (SOKOL_GLES3 -> + _SAPP_EGL), the same way the web/iOS/Android sections were ported: the + whole upstream body renamed _sapp_tc_*, with upstream member names kept + in the state struct so the lifted code reads unchanged. The GLX forks + inside the lifted code are dead (_SAPP_GLX undefined). TrussC patches + ride along: skip_present, RGB10A2 is NOT used here (GLES3 swapchain is + RGBA8, same as upstream). Clipboard, XDND drag&drop, Xcursor cursors, + XKB keytable and EWMH fullscreen are all live upstream features. */ +#define _SAPP_LINUX (1) +#define _SAPP_EGL (1) +#define _SAPP_ANY_GL (1) + +#define GL_GLEXT_PROTOTYPES +#include +#include +#include +#include +#include +#include +#include +#include +#include /* XC_* font cursors */ +#include /* CARD32 */ +#include +#include +#include +#include /* dlopen, dlsym, dlclose */ +#include /* LONG_MAX */ +#include /* only used as a linker-guard, see _sapp_tc_linux_run */ +#include +#include +#include + +#if defined(SOKOL_APP_IMPL_INCLUDED) +#error "sokol_app_tc.h owns the Linux implementation of the sapp_* API; include sokol_app.h WITHOUT an implementation define in this TU" +#endif + +#ifndef SOKOL_ASSERT +#include +#define SOKOL_ASSERT(c) assert(c) +#endif +#ifndef _SOKOL_PRIVATE +#define _SOKOL_PRIVATE static +#endif +#ifndef _SOKOL_UNUSED +#define _SOKOL_UNUSED(x) (void)(x) +#endif +#ifndef SOKOL_API_IMPL +#define SOKOL_API_IMPL +#endif +#ifndef SOKOL_UNREACHABLE +#define SOKOL_UNREACHABLE SOKOL_ASSERT(false) +#endif + +#define _SAPP_MAX_TITLE_LENGTH (128) +#define _SAPP_FALLBACK_DEFAULT_WINDOW_WIDTH (640) +#define _SAPP_FALLBACK_DEFAULT_WINDOW_HEIGHT (480) +#define _sapp_tc_def(val, def) (((val) == 0) ? (def) : (val)) +#if defined(__cplusplus) +#define _SAPP_STRUCT(TYPE, NAME) TYPE NAME = {} +#define _SAPP_CLEAR_ARC_STRUCT(type, item) { item = type(); } +#else +#define _SAPP_STRUCT(TYPE, NAME) TYPE NAME = {0} +#define _SAPP_CLEAR_ARC_STRUCT(type, item) { _sapp_tc_clear(&item, sizeof(item)); } +#endif + +/* controlled failure instead of sokol_app.h's log-item machinery */ +#define _SAPP_PANIC(code) do { fprintf(stderr, "sokol_app_tc.h: panic: " #code "\n"); abort(); } while (0) +#define _SAPP_ERROR(code) fprintf(stderr, "sokol_app_tc.h: error: " #code "\n") +#define _SAPP_ERROR_MSG(code, msg) fprintf(stderr, "sokol_app_tc.h: error: " #code ": %s\n", msg) +#define _SAPP_WARN_MSG(code, msg) fprintf(stderr, "sokol_app_tc.h: warn: " #code ": %s\n", msg) +#define _SAPP_INFO_MSG(code, msg) fprintf(stderr, "sokol_app_tc.h: info: " #code ": %s\n", msg) +#define _SAPP_INFO(code) fprintf(stderr, "sokol_app_tc.h: info: " #code "\n") +#define _SAPP_WARN(code) fprintf(stderr, "sokol_app_tc.h: warn: " #code "\n") +typedef struct { + #if defined(_SAPP_APPLE) + struct { + mach_timebase_info_data_t timebase; + uint64_t start; + } mach; + #elif defined(_SAPP_EMSCRIPTEN) + int _dummy; + #elif defined(_SAPP_WIN32) + struct { + LARGE_INTEGER freq; + LARGE_INTEGER start; + } win; + #else // Linux, Android, ... + #ifdef CLOCK_MONOTONIC + #define _SAPP_CLOCK_MONOTONIC CLOCK_MONOTONIC + #else + // on some embedded platforms, CLOCK_MONOTONIC isn't defined + #define _SAPP_CLOCK_MONOTONIC (1) + #endif + struct { + uint64_t start; + } posix; + #endif +} _sapp_tc_timestamp_t; + +_SOKOL_PRIVATE int64_t _sapp_tc_int64_muldiv(int64_t value, int64_t numer, int64_t denom) { + int64_t q = value / denom; + int64_t r = value % denom; + return q * numer + r * numer / denom; +} + +_SOKOL_PRIVATE void _sapp_tc_timestamp_init(_sapp_tc_timestamp_t* ts) { + #if defined(_SAPP_APPLE) + mach_timebase_info(&ts->mach.timebase); + ts->mach.start = mach_absolute_time(); + #elif defined(_SAPP_EMSCRIPTEN) + (void)ts; + #elif defined(_SAPP_WIN32) + QueryPerformanceFrequency(&ts->win.freq); + QueryPerformanceCounter(&ts->win.start); + #else + struct timespec tspec; + clock_gettime(_SAPP_CLOCK_MONOTONIC, &tspec); + ts->posix.start = (uint64_t)tspec.tv_sec*1000000000 + (uint64_t)tspec.tv_nsec; + #endif +} + +_SOKOL_PRIVATE double _sapp_tc_timestamp_now(_sapp_tc_timestamp_t* ts) { + #if defined(_SAPP_APPLE) + const uint64_t traw = mach_absolute_time() - ts->mach.start; + const uint64_t now = (uint64_t) _sapp_tc_int64_muldiv((int64_t)traw, (int64_t)ts->mach.timebase.numer, (int64_t)ts->mach.timebase.denom); + return (double)now / 1000000000.0; + #elif defined(_SAPP_EMSCRIPTEN) + (void)ts; + SOKOL_ASSERT(false); + return 0.0; + #elif defined(_SAPP_WIN32) + LARGE_INTEGER qpc; + QueryPerformanceCounter(&qpc); + const uint64_t now = (uint64_t)_sapp_tc_int64_muldiv(qpc.QuadPart - ts->win.start.QuadPart, 1000000000, ts->win.freq.QuadPart); + return (double)now / 1000000000.0; + #else + struct timespec tspec; + clock_gettime(_SAPP_CLOCK_MONOTONIC, &tspec); + const uint64_t now = ((uint64_t)tspec.tv_sec*1000000000 + (uint64_t)tspec.tv_nsec) - ts->posix.start; + return (double)now / 1000000000.0; + #endif +} + +typedef struct { + _sapp_tc_timestamp_t timestamp; + double dt_min; // config: min clamp value for unfiltered time delta (seconds) + double dt_max; // config: max clamp value for unfiltered time delta (seconds) + double dt_threshold; // config: threshold time delta for 'resetting' filtering (default: 0.004s, 4ms) + double alpha; // config: smoothing constant, lower values smoother, higher values faster response + double last; // last absolute time in seconds + double dt; // unfiltered frame delta in seconds, clamped to dt_min/dt_max + double ema; // intermediate ema-filter result + double smooth_dt; // smoothed frame delta in seconds +} _sapp_tc_timing_t; + +_SOKOL_PRIVATE void _sapp_tc_timing_init(_sapp_tc_timing_t* t) { + _sapp_tc_timestamp_init(&t->timestamp); + t->dt_min = 0.000001; // 1 us + t->dt_max = 0.1; // 100 ms + t->dt_threshold = 0.004; // 4ms + t->alpha = 0.025; + t->dt = 1.0 / 60.0; // a 'likely' non-null value + t->ema = t->dt; + t->smooth_dt = t->dt; +} + +_SOKOL_PRIVATE double _sapp_tc_timing_clamp(_sapp_tc_timing_t* t, double dt) { + SOKOL_ASSERT((t->dt_min > 0.0) && (t->dt_max > 0.0) && (t->dt_max >= t->dt_min)); + if (dt < t->dt_min) { + return t->dt_min; + } else if (dt > t->dt_max) { + return t->dt_max; + } else { + return dt; + } +} + +_SOKOL_PRIVATE void _sapp_tc_timing_delta(_sapp_tc_timing_t* t, double dt) { + // first clamp raw dt against min/max (min avoid division by zero, max + // may avoids glitches and 'death-spirals' during debugging + dt = _sapp_tc_timing_clamp(t, dt); + t->dt = dt; + const double error = fabs(dt - t->smooth_dt); + if (error > t->dt_threshold) { + // 'reset' filter when new delta is outside threshold + t->ema = dt; + t->smooth_dt = dt; + } else { + // simple ema-filter with fixed alpha + t->ema = t->ema + t->alpha * (dt - t->ema); + t->smooth_dt = _sapp_tc_timing_clamp(t, t->ema); + } +} + +_SOKOL_PRIVATE void _sapp_tc_timing_update(_sapp_tc_timing_t* t, double external_now) { + double now; + if (external_now == 0.0) { + now = _sapp_tc_timestamp_now(&t->timestamp); + } else { + now = external_now; + } + if (t->last > 0.0) { + double dt = now - t->last; + _sapp_tc_timing_delta(t, dt); + } + t->last = now; + +} + +_SOKOL_PRIVATE double _sapp_tc_timing_get(_sapp_tc_timing_t* t) { + return t->smooth_dt; +} + +#if defined(_SAPP_LINUX) + +#define _SAPP_X11_XDND_VERSION (5) +#define _SAPP_X11_MAX_X11_KEYCODES (256) + +#define GLX_VENDOR 1 +#define GLX_RGBA_BIT 0x00000001 +#define GLX_WINDOW_BIT 0x00000001 +#define GLX_DRAWABLE_TYPE 0x8010 +#define GLX_RENDER_TYPE 0x8011 +#define GLX_DOUBLEBUFFER 5 +#define GLX_RED_SIZE 8 +#define GLX_GREEN_SIZE 9 +#define GLX_BLUE_SIZE 10 +#define GLX_ALPHA_SIZE 11 +#define GLX_DEPTH_SIZE 12 +#define GLX_STENCIL_SIZE 13 +#define GLX_SAMPLES 0x186a1 +#define GLX_CONTEXT_CORE_PROFILE_BIT_ARB 0x00000001 +#define GLX_CONTEXT_PROFILE_MASK_ARB 0x9126 +#define GLX_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB 0x00000002 +#define GLX_CONTEXT_MAJOR_VERSION_ARB 0x2091 +#define GLX_CONTEXT_MINOR_VERSION_ARB 0x2092 +#define GLX_CONTEXT_FLAGS_ARB 0x2094 + +typedef XID GLXWindow; +typedef XID GLXDrawable; +typedef struct __GLXFBConfig* GLXFBConfig; +typedef struct __GLXcontext* GLXContext; +typedef void (*__GLXextproc)(void); + +typedef int (*PFNGLXGETFBCONFIGATTRIBPROC)(Display*,GLXFBConfig,int,int*); +typedef const char* (*PFNGLXGETCLIENTSTRINGPROC)(Display*,int); +typedef Bool (*PFNGLXQUERYEXTENSIONPROC)(Display*,int*,int*); +typedef Bool (*PFNGLXQUERYVERSIONPROC)(Display*,int*,int*); +typedef void (*PFNGLXDESTROYCONTEXTPROC)(Display*,GLXContext); +typedef Bool (*PFNGLXMAKECURRENTPROC)(Display*,GLXDrawable,GLXContext); +typedef void (*PFNGLXSWAPBUFFERSPROC)(Display*,GLXDrawable); +typedef const char* (*PFNGLXQUERYEXTENSIONSSTRINGPROC)(Display*,int); +typedef GLXFBConfig* (*PFNGLXGETFBCONFIGSPROC)(Display*,int,int*); +typedef __GLXextproc (* PFNGLXGETPROCADDRESSPROC)(const char *procName); +typedef void (*PFNGLXSWAPINTERVALEXTPROC)(Display*,GLXDrawable,int); +typedef XVisualInfo* (*PFNGLXGETVISUALFROMFBCONFIGPROC)(Display*,GLXFBConfig); +typedef GLXWindow (*PFNGLXCREATEWINDOWPROC)(Display*,GLXFBConfig,Window,const int*); +typedef void (*PFNGLXDESTROYWINDOWPROC)(Display*,GLXWindow); + +typedef int (*PFNGLXSWAPINTERVALMESAPROC)(int); +typedef GLXContext (*PFNGLXCREATECONTEXTATTRIBSARBPROC)(Display*,GLXFBConfig,GLXContext,Bool,const int*); + +typedef struct { + bool available; + int major_opcode; + int event_base; + int error_base; + int major; + int minor; +} _sapp_tc_xi_t; + +typedef struct { + int version; + Window source; + Atom format; + Atom XdndAware; + Atom XdndEnter; + Atom XdndPosition; + Atom XdndStatus; + Atom XdndActionCopy; + Atom XdndDrop; + Atom XdndFinished; + Atom XdndSelection; + Atom XdndTypeList; + Atom text_uri_list; +} _sapp_tc_xdnd_t; + +typedef struct { + uint8_t mouse_buttons; + Display* display; + int screen; + Window root; + Colormap colormap; + Window window; + Cursor hidden_cursor; + Cursor standard_cursors[_SAPP_MOUSECURSOR_NUM]; + Cursor custom_cursors[_SAPP_MOUSECURSOR_NUM]; + int window_state; + float dpi; + unsigned char error_code; + Atom UTF8_STRING; + Atom CLIPBOARD; + Atom TARGETS; + Atom WM_PROTOCOLS; + Atom WM_DELETE_WINDOW; + Atom WM_STATE; + Atom NET_WM_NAME; + Atom NET_WM_ICON_NAME; + Atom NET_WM_ICON; + Atom NET_WM_STATE; + Atom NET_WM_STATE_FULLSCREEN; + _sapp_tc_xi_t xi; + _sapp_tc_xdnd_t xdnd; + // XLib manual says keycodes are in the range [8, 255] inclusive. + // https://tronche.com/gui/x/xlib/input/keyboard-encoding.html + bool key_repeat[_SAPP_X11_MAX_X11_KEYCODES]; +} _sapp_tc_x11_t; + +#if defined(_SAPP_GLX) + +typedef struct { + void* libgl; + int major; + int minor; + int event_base; + int error_base; + GLXContext ctx; + GLXWindow window; + + // GLX 1.3 functions + PFNGLXGETFBCONFIGSPROC GetFBConfigs; + PFNGLXGETFBCONFIGATTRIBPROC GetFBConfigAttrib; + PFNGLXGETCLIENTSTRINGPROC GetClientString; + PFNGLXQUERYEXTENSIONPROC QueryExtension; + PFNGLXQUERYVERSIONPROC QueryVersion; + PFNGLXDESTROYCONTEXTPROC DestroyContext; + PFNGLXMAKECURRENTPROC MakeCurrent; + PFNGLXSWAPBUFFERSPROC SwapBuffers; + PFNGLXQUERYEXTENSIONSSTRINGPROC QueryExtensionsString; + PFNGLXGETVISUALFROMFBCONFIGPROC GetVisualFromFBConfig; + PFNGLXCREATEWINDOWPROC CreateWindow; + PFNGLXDESTROYWINDOWPROC DestroyWindow; + + // GLX 1.4 and extension functions + PFNGLXGETPROCADDRESSPROC GetProcAddress; + PFNGLXGETPROCADDRESSPROC GetProcAddressARB; + PFNGLXSWAPINTERVALEXTPROC SwapIntervalEXT; + PFNGLXSWAPINTERVALMESAPROC SwapIntervalMESA; + PFNGLXCREATECONTEXTATTRIBSARBPROC CreateContextAttribsARB; + + // special case glGetIntegerv + void (*GetIntegerv)(uint32_t pname, int32_t* data); + + // extension availability + bool EXT_swap_control; + bool MESA_swap_control; + bool ARB_multisample; + bool ARB_create_context; + bool ARB_create_context_profile; +} _sapp_tc_glx_t; +#endif // _SAPP_GLX + +#if defined(_SAPP_EGL) +typedef struct { + EGLDisplay display; + EGLContext context; + EGLSurface surface; +} _sapp_tc_egl_t; +#endif // _SAPP_EGL +#endif // _SAPP_LINUX + +typedef struct { + bool enabled; + int buf_size; + char* buffer; +} _sapp_tc_clipboard_t; + +typedef struct { + bool enabled; + int max_files; + int max_path_length; + int num_files; + int buf_size; + char* buffer; +} _sapp_tc_drop_t; + +typedef struct { + float x, y; + float dx, dy; + bool shown; + bool locked; + bool pos_valid; + sapp_mouse_cursor current_cursor; +} _sapp_tc_mouse_t; + +typedef struct { + uint32_t framebuffer; +} _sapp_tc_gl_t; + +/* per-app state -- a GLES3/Linux-sized subset of sokol_app.h's _sapp_t with + the same member names, so the lifted implementation code reads unchanged. */ +typedef struct { + sapp_desc desc; + bool valid; + bool fullscreen; + bool first_frame; + bool init_called; + bool cleanup_called; + bool quit_requested; + bool quit_ordered; + bool event_consumed; + bool html5_ask_leave_site; /* inert */ + bool onscreen_keyboard_shown; + bool skip_present; + int window_width; + int window_height; + int framebuffer_width; + int framebuffer_height; + int sample_count; + int swap_interval; + float dpi_scale; + uint64_t frame_count; + sapp_event event; + _sapp_tc_mouse_t mouse; + _sapp_tc_clipboard_t clipboard; + _sapp_tc_drop_t drop; + _sapp_tc_timing_t timing; + _sapp_tc_x11_t x11; + _sapp_tc_egl_t egl; + _sapp_tc_gl_t gl; + char html5_canvas_selector[_SAPP_MAX_TITLE_LENGTH]; + char window_title[_SAPP_MAX_TITLE_LENGTH]; + sapp_keycode keycodes[SAPP_MAX_KEYCODES]; + bool custom_cursor_bound[_SAPP_MOUSECURSOR_NUM]; +} _sapp_tc_t; +static _sapp_tc_t _sapp_tc; +_SOKOL_PRIVATE void _sapp_tc_clear(void* ptr, size_t size) { + SOKOL_ASSERT(ptr && (size > 0)); + memset(ptr, 0, size); +} + +_SOKOL_PRIVATE void* _sapp_tc_malloc(size_t size) { + SOKOL_ASSERT(size > 0); + void* ptr; + if (_sapp_tc.desc.allocator.alloc_fn) { + ptr = _sapp_tc.desc.allocator.alloc_fn(size, _sapp_tc.desc.allocator.user_data); + } else { + ptr = malloc(size); + } + if (0 == ptr) { + _SAPP_PANIC(MALLOC_FAILED); + } + return ptr; +} + +_SOKOL_PRIVATE void* _sapp_tc_malloc_clear(size_t size) { + void* ptr = _sapp_tc_malloc(size); + _sapp_tc_clear(ptr, size); + return ptr; +} + +_SOKOL_PRIVATE void _sapp_tc_free(void* ptr) { + if (_sapp_tc.desc.allocator.free_fn) { + _sapp_tc.desc.allocator.free_fn(ptr, _sapp_tc.desc.allocator.user_data); + } else { + free(ptr); + } +} + +// ██ ██ ███████ ██ ██████ ███████ ██████ ███████ +// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +// ███████ █████ ██ ██████ █████ ██████ ███████ +// ██ ██ ██ ██ ██ ██ ██ ██ ██ +// ██ ██ ███████ ███████ ██ ███████ ██ ██ ███████ +// +// >>helpers + +// round float to int and at least 1 +_SOKOL_PRIVATE int _sapp_tc_roundf_gzero(float f) { + int val = (int)roundf(f); + if (val <= 0) { + val = 1; + } + return val; +} + +_SOKOL_PRIVATE void _sapp_tc_call_init(void) { + if (_sapp_tc.desc.init_cb) { + _sapp_tc.desc.init_cb(); + } else if (_sapp_tc.desc.init_userdata_cb) { + _sapp_tc.desc.init_userdata_cb(_sapp_tc.desc.user_data); + } + _sapp_tc.init_called = true; +} + +_SOKOL_PRIVATE void _sapp_tc_call_frame(void) { + if (_sapp_tc.init_called && !_sapp_tc.cleanup_called) { + if (_sapp_tc.desc.frame_cb) { + _sapp_tc.desc.frame_cb(); + } else if (_sapp_tc.desc.frame_userdata_cb) { + _sapp_tc.desc.frame_userdata_cb(_sapp_tc.desc.user_data); + } + } +} + +_SOKOL_PRIVATE void _sapp_tc_call_cleanup(void) { + if (!_sapp_tc.cleanup_called) { + if (_sapp_tc.desc.cleanup_cb) { + _sapp_tc.desc.cleanup_cb(); + } else if (_sapp_tc.desc.cleanup_userdata_cb) { + _sapp_tc.desc.cleanup_userdata_cb(_sapp_tc.desc.user_data); + } + _sapp_tc.cleanup_called = true; + } +} + +_SOKOL_PRIVATE bool _sapp_tc_call_event(const sapp_event* e) { + if (!_sapp_tc.cleanup_called) { + if (_sapp_tc.desc.event_cb) { + _sapp_tc.desc.event_cb(e); + } else if (_sapp_tc.desc.event_userdata_cb) { + _sapp_tc.desc.event_userdata_cb(e, _sapp_tc.desc.user_data); + } + } + if (_sapp_tc.event_consumed) { + _sapp_tc.event_consumed = false; + return true; + } else { + return false; + } +} + + +_SOKOL_PRIVATE char* _sapp_tc_dropped_file_path_ptr(int index) { + SOKOL_ASSERT(_sapp_tc.drop.buffer); + SOKOL_ASSERT((index >= 0) && (index <= _sapp_tc.drop.max_files)); + int offset = index * _sapp_tc.drop.max_path_length; + SOKOL_ASSERT(offset < _sapp_tc.drop.buf_size); + return &_sapp_tc.drop.buffer[offset]; +} + +/* Copy a string (either zero-terminated or with explicit length) + into a fixed size buffer with guaranteed zero-termination. + + Return false if the string didn't fit into the buffer and had to be clamped. + + FIXME: Currently UTF-8 strings might become invalid if the string + is clamped, because the last zero-byte might be written into + the middle of a multi-byte sequence. +*/ +_SOKOL_PRIVATE bool _sapp_tc_strcpy_range(const char* src, size_t src_len, char* dst, size_t dst_buf_len) { + SOKOL_ASSERT(src && dst && (dst_buf_len > 0)); + if (0 == src_len) { + src_len = dst_buf_len; + } + char* const end = &(dst[dst_buf_len-1]); + char c = 0; + for (size_t i = 0; i < dst_buf_len; i++) { + c = *src; + if (i >= src_len) { + c = 0; + } + if (c != 0) { + src++; + } + *dst++ = c; + } + // truncated? + if (c != 0) { + *end = 0; + return false; + } else { + return true; + } +} + +_SOKOL_PRIVATE bool _sapp_tc_strcpy(const char* src, char* dst, size_t dst_buf_len) { + return _sapp_tc_strcpy_range(src, 0, dst, dst_buf_len); +} + +_SOKOL_PRIVATE sapp_desc _sapp_tc_desc_defaults(const sapp_desc* desc) { + SOKOL_ASSERT((desc->allocator.alloc_fn && desc->allocator.free_fn) || (!desc->allocator.alloc_fn && !desc->allocator.free_fn)); + sapp_desc res = *desc; + res.sample_count = _sapp_tc_def(res.sample_count, 1); + res.swap_interval = _sapp_tc_def(res.swap_interval, 1); + if (0 == res.gl.major_version) { + #if defined(SOKOL_GLCORE) + res.gl.major_version = 4; + #if defined(_SAPP_APPLE) + res.gl.minor_version = 1; + #else + res.gl.minor_version = 3; + #endif + #elif defined(SOKOL_GLES3) + res.gl.major_version = 3; + #if defined(_SAPP_ANDROID) || defined(_SAPP_LINUX) + res.gl.minor_version = 1; + #else + res.gl.minor_version = 0; + #endif + #endif + } + res.html5.canvas_selector = _sapp_tc_def(res.html5.canvas_selector, "#canvas"); + res.clipboard_size = _sapp_tc_def(res.clipboard_size, 8192); + res.max_dropped_files = _sapp_tc_def(res.max_dropped_files, 1); + res.max_dropped_file_path_length = _sapp_tc_def(res.max_dropped_file_path_length, 2048); + res.window_title = _sapp_tc_def(res.window_title, "sokol"); + return res; +} + +_SOKOL_PRIVATE void _sapp_tc_init_state(const sapp_desc* desc) { + SOKOL_ASSERT(desc); + SOKOL_ASSERT(desc->width >= 0); + SOKOL_ASSERT(desc->height >= 0); + SOKOL_ASSERT(desc->sample_count >= 0); + SOKOL_ASSERT(desc->swap_interval >= 0); + SOKOL_ASSERT(desc->clipboard_size >= 0); + SOKOL_ASSERT(desc->max_dropped_files >= 0); + SOKOL_ASSERT(desc->max_dropped_file_path_length >= 0); + _SAPP_CLEAR_ARC_STRUCT(_sapp_tc_t, _sapp_tc); + _sapp_tc.desc = _sapp_tc_desc_defaults(desc); + _sapp_tc.first_frame = true; + // NOTE: _sapp_tc.desc.width/height may be 0! Platform backends need to deal with this + _sapp_tc.window_width = _sapp_tc.desc.width; + _sapp_tc.window_height = _sapp_tc.desc.height; + _sapp_tc.framebuffer_width = _sapp_tc.window_width; + _sapp_tc.framebuffer_height = _sapp_tc.window_height; + _sapp_tc.sample_count = _sapp_tc.desc.sample_count; + _sapp_tc.swap_interval = _sapp_tc.desc.swap_interval; + _sapp_tc_strcpy(_sapp_tc.desc.html5.canvas_selector, _sapp_tc.html5_canvas_selector, sizeof(_sapp_tc.html5_canvas_selector)); + _sapp_tc.desc.html5.canvas_selector = _sapp_tc.html5_canvas_selector; + _sapp_tc.html5_ask_leave_site = _sapp_tc.desc.html5.ask_leave_site; + _sapp_tc.clipboard.enabled = _sapp_tc.desc.enable_clipboard; + if (_sapp_tc.clipboard.enabled) { + _sapp_tc.clipboard.buf_size = _sapp_tc.desc.clipboard_size; + _sapp_tc.clipboard.buffer = (char*) _sapp_tc_malloc_clear((size_t)_sapp_tc.clipboard.buf_size); + } + _sapp_tc.drop.enabled = _sapp_tc.desc.enable_dragndrop; + if (_sapp_tc.drop.enabled) { + _sapp_tc.drop.max_files = _sapp_tc.desc.max_dropped_files; + _sapp_tc.drop.max_path_length = _sapp_tc.desc.max_dropped_file_path_length; + _sapp_tc.drop.buf_size = _sapp_tc.drop.max_files * _sapp_tc.drop.max_path_length; + _sapp_tc.drop.buffer = (char*) _sapp_tc_malloc_clear((size_t)_sapp_tc.drop.buf_size); + } + _sapp_tc_strcpy(_sapp_tc.desc.window_title, _sapp_tc.window_title, sizeof(_sapp_tc.window_title)); + _sapp_tc.desc.window_title = _sapp_tc.window_title; + _sapp_tc.dpi_scale = 1.0f; + _sapp_tc.fullscreen = _sapp_tc.desc.fullscreen; + _sapp_tc.mouse.shown = true; + _sapp_tc_timing_init(&_sapp_tc.timing); +} + +_SOKOL_PRIVATE void _sapp_tc_discard_state(void) { + if (_sapp_tc.clipboard.enabled) { + SOKOL_ASSERT(_sapp_tc.clipboard.buffer); + _sapp_tc_free((void*)_sapp_tc.clipboard.buffer); + } + if (_sapp_tc.drop.enabled) { + SOKOL_ASSERT(_sapp_tc.drop.buffer); + _sapp_tc_free((void*)_sapp_tc.drop.buffer); + } + for (int i = 0; i < _SAPP_MOUSECURSOR_NUM; i++) { + sapp_unbind_mouse_cursor_image((sapp_mouse_cursor) i); + } + _SAPP_CLEAR_ARC_STRUCT(_sapp_tc_t, _sapp_tc); +} + +_SOKOL_PRIVATE void _sapp_tc_init_event(sapp_event_type type) { + _sapp_tc_clear(&_sapp_tc.event, sizeof(_sapp_tc.event)); + _sapp_tc.event.type = type; + _sapp_tc.event.frame_count = _sapp_tc.frame_count; + _sapp_tc.event.mouse_button = SAPP_MOUSEBUTTON_INVALID; + _sapp_tc.event.window_width = _sapp_tc.window_width; + _sapp_tc.event.window_height = _sapp_tc.window_height; + _sapp_tc.event.framebuffer_width = _sapp_tc.framebuffer_width; + _sapp_tc.event.framebuffer_height = _sapp_tc.framebuffer_height; + _sapp_tc.event.mouse_x = _sapp_tc.mouse.x; + _sapp_tc.event.mouse_y = _sapp_tc.mouse.y; + _sapp_tc.event.mouse_dx = _sapp_tc.mouse.dx; + _sapp_tc.event.mouse_dy = _sapp_tc.mouse.dy; +} + +_SOKOL_PRIVATE bool _sapp_tc_events_enabled(void) { + /* only send events when an event callback is set, and the init function was called */ + return (_sapp_tc.desc.event_cb || _sapp_tc.desc.event_userdata_cb) && _sapp_tc.init_called; +} + +_SOKOL_PRIVATE void _sapp_tc_clear_drop_buffer(void) { + if (_sapp_tc.drop.enabled) { + SOKOL_ASSERT(_sapp_tc.drop.buffer); + _sapp_tc_clear(_sapp_tc.drop.buffer, (size_t)_sapp_tc.drop.buf_size); + } +} + +_SOKOL_PRIVATE void _sapp_tc_frame(void) { + if (_sapp_tc.first_frame) { + _sapp_tc.first_frame = false; + _sapp_tc_call_init(); + } + _sapp_tc_call_frame(); + _sapp_tc.frame_count++; +} + +_SOKOL_PRIVATE bool _sapp_tc_image_validate(const sapp_image_desc* desc) { + SOKOL_ASSERT(desc->width > 0); + SOKOL_ASSERT(desc->height > 0); + SOKOL_ASSERT(desc->pixels.ptr != 0); + SOKOL_ASSERT(desc->pixels.size > 0); + const size_t wh_size = (size_t)(desc->width * desc->height) * sizeof(uint32_t); + if (wh_size != desc->pixels.size) { + _SAPP_ERROR(IMAGE_DATA_SIZE_MISMATCH); + return false; + } + return true; +} + +_SOKOL_PRIVATE int _sapp_tc_image_bestmatch(const sapp_image_desc image_descs[], int num_images, int width, int height) { + int least_diff = 0x7FFFFFFF; + int least_index = 0; + for (int i = 0; i < num_images; i++) { + int diff = (image_descs[i].width * image_descs[i].height) - (width * height); + if (diff < 0) { + diff = -diff; + } + if (diff < least_diff) { + least_diff = diff; + least_index = i; + } + } + return least_index; +} + +_SOKOL_PRIVATE int _sapp_tc_icon_num_images(const sapp_icon_desc* desc) { + int index = 0; + for (; index < SAPP_MAX_ICONIMAGES; index++) { + if (0 == desc->images[index].pixels.ptr) { + break; + } + } + return index; +} + +_SOKOL_PRIVATE bool _sapp_tc_validate_icon_desc(const sapp_icon_desc* desc, int num_images) { + SOKOL_ASSERT(num_images <= SAPP_MAX_ICONIMAGES); + for (int i = 0; i < num_images; i++) { + const sapp_image_desc* img_desc = &desc->images[i]; + if (!_sapp_tc_image_validate(img_desc)) { + return false; + } + } + return true; +} +// ██ ██ ███ ██ ██ ██ ██ ██ +// ██ ██ ████ ██ ██ ██ ██ ██ +// ██ ██ ██ ██ ██ ██ ██ ███ +// ██ ██ ██ ██ ██ ██ ██ ██ ██ +// ███████ ██ ██ ████ ██████ ██ ██ +// +// >>linux +#if defined(_SAPP_LINUX) + +/* see GLFW's xkb_unicode.c */ +static const struct _sapp_tc_x11_codepair { + uint16_t keysym; + uint16_t ucs; +} _sapp_tc_x11_keysymtab[] = { + { 0x01a1, 0x0104 }, + { 0x01a2, 0x02d8 }, + { 0x01a3, 0x0141 }, + { 0x01a5, 0x013d }, + { 0x01a6, 0x015a }, + { 0x01a9, 0x0160 }, + { 0x01aa, 0x015e }, + { 0x01ab, 0x0164 }, + { 0x01ac, 0x0179 }, + { 0x01ae, 0x017d }, + { 0x01af, 0x017b }, + { 0x01b1, 0x0105 }, + { 0x01b2, 0x02db }, + { 0x01b3, 0x0142 }, + { 0x01b5, 0x013e }, + { 0x01b6, 0x015b }, + { 0x01b7, 0x02c7 }, + { 0x01b9, 0x0161 }, + { 0x01ba, 0x015f }, + { 0x01bb, 0x0165 }, + { 0x01bc, 0x017a }, + { 0x01bd, 0x02dd }, + { 0x01be, 0x017e }, + { 0x01bf, 0x017c }, + { 0x01c0, 0x0154 }, + { 0x01c3, 0x0102 }, + { 0x01c5, 0x0139 }, + { 0x01c6, 0x0106 }, + { 0x01c8, 0x010c }, + { 0x01ca, 0x0118 }, + { 0x01cc, 0x011a }, + { 0x01cf, 0x010e }, + { 0x01d0, 0x0110 }, + { 0x01d1, 0x0143 }, + { 0x01d2, 0x0147 }, + { 0x01d5, 0x0150 }, + { 0x01d8, 0x0158 }, + { 0x01d9, 0x016e }, + { 0x01db, 0x0170 }, + { 0x01de, 0x0162 }, + { 0x01e0, 0x0155 }, + { 0x01e3, 0x0103 }, + { 0x01e5, 0x013a }, + { 0x01e6, 0x0107 }, + { 0x01e8, 0x010d }, + { 0x01ea, 0x0119 }, + { 0x01ec, 0x011b }, + { 0x01ef, 0x010f }, + { 0x01f0, 0x0111 }, + { 0x01f1, 0x0144 }, + { 0x01f2, 0x0148 }, + { 0x01f5, 0x0151 }, + { 0x01f8, 0x0159 }, + { 0x01f9, 0x016f }, + { 0x01fb, 0x0171 }, + { 0x01fe, 0x0163 }, + { 0x01ff, 0x02d9 }, + { 0x02a1, 0x0126 }, + { 0x02a6, 0x0124 }, + { 0x02a9, 0x0130 }, + { 0x02ab, 0x011e }, + { 0x02ac, 0x0134 }, + { 0x02b1, 0x0127 }, + { 0x02b6, 0x0125 }, + { 0x02b9, 0x0131 }, + { 0x02bb, 0x011f }, + { 0x02bc, 0x0135 }, + { 0x02c5, 0x010a }, + { 0x02c6, 0x0108 }, + { 0x02d5, 0x0120 }, + { 0x02d8, 0x011c }, + { 0x02dd, 0x016c }, + { 0x02de, 0x015c }, + { 0x02e5, 0x010b }, + { 0x02e6, 0x0109 }, + { 0x02f5, 0x0121 }, + { 0x02f8, 0x011d }, + { 0x02fd, 0x016d }, + { 0x02fe, 0x015d }, + { 0x03a2, 0x0138 }, + { 0x03a3, 0x0156 }, + { 0x03a5, 0x0128 }, + { 0x03a6, 0x013b }, + { 0x03aa, 0x0112 }, + { 0x03ab, 0x0122 }, + { 0x03ac, 0x0166 }, + { 0x03b3, 0x0157 }, + { 0x03b5, 0x0129 }, + { 0x03b6, 0x013c }, + { 0x03ba, 0x0113 }, + { 0x03bb, 0x0123 }, + { 0x03bc, 0x0167 }, + { 0x03bd, 0x014a }, + { 0x03bf, 0x014b }, + { 0x03c0, 0x0100 }, + { 0x03c7, 0x012e }, + { 0x03cc, 0x0116 }, + { 0x03cf, 0x012a }, + { 0x03d1, 0x0145 }, + { 0x03d2, 0x014c }, + { 0x03d3, 0x0136 }, + { 0x03d9, 0x0172 }, + { 0x03dd, 0x0168 }, + { 0x03de, 0x016a }, + { 0x03e0, 0x0101 }, + { 0x03e7, 0x012f }, + { 0x03ec, 0x0117 }, + { 0x03ef, 0x012b }, + { 0x03f1, 0x0146 }, + { 0x03f2, 0x014d }, + { 0x03f3, 0x0137 }, + { 0x03f9, 0x0173 }, + { 0x03fd, 0x0169 }, + { 0x03fe, 0x016b }, + { 0x047e, 0x203e }, + { 0x04a1, 0x3002 }, + { 0x04a2, 0x300c }, + { 0x04a3, 0x300d }, + { 0x04a4, 0x3001 }, + { 0x04a5, 0x30fb }, + { 0x04a6, 0x30f2 }, + { 0x04a7, 0x30a1 }, + { 0x04a8, 0x30a3 }, + { 0x04a9, 0x30a5 }, + { 0x04aa, 0x30a7 }, + { 0x04ab, 0x30a9 }, + { 0x04ac, 0x30e3 }, + { 0x04ad, 0x30e5 }, + { 0x04ae, 0x30e7 }, + { 0x04af, 0x30c3 }, + { 0x04b0, 0x30fc }, + { 0x04b1, 0x30a2 }, + { 0x04b2, 0x30a4 }, + { 0x04b3, 0x30a6 }, + { 0x04b4, 0x30a8 }, + { 0x04b5, 0x30aa }, + { 0x04b6, 0x30ab }, + { 0x04b7, 0x30ad }, + { 0x04b8, 0x30af }, + { 0x04b9, 0x30b1 }, + { 0x04ba, 0x30b3 }, + { 0x04bb, 0x30b5 }, + { 0x04bc, 0x30b7 }, + { 0x04bd, 0x30b9 }, + { 0x04be, 0x30bb }, + { 0x04bf, 0x30bd }, + { 0x04c0, 0x30bf }, + { 0x04c1, 0x30c1 }, + { 0x04c2, 0x30c4 }, + { 0x04c3, 0x30c6 }, + { 0x04c4, 0x30c8 }, + { 0x04c5, 0x30ca }, + { 0x04c6, 0x30cb }, + { 0x04c7, 0x30cc }, + { 0x04c8, 0x30cd }, + { 0x04c9, 0x30ce }, + { 0x04ca, 0x30cf }, + { 0x04cb, 0x30d2 }, + { 0x04cc, 0x30d5 }, + { 0x04cd, 0x30d8 }, + { 0x04ce, 0x30db }, + { 0x04cf, 0x30de }, + { 0x04d0, 0x30df }, + { 0x04d1, 0x30e0 }, + { 0x04d2, 0x30e1 }, + { 0x04d3, 0x30e2 }, + { 0x04d4, 0x30e4 }, + { 0x04d5, 0x30e6 }, + { 0x04d6, 0x30e8 }, + { 0x04d7, 0x30e9 }, + { 0x04d8, 0x30ea }, + { 0x04d9, 0x30eb }, + { 0x04da, 0x30ec }, + { 0x04db, 0x30ed }, + { 0x04dc, 0x30ef }, + { 0x04dd, 0x30f3 }, + { 0x04de, 0x309b }, + { 0x04df, 0x309c }, + { 0x05ac, 0x060c }, + { 0x05bb, 0x061b }, + { 0x05bf, 0x061f }, + { 0x05c1, 0x0621 }, + { 0x05c2, 0x0622 }, + { 0x05c3, 0x0623 }, + { 0x05c4, 0x0624 }, + { 0x05c5, 0x0625 }, + { 0x05c6, 0x0626 }, + { 0x05c7, 0x0627 }, + { 0x05c8, 0x0628 }, + { 0x05c9, 0x0629 }, + { 0x05ca, 0x062a }, + { 0x05cb, 0x062b }, + { 0x05cc, 0x062c }, + { 0x05cd, 0x062d }, + { 0x05ce, 0x062e }, + { 0x05cf, 0x062f }, + { 0x05d0, 0x0630 }, + { 0x05d1, 0x0631 }, + { 0x05d2, 0x0632 }, + { 0x05d3, 0x0633 }, + { 0x05d4, 0x0634 }, + { 0x05d5, 0x0635 }, + { 0x05d6, 0x0636 }, + { 0x05d7, 0x0637 }, + { 0x05d8, 0x0638 }, + { 0x05d9, 0x0639 }, + { 0x05da, 0x063a }, + { 0x05e0, 0x0640 }, + { 0x05e1, 0x0641 }, + { 0x05e2, 0x0642 }, + { 0x05e3, 0x0643 }, + { 0x05e4, 0x0644 }, + { 0x05e5, 0x0645 }, + { 0x05e6, 0x0646 }, + { 0x05e7, 0x0647 }, + { 0x05e8, 0x0648 }, + { 0x05e9, 0x0649 }, + { 0x05ea, 0x064a }, + { 0x05eb, 0x064b }, + { 0x05ec, 0x064c }, + { 0x05ed, 0x064d }, + { 0x05ee, 0x064e }, + { 0x05ef, 0x064f }, + { 0x05f0, 0x0650 }, + { 0x05f1, 0x0651 }, + { 0x05f2, 0x0652 }, + { 0x06a1, 0x0452 }, + { 0x06a2, 0x0453 }, + { 0x06a3, 0x0451 }, + { 0x06a4, 0x0454 }, + { 0x06a5, 0x0455 }, + { 0x06a6, 0x0456 }, + { 0x06a7, 0x0457 }, + { 0x06a8, 0x0458 }, + { 0x06a9, 0x0459 }, + { 0x06aa, 0x045a }, + { 0x06ab, 0x045b }, + { 0x06ac, 0x045c }, + { 0x06ae, 0x045e }, + { 0x06af, 0x045f }, + { 0x06b0, 0x2116 }, + { 0x06b1, 0x0402 }, + { 0x06b2, 0x0403 }, + { 0x06b3, 0x0401 }, + { 0x06b4, 0x0404 }, + { 0x06b5, 0x0405 }, + { 0x06b6, 0x0406 }, + { 0x06b7, 0x0407 }, + { 0x06b8, 0x0408 }, + { 0x06b9, 0x0409 }, + { 0x06ba, 0x040a }, + { 0x06bb, 0x040b }, + { 0x06bc, 0x040c }, + { 0x06be, 0x040e }, + { 0x06bf, 0x040f }, + { 0x06c0, 0x044e }, + { 0x06c1, 0x0430 }, + { 0x06c2, 0x0431 }, + { 0x06c3, 0x0446 }, + { 0x06c4, 0x0434 }, + { 0x06c5, 0x0435 }, + { 0x06c6, 0x0444 }, + { 0x06c7, 0x0433 }, + { 0x06c8, 0x0445 }, + { 0x06c9, 0x0438 }, + { 0x06ca, 0x0439 }, + { 0x06cb, 0x043a }, + { 0x06cc, 0x043b }, + { 0x06cd, 0x043c }, + { 0x06ce, 0x043d }, + { 0x06cf, 0x043e }, + { 0x06d0, 0x043f }, + { 0x06d1, 0x044f }, + { 0x06d2, 0x0440 }, + { 0x06d3, 0x0441 }, + { 0x06d4, 0x0442 }, + { 0x06d5, 0x0443 }, + { 0x06d6, 0x0436 }, + { 0x06d7, 0x0432 }, + { 0x06d8, 0x044c }, + { 0x06d9, 0x044b }, + { 0x06da, 0x0437 }, + { 0x06db, 0x0448 }, + { 0x06dc, 0x044d }, + { 0x06dd, 0x0449 }, + { 0x06de, 0x0447 }, + { 0x06df, 0x044a }, + { 0x06e0, 0x042e }, + { 0x06e1, 0x0410 }, + { 0x06e2, 0x0411 }, + { 0x06e3, 0x0426 }, + { 0x06e4, 0x0414 }, + { 0x06e5, 0x0415 }, + { 0x06e6, 0x0424 }, + { 0x06e7, 0x0413 }, + { 0x06e8, 0x0425 }, + { 0x06e9, 0x0418 }, + { 0x06ea, 0x0419 }, + { 0x06eb, 0x041a }, + { 0x06ec, 0x041b }, + { 0x06ed, 0x041c }, + { 0x06ee, 0x041d }, + { 0x06ef, 0x041e }, + { 0x06f0, 0x041f }, + { 0x06f1, 0x042f }, + { 0x06f2, 0x0420 }, + { 0x06f3, 0x0421 }, + { 0x06f4, 0x0422 }, + { 0x06f5, 0x0423 }, + { 0x06f6, 0x0416 }, + { 0x06f7, 0x0412 }, + { 0x06f8, 0x042c }, + { 0x06f9, 0x042b }, + { 0x06fa, 0x0417 }, + { 0x06fb, 0x0428 }, + { 0x06fc, 0x042d }, + { 0x06fd, 0x0429 }, + { 0x06fe, 0x0427 }, + { 0x06ff, 0x042a }, + { 0x07a1, 0x0386 }, + { 0x07a2, 0x0388 }, + { 0x07a3, 0x0389 }, + { 0x07a4, 0x038a }, + { 0x07a5, 0x03aa }, + { 0x07a7, 0x038c }, + { 0x07a8, 0x038e }, + { 0x07a9, 0x03ab }, + { 0x07ab, 0x038f }, + { 0x07ae, 0x0385 }, + { 0x07af, 0x2015 }, + { 0x07b1, 0x03ac }, + { 0x07b2, 0x03ad }, + { 0x07b3, 0x03ae }, + { 0x07b4, 0x03af }, + { 0x07b5, 0x03ca }, + { 0x07b6, 0x0390 }, + { 0x07b7, 0x03cc }, + { 0x07b8, 0x03cd }, + { 0x07b9, 0x03cb }, + { 0x07ba, 0x03b0 }, + { 0x07bb, 0x03ce }, + { 0x07c1, 0x0391 }, + { 0x07c2, 0x0392 }, + { 0x07c3, 0x0393 }, + { 0x07c4, 0x0394 }, + { 0x07c5, 0x0395 }, + { 0x07c6, 0x0396 }, + { 0x07c7, 0x0397 }, + { 0x07c8, 0x0398 }, + { 0x07c9, 0x0399 }, + { 0x07ca, 0x039a }, + { 0x07cb, 0x039b }, + { 0x07cc, 0x039c }, + { 0x07cd, 0x039d }, + { 0x07ce, 0x039e }, + { 0x07cf, 0x039f }, + { 0x07d0, 0x03a0 }, + { 0x07d1, 0x03a1 }, + { 0x07d2, 0x03a3 }, + { 0x07d4, 0x03a4 }, + { 0x07d5, 0x03a5 }, + { 0x07d6, 0x03a6 }, + { 0x07d7, 0x03a7 }, + { 0x07d8, 0x03a8 }, + { 0x07d9, 0x03a9 }, + { 0x07e1, 0x03b1 }, + { 0x07e2, 0x03b2 }, + { 0x07e3, 0x03b3 }, + { 0x07e4, 0x03b4 }, + { 0x07e5, 0x03b5 }, + { 0x07e6, 0x03b6 }, + { 0x07e7, 0x03b7 }, + { 0x07e8, 0x03b8 }, + { 0x07e9, 0x03b9 }, + { 0x07ea, 0x03ba }, + { 0x07eb, 0x03bb }, + { 0x07ec, 0x03bc }, + { 0x07ed, 0x03bd }, + { 0x07ee, 0x03be }, + { 0x07ef, 0x03bf }, + { 0x07f0, 0x03c0 }, + { 0x07f1, 0x03c1 }, + { 0x07f2, 0x03c3 }, + { 0x07f3, 0x03c2 }, + { 0x07f4, 0x03c4 }, + { 0x07f5, 0x03c5 }, + { 0x07f6, 0x03c6 }, + { 0x07f7, 0x03c7 }, + { 0x07f8, 0x03c8 }, + { 0x07f9, 0x03c9 }, + { 0x08a1, 0x23b7 }, + { 0x08a2, 0x250c }, + { 0x08a3, 0x2500 }, + { 0x08a4, 0x2320 }, + { 0x08a5, 0x2321 }, + { 0x08a6, 0x2502 }, + { 0x08a7, 0x23a1 }, + { 0x08a8, 0x23a3 }, + { 0x08a9, 0x23a4 }, + { 0x08aa, 0x23a6 }, + { 0x08ab, 0x239b }, + { 0x08ac, 0x239d }, + { 0x08ad, 0x239e }, + { 0x08ae, 0x23a0 }, + { 0x08af, 0x23a8 }, + { 0x08b0, 0x23ac }, + { 0x08bc, 0x2264 }, + { 0x08bd, 0x2260 }, + { 0x08be, 0x2265 }, + { 0x08bf, 0x222b }, + { 0x08c0, 0x2234 }, + { 0x08c1, 0x221d }, + { 0x08c2, 0x221e }, + { 0x08c5, 0x2207 }, + { 0x08c8, 0x223c }, + { 0x08c9, 0x2243 }, + { 0x08cd, 0x21d4 }, + { 0x08ce, 0x21d2 }, + { 0x08cf, 0x2261 }, + { 0x08d6, 0x221a }, + { 0x08da, 0x2282 }, + { 0x08db, 0x2283 }, + { 0x08dc, 0x2229 }, + { 0x08dd, 0x222a }, + { 0x08de, 0x2227 }, + { 0x08df, 0x2228 }, + { 0x08ef, 0x2202 }, + { 0x08f6, 0x0192 }, + { 0x08fb, 0x2190 }, + { 0x08fc, 0x2191 }, + { 0x08fd, 0x2192 }, + { 0x08fe, 0x2193 }, + { 0x09e0, 0x25c6 }, + { 0x09e1, 0x2592 }, + { 0x09e2, 0x2409 }, + { 0x09e3, 0x240c }, + { 0x09e4, 0x240d }, + { 0x09e5, 0x240a }, + { 0x09e8, 0x2424 }, + { 0x09e9, 0x240b }, + { 0x09ea, 0x2518 }, + { 0x09eb, 0x2510 }, + { 0x09ec, 0x250c }, + { 0x09ed, 0x2514 }, + { 0x09ee, 0x253c }, + { 0x09ef, 0x23ba }, + { 0x09f0, 0x23bb }, + { 0x09f1, 0x2500 }, + { 0x09f2, 0x23bc }, + { 0x09f3, 0x23bd }, + { 0x09f4, 0x251c }, + { 0x09f5, 0x2524 }, + { 0x09f6, 0x2534 }, + { 0x09f7, 0x252c }, + { 0x09f8, 0x2502 }, + { 0x0aa1, 0x2003 }, + { 0x0aa2, 0x2002 }, + { 0x0aa3, 0x2004 }, + { 0x0aa4, 0x2005 }, + { 0x0aa5, 0x2007 }, + { 0x0aa6, 0x2008 }, + { 0x0aa7, 0x2009 }, + { 0x0aa8, 0x200a }, + { 0x0aa9, 0x2014 }, + { 0x0aaa, 0x2013 }, + { 0x0aae, 0x2026 }, + { 0x0aaf, 0x2025 }, + { 0x0ab0, 0x2153 }, + { 0x0ab1, 0x2154 }, + { 0x0ab2, 0x2155 }, + { 0x0ab3, 0x2156 }, + { 0x0ab4, 0x2157 }, + { 0x0ab5, 0x2158 }, + { 0x0ab6, 0x2159 }, + { 0x0ab7, 0x215a }, + { 0x0ab8, 0x2105 }, + { 0x0abb, 0x2012 }, + { 0x0abc, 0x2329 }, + { 0x0abe, 0x232a }, + { 0x0ac3, 0x215b }, + { 0x0ac4, 0x215c }, + { 0x0ac5, 0x215d }, + { 0x0ac6, 0x215e }, + { 0x0ac9, 0x2122 }, + { 0x0aca, 0x2613 }, + { 0x0acc, 0x25c1 }, + { 0x0acd, 0x25b7 }, + { 0x0ace, 0x25cb }, + { 0x0acf, 0x25af }, + { 0x0ad0, 0x2018 }, + { 0x0ad1, 0x2019 }, + { 0x0ad2, 0x201c }, + { 0x0ad3, 0x201d }, + { 0x0ad4, 0x211e }, + { 0x0ad6, 0x2032 }, + { 0x0ad7, 0x2033 }, + { 0x0ad9, 0x271d }, + { 0x0adb, 0x25ac }, + { 0x0adc, 0x25c0 }, + { 0x0add, 0x25b6 }, + { 0x0ade, 0x25cf }, + { 0x0adf, 0x25ae }, + { 0x0ae0, 0x25e6 }, + { 0x0ae1, 0x25ab }, + { 0x0ae2, 0x25ad }, + { 0x0ae3, 0x25b3 }, + { 0x0ae4, 0x25bd }, + { 0x0ae5, 0x2606 }, + { 0x0ae6, 0x2022 }, + { 0x0ae7, 0x25aa }, + { 0x0ae8, 0x25b2 }, + { 0x0ae9, 0x25bc }, + { 0x0aea, 0x261c }, + { 0x0aeb, 0x261e }, + { 0x0aec, 0x2663 }, + { 0x0aed, 0x2666 }, + { 0x0aee, 0x2665 }, + { 0x0af0, 0x2720 }, + { 0x0af1, 0x2020 }, + { 0x0af2, 0x2021 }, + { 0x0af3, 0x2713 }, + { 0x0af4, 0x2717 }, + { 0x0af5, 0x266f }, + { 0x0af6, 0x266d }, + { 0x0af7, 0x2642 }, + { 0x0af8, 0x2640 }, + { 0x0af9, 0x260e }, + { 0x0afa, 0x2315 }, + { 0x0afb, 0x2117 }, + { 0x0afc, 0x2038 }, + { 0x0afd, 0x201a }, + { 0x0afe, 0x201e }, + { 0x0ba3, 0x003c }, + { 0x0ba6, 0x003e }, + { 0x0ba8, 0x2228 }, + { 0x0ba9, 0x2227 }, + { 0x0bc0, 0x00af }, + { 0x0bc2, 0x22a5 }, + { 0x0bc3, 0x2229 }, + { 0x0bc4, 0x230a }, + { 0x0bc6, 0x005f }, + { 0x0bca, 0x2218 }, + { 0x0bcc, 0x2395 }, + { 0x0bce, 0x22a4 }, + { 0x0bcf, 0x25cb }, + { 0x0bd3, 0x2308 }, + { 0x0bd6, 0x222a }, + { 0x0bd8, 0x2283 }, + { 0x0bda, 0x2282 }, + { 0x0bdc, 0x22a2 }, + { 0x0bfc, 0x22a3 }, + { 0x0cdf, 0x2017 }, + { 0x0ce0, 0x05d0 }, + { 0x0ce1, 0x05d1 }, + { 0x0ce2, 0x05d2 }, + { 0x0ce3, 0x05d3 }, + { 0x0ce4, 0x05d4 }, + { 0x0ce5, 0x05d5 }, + { 0x0ce6, 0x05d6 }, + { 0x0ce7, 0x05d7 }, + { 0x0ce8, 0x05d8 }, + { 0x0ce9, 0x05d9 }, + { 0x0cea, 0x05da }, + { 0x0ceb, 0x05db }, + { 0x0cec, 0x05dc }, + { 0x0ced, 0x05dd }, + { 0x0cee, 0x05de }, + { 0x0cef, 0x05df }, + { 0x0cf0, 0x05e0 }, + { 0x0cf1, 0x05e1 }, + { 0x0cf2, 0x05e2 }, + { 0x0cf3, 0x05e3 }, + { 0x0cf4, 0x05e4 }, + { 0x0cf5, 0x05e5 }, + { 0x0cf6, 0x05e6 }, + { 0x0cf7, 0x05e7 }, + { 0x0cf8, 0x05e8 }, + { 0x0cf9, 0x05e9 }, + { 0x0cfa, 0x05ea }, + { 0x0da1, 0x0e01 }, + { 0x0da2, 0x0e02 }, + { 0x0da3, 0x0e03 }, + { 0x0da4, 0x0e04 }, + { 0x0da5, 0x0e05 }, + { 0x0da6, 0x0e06 }, + { 0x0da7, 0x0e07 }, + { 0x0da8, 0x0e08 }, + { 0x0da9, 0x0e09 }, + { 0x0daa, 0x0e0a }, + { 0x0dab, 0x0e0b }, + { 0x0dac, 0x0e0c }, + { 0x0dad, 0x0e0d }, + { 0x0dae, 0x0e0e }, + { 0x0daf, 0x0e0f }, + { 0x0db0, 0x0e10 }, + { 0x0db1, 0x0e11 }, + { 0x0db2, 0x0e12 }, + { 0x0db3, 0x0e13 }, + { 0x0db4, 0x0e14 }, + { 0x0db5, 0x0e15 }, + { 0x0db6, 0x0e16 }, + { 0x0db7, 0x0e17 }, + { 0x0db8, 0x0e18 }, + { 0x0db9, 0x0e19 }, + { 0x0dba, 0x0e1a }, + { 0x0dbb, 0x0e1b }, + { 0x0dbc, 0x0e1c }, + { 0x0dbd, 0x0e1d }, + { 0x0dbe, 0x0e1e }, + { 0x0dbf, 0x0e1f }, + { 0x0dc0, 0x0e20 }, + { 0x0dc1, 0x0e21 }, + { 0x0dc2, 0x0e22 }, + { 0x0dc3, 0x0e23 }, + { 0x0dc4, 0x0e24 }, + { 0x0dc5, 0x0e25 }, + { 0x0dc6, 0x0e26 }, + { 0x0dc7, 0x0e27 }, + { 0x0dc8, 0x0e28 }, + { 0x0dc9, 0x0e29 }, + { 0x0dca, 0x0e2a }, + { 0x0dcb, 0x0e2b }, + { 0x0dcc, 0x0e2c }, + { 0x0dcd, 0x0e2d }, + { 0x0dce, 0x0e2e }, + { 0x0dcf, 0x0e2f }, + { 0x0dd0, 0x0e30 }, + { 0x0dd1, 0x0e31 }, + { 0x0dd2, 0x0e32 }, + { 0x0dd3, 0x0e33 }, + { 0x0dd4, 0x0e34 }, + { 0x0dd5, 0x0e35 }, + { 0x0dd6, 0x0e36 }, + { 0x0dd7, 0x0e37 }, + { 0x0dd8, 0x0e38 }, + { 0x0dd9, 0x0e39 }, + { 0x0dda, 0x0e3a }, + { 0x0ddf, 0x0e3f }, + { 0x0de0, 0x0e40 }, + { 0x0de1, 0x0e41 }, + { 0x0de2, 0x0e42 }, + { 0x0de3, 0x0e43 }, + { 0x0de4, 0x0e44 }, + { 0x0de5, 0x0e45 }, + { 0x0de6, 0x0e46 }, + { 0x0de7, 0x0e47 }, + { 0x0de8, 0x0e48 }, + { 0x0de9, 0x0e49 }, + { 0x0dea, 0x0e4a }, + { 0x0deb, 0x0e4b }, + { 0x0dec, 0x0e4c }, + { 0x0ded, 0x0e4d }, + { 0x0df0, 0x0e50 }, + { 0x0df1, 0x0e51 }, + { 0x0df2, 0x0e52 }, + { 0x0df3, 0x0e53 }, + { 0x0df4, 0x0e54 }, + { 0x0df5, 0x0e55 }, + { 0x0df6, 0x0e56 }, + { 0x0df7, 0x0e57 }, + { 0x0df8, 0x0e58 }, + { 0x0df9, 0x0e59 }, + { 0x0ea1, 0x3131 }, + { 0x0ea2, 0x3132 }, + { 0x0ea3, 0x3133 }, + { 0x0ea4, 0x3134 }, + { 0x0ea5, 0x3135 }, + { 0x0ea6, 0x3136 }, + { 0x0ea7, 0x3137 }, + { 0x0ea8, 0x3138 }, + { 0x0ea9, 0x3139 }, + { 0x0eaa, 0x313a }, + { 0x0eab, 0x313b }, + { 0x0eac, 0x313c }, + { 0x0ead, 0x313d }, + { 0x0eae, 0x313e }, + { 0x0eaf, 0x313f }, + { 0x0eb0, 0x3140 }, + { 0x0eb1, 0x3141 }, + { 0x0eb2, 0x3142 }, + { 0x0eb3, 0x3143 }, + { 0x0eb4, 0x3144 }, + { 0x0eb5, 0x3145 }, + { 0x0eb6, 0x3146 }, + { 0x0eb7, 0x3147 }, + { 0x0eb8, 0x3148 }, + { 0x0eb9, 0x3149 }, + { 0x0eba, 0x314a }, + { 0x0ebb, 0x314b }, + { 0x0ebc, 0x314c }, + { 0x0ebd, 0x314d }, + { 0x0ebe, 0x314e }, + { 0x0ebf, 0x314f }, + { 0x0ec0, 0x3150 }, + { 0x0ec1, 0x3151 }, + { 0x0ec2, 0x3152 }, + { 0x0ec3, 0x3153 }, + { 0x0ec4, 0x3154 }, + { 0x0ec5, 0x3155 }, + { 0x0ec6, 0x3156 }, + { 0x0ec7, 0x3157 }, + { 0x0ec8, 0x3158 }, + { 0x0ec9, 0x3159 }, + { 0x0eca, 0x315a }, + { 0x0ecb, 0x315b }, + { 0x0ecc, 0x315c }, + { 0x0ecd, 0x315d }, + { 0x0ece, 0x315e }, + { 0x0ecf, 0x315f }, + { 0x0ed0, 0x3160 }, + { 0x0ed1, 0x3161 }, + { 0x0ed2, 0x3162 }, + { 0x0ed3, 0x3163 }, + { 0x0ed4, 0x11a8 }, + { 0x0ed5, 0x11a9 }, + { 0x0ed6, 0x11aa }, + { 0x0ed7, 0x11ab }, + { 0x0ed8, 0x11ac }, + { 0x0ed9, 0x11ad }, + { 0x0eda, 0x11ae }, + { 0x0edb, 0x11af }, + { 0x0edc, 0x11b0 }, + { 0x0edd, 0x11b1 }, + { 0x0ede, 0x11b2 }, + { 0x0edf, 0x11b3 }, + { 0x0ee0, 0x11b4 }, + { 0x0ee1, 0x11b5 }, + { 0x0ee2, 0x11b6 }, + { 0x0ee3, 0x11b7 }, + { 0x0ee4, 0x11b8 }, + { 0x0ee5, 0x11b9 }, + { 0x0ee6, 0x11ba }, + { 0x0ee7, 0x11bb }, + { 0x0ee8, 0x11bc }, + { 0x0ee9, 0x11bd }, + { 0x0eea, 0x11be }, + { 0x0eeb, 0x11bf }, + { 0x0eec, 0x11c0 }, + { 0x0eed, 0x11c1 }, + { 0x0eee, 0x11c2 }, + { 0x0eef, 0x316d }, + { 0x0ef0, 0x3171 }, + { 0x0ef1, 0x3178 }, + { 0x0ef2, 0x317f }, + { 0x0ef3, 0x3181 }, + { 0x0ef4, 0x3184 }, + { 0x0ef5, 0x3186 }, + { 0x0ef6, 0x318d }, + { 0x0ef7, 0x318e }, + { 0x0ef8, 0x11eb }, + { 0x0ef9, 0x11f0 }, + { 0x0efa, 0x11f9 }, + { 0x0eff, 0x20a9 }, + { 0x13a4, 0x20ac }, + { 0x13bc, 0x0152 }, + { 0x13bd, 0x0153 }, + { 0x13be, 0x0178 }, + { 0x20ac, 0x20ac }, + { 0xfe50, '`' }, + { 0xfe51, 0x00b4 }, + { 0xfe52, '^' }, + { 0xfe53, '~' }, + { 0xfe54, 0x00af }, + { 0xfe55, 0x02d8 }, + { 0xfe56, 0x02d9 }, + { 0xfe57, 0x00a8 }, + { 0xfe58, 0x02da }, + { 0xfe59, 0x02dd }, + { 0xfe5a, 0x02c7 }, + { 0xfe5b, 0x00b8 }, + { 0xfe5c, 0x02db }, + { 0xfe5d, 0x037a }, + { 0xfe5e, 0x309b }, + { 0xfe5f, 0x309c }, + { 0xfe63, '/' }, + { 0xfe64, 0x02bc }, + { 0xfe65, 0x02bd }, + { 0xfe66, 0x02f5 }, + { 0xfe67, 0x02f3 }, + { 0xfe68, 0x02cd }, + { 0xfe69, 0xa788 }, + { 0xfe6a, 0x02f7 }, + { 0xfe6e, ',' }, + { 0xfe6f, 0x00a4 }, + { 0xfe80, 'a' }, /* XK_dead_a */ + { 0xfe81, 'A' }, /* XK_dead_A */ + { 0xfe82, 'e' }, /* XK_dead_e */ + { 0xfe83, 'E' }, /* XK_dead_E */ + { 0xfe84, 'i' }, /* XK_dead_i */ + { 0xfe85, 'I' }, /* XK_dead_I */ + { 0xfe86, 'o' }, /* XK_dead_o */ + { 0xfe87, 'O' }, /* XK_dead_O */ + { 0xfe88, 'u' }, /* XK_dead_u */ + { 0xfe89, 'U' }, /* XK_dead_U */ + { 0xfe8a, 0x0259 }, + { 0xfe8b, 0x018f }, + { 0xfe8c, 0x00b5 }, + { 0xfe90, '_' }, + { 0xfe91, 0x02c8 }, + { 0xfe92, 0x02cc }, + { 0xff80 /*XKB_KEY_KP_Space*/, ' ' }, + { 0xff95 /*XKB_KEY_KP_7*/, 0x0037 }, + { 0xff96 /*XKB_KEY_KP_4*/, 0x0034 }, + { 0xff97 /*XKB_KEY_KP_8*/, 0x0038 }, + { 0xff98 /*XKB_KEY_KP_6*/, 0x0036 }, + { 0xff99 /*XKB_KEY_KP_2*/, 0x0032 }, + { 0xff9a /*XKB_KEY_KP_9*/, 0x0039 }, + { 0xff9b /*XKB_KEY_KP_3*/, 0x0033 }, + { 0xff9c /*XKB_KEY_KP_1*/, 0x0031 }, + { 0xff9d /*XKB_KEY_KP_5*/, 0x0035 }, + { 0xff9e /*XKB_KEY_KP_0*/, 0x0030 }, + { 0xffaa /*XKB_KEY_KP_Multiply*/, '*' }, + { 0xffab /*XKB_KEY_KP_Add*/, '+' }, + { 0xffac /*XKB_KEY_KP_Separator*/, ',' }, + { 0xffad /*XKB_KEY_KP_Subtract*/, '-' }, + { 0xffae /*XKB_KEY_KP_Decimal*/, '.' }, + { 0xffaf /*XKB_KEY_KP_Divide*/, '/' }, + { 0xffb0 /*XKB_KEY_KP_0*/, 0x0030 }, + { 0xffb1 /*XKB_KEY_KP_1*/, 0x0031 }, + { 0xffb2 /*XKB_KEY_KP_2*/, 0x0032 }, + { 0xffb3 /*XKB_KEY_KP_3*/, 0x0033 }, + { 0xffb4 /*XKB_KEY_KP_4*/, 0x0034 }, + { 0xffb5 /*XKB_KEY_KP_5*/, 0x0035 }, + { 0xffb6 /*XKB_KEY_KP_6*/, 0x0036 }, + { 0xffb7 /*XKB_KEY_KP_7*/, 0x0037 }, + { 0xffb8 /*XKB_KEY_KP_8*/, 0x0038 }, + { 0xffb9 /*XKB_KEY_KP_9*/, 0x0039 }, + { 0xffbd /*XKB_KEY_KP_Equal*/, '=' } +}; + +_SOKOL_PRIVATE int _sapp_tc_x11_error_handler(Display* display, XErrorEvent* event) { + _SOKOL_UNUSED(display); + _sapp_tc.x11.error_code = event->error_code; + return 0; +} + +_SOKOL_PRIVATE void _sapp_tc_x11_grab_error_handler(void) { + _sapp_tc.x11.error_code = Success; + XSetErrorHandler(_sapp_tc_x11_error_handler); +} + +_SOKOL_PRIVATE void _sapp_tc_x11_release_error_handler(void) { + XSync(_sapp_tc.x11.display, False); + XSetErrorHandler(NULL); +} + +_SOKOL_PRIVATE void _sapp_tc_x11_init_extensions(void) { + _sapp_tc.x11.UTF8_STRING = XInternAtom(_sapp_tc.x11.display, "UTF8_STRING", False); + _sapp_tc.x11.WM_PROTOCOLS = XInternAtom(_sapp_tc.x11.display, "WM_PROTOCOLS", False); + _sapp_tc.x11.WM_DELETE_WINDOW = XInternAtom(_sapp_tc.x11.display, "WM_DELETE_WINDOW", False); + _sapp_tc.x11.WM_STATE = XInternAtom(_sapp_tc.x11.display, "WM_STATE", False); + _sapp_tc.x11.NET_WM_NAME = XInternAtom(_sapp_tc.x11.display, "_NET_WM_NAME", False); + _sapp_tc.x11.NET_WM_ICON_NAME = XInternAtom(_sapp_tc.x11.display, "_NET_WM_ICON_NAME", False); + _sapp_tc.x11.NET_WM_ICON = XInternAtom(_sapp_tc.x11.display, "_NET_WM_ICON", False); + _sapp_tc.x11.NET_WM_STATE = XInternAtom(_sapp_tc.x11.display, "_NET_WM_STATE", False); + _sapp_tc.x11.NET_WM_STATE_FULLSCREEN = XInternAtom(_sapp_tc.x11.display, "_NET_WM_STATE_FULLSCREEN", False); + _sapp_tc.x11.CLIPBOARD = XInternAtom(_sapp_tc.x11.display, "CLIPBOARD", False); + _sapp_tc.x11.TARGETS = XInternAtom(_sapp_tc.x11.display, "TARGETS", False); + if (_sapp_tc.drop.enabled) { + _sapp_tc.x11.xdnd.XdndAware = XInternAtom(_sapp_tc.x11.display, "XdndAware", False); + _sapp_tc.x11.xdnd.XdndEnter = XInternAtom(_sapp_tc.x11.display, "XdndEnter", False); + _sapp_tc.x11.xdnd.XdndPosition = XInternAtom(_sapp_tc.x11.display, "XdndPosition", False); + _sapp_tc.x11.xdnd.XdndStatus = XInternAtom(_sapp_tc.x11.display, "XdndStatus", False); + _sapp_tc.x11.xdnd.XdndActionCopy = XInternAtom(_sapp_tc.x11.display, "XdndActionCopy", False); + _sapp_tc.x11.xdnd.XdndDrop = XInternAtom(_sapp_tc.x11.display, "XdndDrop", False); + _sapp_tc.x11.xdnd.XdndFinished = XInternAtom(_sapp_tc.x11.display, "XdndFinished", False); + _sapp_tc.x11.xdnd.XdndSelection = XInternAtom(_sapp_tc.x11.display, "XdndSelection", False); + _sapp_tc.x11.xdnd.XdndTypeList = XInternAtom(_sapp_tc.x11.display, "XdndTypeList", False); + _sapp_tc.x11.xdnd.text_uri_list = XInternAtom(_sapp_tc.x11.display, "text/uri-list", False); + } + + /* check Xi extension for raw mouse input */ + if (XQueryExtension(_sapp_tc.x11.display, "XInputExtension", &_sapp_tc.x11.xi.major_opcode, &_sapp_tc.x11.xi.event_base, &_sapp_tc.x11.xi.error_base)) { + _sapp_tc.x11.xi.major = 2; + _sapp_tc.x11.xi.minor = 0; + if (XIQueryVersion(_sapp_tc.x11.display, &_sapp_tc.x11.xi.major, &_sapp_tc.x11.xi.minor) == Success) { + _sapp_tc.x11.xi.available = true; + } + } +} + +// translate the X11 KeySyms for a key to sokol-app key code +// NOTE: this is only used as a fallback, in case the XBK method fails +// it is layout-dependent and will fail partially on most non-US layouts. +// +_SOKOL_PRIVATE sapp_keycode _sapp_tc_x11_translate_keysyms(const KeySym* keysyms, int width) { + if (width > 1) { + switch (keysyms[1]) { + case XK_KP_0: return SAPP_KEYCODE_KP_0; + case XK_KP_1: return SAPP_KEYCODE_KP_1; + case XK_KP_2: return SAPP_KEYCODE_KP_2; + case XK_KP_3: return SAPP_KEYCODE_KP_3; + case XK_KP_4: return SAPP_KEYCODE_KP_4; + case XK_KP_5: return SAPP_KEYCODE_KP_5; + case XK_KP_6: return SAPP_KEYCODE_KP_6; + case XK_KP_7: return SAPP_KEYCODE_KP_7; + case XK_KP_8: return SAPP_KEYCODE_KP_8; + case XK_KP_9: return SAPP_KEYCODE_KP_9; + case XK_KP_Separator: + case XK_KP_Decimal: return SAPP_KEYCODE_KP_DECIMAL; + case XK_KP_Equal: return SAPP_KEYCODE_KP_EQUAL; + case XK_KP_Enter: return SAPP_KEYCODE_KP_ENTER; + default: break; + } + } + + switch (keysyms[0]) { + case XK_Escape: return SAPP_KEYCODE_ESCAPE; + case XK_Tab: return SAPP_KEYCODE_TAB; + case XK_Shift_L: return SAPP_KEYCODE_LEFT_SHIFT; + case XK_Shift_R: return SAPP_KEYCODE_RIGHT_SHIFT; + case XK_Control_L: return SAPP_KEYCODE_LEFT_CONTROL; + case XK_Control_R: return SAPP_KEYCODE_RIGHT_CONTROL; + case XK_Meta_L: + case XK_Alt_L: return SAPP_KEYCODE_LEFT_ALT; + case XK_Mode_switch: // Mapped to Alt_R on many keyboards + case XK_ISO_Level3_Shift: // AltGr on at least some machines + case XK_Meta_R: + case XK_Alt_R: return SAPP_KEYCODE_RIGHT_ALT; + case XK_Super_L: return SAPP_KEYCODE_LEFT_SUPER; + case XK_Super_R: return SAPP_KEYCODE_RIGHT_SUPER; + case XK_Menu: return SAPP_KEYCODE_MENU; + case XK_Num_Lock: return SAPP_KEYCODE_NUM_LOCK; + case XK_Caps_Lock: return SAPP_KEYCODE_CAPS_LOCK; + case XK_Print: return SAPP_KEYCODE_PRINT_SCREEN; + case XK_Scroll_Lock: return SAPP_KEYCODE_SCROLL_LOCK; + case XK_Pause: return SAPP_KEYCODE_PAUSE; + case XK_Delete: return SAPP_KEYCODE_DELETE; + case XK_BackSpace: return SAPP_KEYCODE_BACKSPACE; + case XK_Return: return SAPP_KEYCODE_ENTER; + case XK_Home: return SAPP_KEYCODE_HOME; + case XK_End: return SAPP_KEYCODE_END; + case XK_Page_Up: return SAPP_KEYCODE_PAGE_UP; + case XK_Page_Down: return SAPP_KEYCODE_PAGE_DOWN; + case XK_Insert: return SAPP_KEYCODE_INSERT; + case XK_Left: return SAPP_KEYCODE_LEFT; + case XK_Right: return SAPP_KEYCODE_RIGHT; + case XK_Down: return SAPP_KEYCODE_DOWN; + case XK_Up: return SAPP_KEYCODE_UP; + case XK_F1: return SAPP_KEYCODE_F1; + case XK_F2: return SAPP_KEYCODE_F2; + case XK_F3: return SAPP_KEYCODE_F3; + case XK_F4: return SAPP_KEYCODE_F4; + case XK_F5: return SAPP_KEYCODE_F5; + case XK_F6: return SAPP_KEYCODE_F6; + case XK_F7: return SAPP_KEYCODE_F7; + case XK_F8: return SAPP_KEYCODE_F8; + case XK_F9: return SAPP_KEYCODE_F9; + case XK_F10: return SAPP_KEYCODE_F10; + case XK_F11: return SAPP_KEYCODE_F11; + case XK_F12: return SAPP_KEYCODE_F12; + case XK_F13: return SAPP_KEYCODE_F13; + case XK_F14: return SAPP_KEYCODE_F14; + case XK_F15: return SAPP_KEYCODE_F15; + case XK_F16: return SAPP_KEYCODE_F16; + case XK_F17: return SAPP_KEYCODE_F17; + case XK_F18: return SAPP_KEYCODE_F18; + case XK_F19: return SAPP_KEYCODE_F19; + case XK_F20: return SAPP_KEYCODE_F20; + case XK_F21: return SAPP_KEYCODE_F21; + case XK_F22: return SAPP_KEYCODE_F22; + case XK_F23: return SAPP_KEYCODE_F23; + case XK_F24: return SAPP_KEYCODE_F24; + case XK_F25: return SAPP_KEYCODE_F25; + + // numeric keypad + case XK_KP_Divide: return SAPP_KEYCODE_KP_DIVIDE; + case XK_KP_Multiply: return SAPP_KEYCODE_KP_MULTIPLY; + case XK_KP_Subtract: return SAPP_KEYCODE_KP_SUBTRACT; + case XK_KP_Add: return SAPP_KEYCODE_KP_ADD; + + // these should have been detected in secondary keysym test above! + case XK_KP_Insert: return SAPP_KEYCODE_KP_0; + case XK_KP_End: return SAPP_KEYCODE_KP_1; + case XK_KP_Down: return SAPP_KEYCODE_KP_2; + case XK_KP_Page_Down: return SAPP_KEYCODE_KP_3; + case XK_KP_Left: return SAPP_KEYCODE_KP_4; + case XK_KP_Right: return SAPP_KEYCODE_KP_6; + case XK_KP_Home: return SAPP_KEYCODE_KP_7; + case XK_KP_Up: return SAPP_KEYCODE_KP_8; + case XK_KP_Page_Up: return SAPP_KEYCODE_KP_9; + case XK_KP_Delete: return SAPP_KEYCODE_KP_DECIMAL; + case XK_KP_Equal: return SAPP_KEYCODE_KP_EQUAL; + case XK_KP_Enter: return SAPP_KEYCODE_KP_ENTER; + + // last resort: Check for printable keys (should not happen if the XKB + // extension is available). This will give a layout dependent mapping + // (which is wrong, and we may miss some keys, especially on non-US + // keyboards), but it's better than nothing... + case XK_a: return SAPP_KEYCODE_A; + case XK_b: return SAPP_KEYCODE_B; + case XK_c: return SAPP_KEYCODE_C; + case XK_d: return SAPP_KEYCODE_D; + case XK_e: return SAPP_KEYCODE_E; + case XK_f: return SAPP_KEYCODE_F; + case XK_g: return SAPP_KEYCODE_G; + case XK_h: return SAPP_KEYCODE_H; + case XK_i: return SAPP_KEYCODE_I; + case XK_j: return SAPP_KEYCODE_J; + case XK_k: return SAPP_KEYCODE_K; + case XK_l: return SAPP_KEYCODE_L; + case XK_m: return SAPP_KEYCODE_M; + case XK_n: return SAPP_KEYCODE_N; + case XK_o: return SAPP_KEYCODE_O; + case XK_p: return SAPP_KEYCODE_P; + case XK_q: return SAPP_KEYCODE_Q; + case XK_r: return SAPP_KEYCODE_R; + case XK_s: return SAPP_KEYCODE_S; + case XK_t: return SAPP_KEYCODE_T; + case XK_u: return SAPP_KEYCODE_U; + case XK_v: return SAPP_KEYCODE_V; + case XK_w: return SAPP_KEYCODE_W; + case XK_x: return SAPP_KEYCODE_X; + case XK_y: return SAPP_KEYCODE_Y; + case XK_z: return SAPP_KEYCODE_Z; + case XK_1: return SAPP_KEYCODE_1; + case XK_2: return SAPP_KEYCODE_2; + case XK_3: return SAPP_KEYCODE_3; + case XK_4: return SAPP_KEYCODE_4; + case XK_5: return SAPP_KEYCODE_5; + case XK_6: return SAPP_KEYCODE_6; + case XK_7: return SAPP_KEYCODE_7; + case XK_8: return SAPP_KEYCODE_8; + case XK_9: return SAPP_KEYCODE_9; + case XK_0: return SAPP_KEYCODE_0; + case XK_space: return SAPP_KEYCODE_SPACE; + case XK_minus: return SAPP_KEYCODE_MINUS; + case XK_equal: return SAPP_KEYCODE_EQUAL; + case XK_bracketleft: return SAPP_KEYCODE_LEFT_BRACKET; + case XK_bracketright: return SAPP_KEYCODE_RIGHT_BRACKET; + case XK_backslash: return SAPP_KEYCODE_BACKSLASH; + case XK_semicolon: return SAPP_KEYCODE_SEMICOLON; + case XK_apostrophe: return SAPP_KEYCODE_APOSTROPHE; + case XK_grave: return SAPP_KEYCODE_GRAVE_ACCENT; + case XK_comma: return SAPP_KEYCODE_COMMA; + case XK_period: return SAPP_KEYCODE_PERIOD; + case XK_slash: return SAPP_KEYCODE_SLASH; + case XK_less: return SAPP_KEYCODE_WORLD_1; // At least in some layouts... + default: break; + } + + // no matching translation was found + return SAPP_KEYCODE_INVALID; +} + + +// setup dynamic keycode/scancode mapping tables, this is required +// for getting layout-independent keycodes on X11. +// +// see GLFW x11_init.c/createKeyTables() +_SOKOL_PRIVATE void _sapp_tc_x11_init_keytable(void) { + for (int i = 0; i < SAPP_MAX_KEYCODES; i++) { + _sapp_tc.keycodes[i] = SAPP_KEYCODE_INVALID; + } + // use XKB to determine physical key locations independently of the current keyboard layout + XkbDescPtr desc = XkbGetMap(_sapp_tc.x11.display, 0, XkbUseCoreKbd); + SOKOL_ASSERT(desc); + XkbGetNames(_sapp_tc.x11.display, XkbKeyNamesMask | XkbKeyAliasesMask, desc); + + const int scancode_min = desc->min_key_code; + const int scancode_max = desc->max_key_code; + + const struct { sapp_keycode key; const char* name; } keymap[] = { + { SAPP_KEYCODE_GRAVE_ACCENT, "TLDE" }, + { SAPP_KEYCODE_1, "AE01" }, + { SAPP_KEYCODE_2, "AE02" }, + { SAPP_KEYCODE_3, "AE03" }, + { SAPP_KEYCODE_4, "AE04" }, + { SAPP_KEYCODE_5, "AE05" }, + { SAPP_KEYCODE_6, "AE06" }, + { SAPP_KEYCODE_7, "AE07" }, + { SAPP_KEYCODE_8, "AE08" }, + { SAPP_KEYCODE_9, "AE09" }, + { SAPP_KEYCODE_0, "AE10" }, + { SAPP_KEYCODE_MINUS, "AE11" }, + { SAPP_KEYCODE_EQUAL, "AE12" }, + { SAPP_KEYCODE_Q, "AD01" }, + { SAPP_KEYCODE_W, "AD02" }, + { SAPP_KEYCODE_E, "AD03" }, + { SAPP_KEYCODE_R, "AD04" }, + { SAPP_KEYCODE_T, "AD05" }, + { SAPP_KEYCODE_Y, "AD06" }, + { SAPP_KEYCODE_U, "AD07" }, + { SAPP_KEYCODE_I, "AD08" }, + { SAPP_KEYCODE_O, "AD09" }, + { SAPP_KEYCODE_P, "AD10" }, + { SAPP_KEYCODE_LEFT_BRACKET, "AD11" }, + { SAPP_KEYCODE_RIGHT_BRACKET, "AD12" }, + { SAPP_KEYCODE_A, "AC01" }, + { SAPP_KEYCODE_S, "AC02" }, + { SAPP_KEYCODE_D, "AC03" }, + { SAPP_KEYCODE_F, "AC04" }, + { SAPP_KEYCODE_G, "AC05" }, + { SAPP_KEYCODE_H, "AC06" }, + { SAPP_KEYCODE_J, "AC07" }, + { SAPP_KEYCODE_K, "AC08" }, + { SAPP_KEYCODE_L, "AC09" }, + { SAPP_KEYCODE_SEMICOLON, "AC10" }, + { SAPP_KEYCODE_APOSTROPHE, "AC11" }, + { SAPP_KEYCODE_Z, "AB01" }, + { SAPP_KEYCODE_X, "AB02" }, + { SAPP_KEYCODE_C, "AB03" }, + { SAPP_KEYCODE_V, "AB04" }, + { SAPP_KEYCODE_B, "AB05" }, + { SAPP_KEYCODE_N, "AB06" }, + { SAPP_KEYCODE_M, "AB07" }, + { SAPP_KEYCODE_COMMA, "AB08" }, + { SAPP_KEYCODE_PERIOD, "AB09" }, + { SAPP_KEYCODE_SLASH, "AB10" }, + { SAPP_KEYCODE_BACKSLASH, "BKSL" }, + { SAPP_KEYCODE_WORLD_1, "LSGT" }, + { SAPP_KEYCODE_SPACE, "SPCE" }, + { SAPP_KEYCODE_ESCAPE, "ESC" }, + { SAPP_KEYCODE_ENTER, "RTRN" }, + { SAPP_KEYCODE_TAB, "TAB" }, + { SAPP_KEYCODE_BACKSPACE, "BKSP" }, + { SAPP_KEYCODE_INSERT, "INS" }, + { SAPP_KEYCODE_DELETE, "DELE" }, + { SAPP_KEYCODE_RIGHT, "RGHT" }, + { SAPP_KEYCODE_LEFT, "LEFT" }, + { SAPP_KEYCODE_DOWN, "DOWN" }, + { SAPP_KEYCODE_UP, "UP" }, + { SAPP_KEYCODE_PAGE_UP, "PGUP" }, + { SAPP_KEYCODE_PAGE_DOWN, "PGDN" }, + { SAPP_KEYCODE_HOME, "HOME" }, + { SAPP_KEYCODE_END, "END" }, + { SAPP_KEYCODE_CAPS_LOCK, "CAPS" }, + { SAPP_KEYCODE_SCROLL_LOCK, "SCLK" }, + { SAPP_KEYCODE_NUM_LOCK, "NMLK" }, + { SAPP_KEYCODE_PRINT_SCREEN, "PRSC" }, + { SAPP_KEYCODE_PAUSE, "PAUS" }, + { SAPP_KEYCODE_F1, "FK01" }, + { SAPP_KEYCODE_F2, "FK02" }, + { SAPP_KEYCODE_F3, "FK03" }, + { SAPP_KEYCODE_F4, "FK04" }, + { SAPP_KEYCODE_F5, "FK05" }, + { SAPP_KEYCODE_F6, "FK06" }, + { SAPP_KEYCODE_F7, "FK07" }, + { SAPP_KEYCODE_F8, "FK08" }, + { SAPP_KEYCODE_F9, "FK09" }, + { SAPP_KEYCODE_F10, "FK10" }, + { SAPP_KEYCODE_F11, "FK11" }, + { SAPP_KEYCODE_F12, "FK12" }, + { SAPP_KEYCODE_F13, "FK13" }, + { SAPP_KEYCODE_F14, "FK14" }, + { SAPP_KEYCODE_F15, "FK15" }, + { SAPP_KEYCODE_F16, "FK16" }, + { SAPP_KEYCODE_F17, "FK17" }, + { SAPP_KEYCODE_F18, "FK18" }, + { SAPP_KEYCODE_F19, "FK19" }, + { SAPP_KEYCODE_F20, "FK20" }, + { SAPP_KEYCODE_F21, "FK21" }, + { SAPP_KEYCODE_F22, "FK22" }, + { SAPP_KEYCODE_F23, "FK23" }, + { SAPP_KEYCODE_F24, "FK24" }, + { SAPP_KEYCODE_F25, "FK25" }, + { SAPP_KEYCODE_KP_0, "KP0" }, + { SAPP_KEYCODE_KP_1, "KP1" }, + { SAPP_KEYCODE_KP_2, "KP2" }, + { SAPP_KEYCODE_KP_3, "KP3" }, + { SAPP_KEYCODE_KP_4, "KP4" }, + { SAPP_KEYCODE_KP_5, "KP5" }, + { SAPP_KEYCODE_KP_6, "KP6" }, + { SAPP_KEYCODE_KP_7, "KP7" }, + { SAPP_KEYCODE_KP_8, "KP8" }, + { SAPP_KEYCODE_KP_9, "KP9" }, + { SAPP_KEYCODE_KP_DECIMAL, "KPDL" }, + { SAPP_KEYCODE_KP_DIVIDE, "KPDV" }, + { SAPP_KEYCODE_KP_MULTIPLY, "KPMU" }, + { SAPP_KEYCODE_KP_SUBTRACT, "KPSU" }, + { SAPP_KEYCODE_KP_ADD, "KPAD" }, + { SAPP_KEYCODE_KP_ENTER, "KPEN" }, + { SAPP_KEYCODE_KP_EQUAL, "KPEQ" }, + { SAPP_KEYCODE_LEFT_SHIFT, "LFSH" }, + { SAPP_KEYCODE_LEFT_CONTROL, "LCTL" }, + { SAPP_KEYCODE_LEFT_ALT, "LALT" }, + { SAPP_KEYCODE_LEFT_SUPER, "LWIN" }, + { SAPP_KEYCODE_RIGHT_SHIFT, "RTSH" }, + { SAPP_KEYCODE_RIGHT_CONTROL, "RCTL" }, + { SAPP_KEYCODE_RIGHT_ALT, "RALT" }, + { SAPP_KEYCODE_RIGHT_ALT, "LVL3" }, + { SAPP_KEYCODE_RIGHT_ALT, "MDSW" }, + { SAPP_KEYCODE_RIGHT_SUPER, "RWIN" }, + { SAPP_KEYCODE_MENU, "MENU" } + }; + const int num_keymap_items = (int)(sizeof(keymap) / sizeof(keymap[0])); + + // find X11 keycode to sokol-app key code mapping + for (int scancode = scancode_min; scancode <= scancode_max; scancode++) { + sapp_keycode key = SAPP_KEYCODE_INVALID; + for (int i = 0; i < num_keymap_items; i++) { + if (strncmp(desc->names->keys[scancode].name, keymap[i].name, XkbKeyNameLength) == 0) { + key = keymap[i].key; + break; + } + } + + // fall back to key aliases in case the key name did not match + for (int i = 0; i < desc->names->num_key_aliases; i++) { + if (key != SAPP_KEYCODE_INVALID) { + break; + } + if (strncmp(desc->names->key_aliases[i].real, desc->names->keys[scancode].name, XkbKeyNameLength) != 0) { + continue; + } + for (int j = 0; j < num_keymap_items; j++) { + if (strncmp(desc->names->key_aliases[i].alias, keymap[j].name, XkbKeyNameLength) == 0) { + key = keymap[j].key; + break; + } + } + } + _sapp_tc.keycodes[scancode] = key; + } + XkbFreeNames(desc, XkbKeyNamesMask, True); + XkbFreeKeyboard(desc, 0, True); + + int width = 0; + KeySym* keysyms = XGetKeyboardMapping(_sapp_tc.x11.display, scancode_min, scancode_max - scancode_min + 1, &width); + for (int scancode = scancode_min; scancode <= scancode_max; scancode++) { + // translate untranslated key codes using the traditional X11 KeySym lookups + if (_sapp_tc.keycodes[scancode] == SAPP_KEYCODE_INVALID) { + const size_t base = (size_t)((scancode - scancode_min) * width); + _sapp_tc.keycodes[scancode] = _sapp_tc_x11_translate_keysyms(&keysyms[base], width); + } + } + XFree(keysyms); +} + +_SOKOL_PRIVATE void _sapp_tc_x11_query_system_dpi(void) { + /* from GLFW: + + NOTE: Default to the display-wide DPI as we don't currently have a policy + for which monitor a window is considered to be on + + _sapp_tc.x11.dpi = DisplayWidth(_sapp_tc.x11.display, _sapp_tc.x11.screen) * + 25.4f / DisplayWidthMM(_sapp_tc.x11.display, _sapp_tc.x11.screen); + + NOTE: Basing the scale on Xft.dpi where available should provide the most + consistent user experience (matches Qt, Gtk, etc), although not + always the most accurate one + */ + bool dpi_ok = false; + char* rms = XResourceManagerString(_sapp_tc.x11.display); + if (rms) { + XrmDatabase db = XrmGetStringDatabase(rms); + if (db) { + XrmValue value; + char* type = NULL; + if (XrmGetResource(db, "Xft.dpi", "Xft.Dpi", &type, &value)) { + if (type && strcmp(type, "String") == 0) { + _sapp_tc.x11.dpi = atof(value.addr); + dpi_ok = true; + } + } + XrmDestroyDatabase(db); + } + } + // fallback if querying DPI had failed: assume the standard DPI 96.0f + if (!dpi_ok) { + _sapp_tc.x11.dpi = 96.0f; + _SAPP_WARN(LINUX_X11_QUERY_SYSTEM_DPI_FAILED); + } +} + +#if defined(_SAPP_GLX) + +_SOKOL_PRIVATE bool _sapp_tc_glx_has_ext(const char* ext, const char* extensions) { + SOKOL_ASSERT(ext); + const char* start = extensions; + while (true) { + const char* where = strstr(start, ext); + if (!where) { + return false; + } + const char* terminator = where + strlen(ext); + if ((where == start) || (*(where - 1) == ' ')) { + if (*terminator == ' ' || *terminator == '\0') { + break; + } + } + start = terminator; + } + return true; +} + +_SOKOL_PRIVATE bool _sapp_tc_glx_extsupported(const char* ext, const char* extensions) { + if (extensions) { + return _sapp_tc_glx_has_ext(ext, extensions); + } else { + return false; + } +} + +_SOKOL_PRIVATE void* _sapp_tc_glx_getprocaddr(const char* procname) +{ + if (_sapp_tc.glx.GetProcAddress) { + return (void*) _sapp_tc.glx.GetProcAddress(procname); + } else if (_sapp_tc.glx.GetProcAddressARB) { + return (void*) _sapp_tc.glx.GetProcAddressARB(procname); + } else { + return dlsym(_sapp_tc.glx.libgl, procname); + } +} + +_SOKOL_PRIVATE void _sapp_tc_glx_init(void) { + const char* sonames[] = { "libGL.so.1", "libGL.so", 0 }; + for (int i = 0; sonames[i]; i++) { + _sapp_tc.glx.libgl = dlopen(sonames[i], RTLD_LAZY|RTLD_GLOBAL); + if (_sapp_tc.glx.libgl) { + break; + } + } + if (!_sapp_tc.glx.libgl) { + _SAPP_PANIC(LINUX_GLX_LOAD_LIBGL_FAILED); + } + _sapp_tc.glx.GetFBConfigs = (PFNGLXGETFBCONFIGSPROC) dlsym(_sapp_tc.glx.libgl, "glXGetFBConfigs"); + _sapp_tc.glx.GetFBConfigAttrib = (PFNGLXGETFBCONFIGATTRIBPROC) dlsym(_sapp_tc.glx.libgl, "glXGetFBConfigAttrib"); + _sapp_tc.glx.GetClientString = (PFNGLXGETCLIENTSTRINGPROC) dlsym(_sapp_tc.glx.libgl, "glXGetClientString"); + _sapp_tc.glx.QueryExtension = (PFNGLXQUERYEXTENSIONPROC) dlsym(_sapp_tc.glx.libgl, "glXQueryExtension"); + _sapp_tc.glx.QueryVersion = (PFNGLXQUERYVERSIONPROC) dlsym(_sapp_tc.glx.libgl, "glXQueryVersion"); + _sapp_tc.glx.DestroyContext = (PFNGLXDESTROYCONTEXTPROC) dlsym(_sapp_tc.glx.libgl, "glXDestroyContext"); + _sapp_tc.glx.MakeCurrent = (PFNGLXMAKECURRENTPROC) dlsym(_sapp_tc.glx.libgl, "glXMakeCurrent"); + _sapp_tc.glx.SwapBuffers = (PFNGLXSWAPBUFFERSPROC) dlsym(_sapp_tc.glx.libgl, "glXSwapBuffers"); + _sapp_tc.glx.QueryExtensionsString = (PFNGLXQUERYEXTENSIONSSTRINGPROC) dlsym(_sapp_tc.glx.libgl, "glXQueryExtensionsString"); + _sapp_tc.glx.CreateWindow = (PFNGLXCREATEWINDOWPROC) dlsym(_sapp_tc.glx.libgl, "glXCreateWindow"); + _sapp_tc.glx.DestroyWindow = (PFNGLXDESTROYWINDOWPROC) dlsym(_sapp_tc.glx.libgl, "glXDestroyWindow"); + _sapp_tc.glx.GetProcAddress = (PFNGLXGETPROCADDRESSPROC) dlsym(_sapp_tc.glx.libgl, "glXGetProcAddress"); + _sapp_tc.glx.GetProcAddressARB = (PFNGLXGETPROCADDRESSPROC) dlsym(_sapp_tc.glx.libgl, "glXGetProcAddressARB"); + _sapp_tc.glx.GetVisualFromFBConfig = (PFNGLXGETVISUALFROMFBCONFIGPROC) dlsym(_sapp_tc.glx.libgl, "glXGetVisualFromFBConfig"); + if (!_sapp_tc.glx.GetFBConfigs || + !_sapp_tc.glx.GetFBConfigAttrib || + !_sapp_tc.glx.GetClientString || + !_sapp_tc.glx.QueryExtension || + !_sapp_tc.glx.QueryVersion || + !_sapp_tc.glx.DestroyContext || + !_sapp_tc.glx.MakeCurrent || + !_sapp_tc.glx.SwapBuffers || + !_sapp_tc.glx.QueryExtensionsString || + !_sapp_tc.glx.CreateWindow || + !_sapp_tc.glx.DestroyWindow || + !_sapp_tc.glx.GetProcAddress || + !_sapp_tc.glx.GetProcAddressARB || + !_sapp_tc.glx.GetVisualFromFBConfig) + { + _SAPP_PANIC(LINUX_GLX_LOAD_ENTRY_POINTS_FAILED); + } + + if (!_sapp_tc.glx.QueryExtension(_sapp_tc.x11.display, &_sapp_tc.glx.error_base, &_sapp_tc.glx.event_base)) { + _SAPP_PANIC(LINUX_GLX_EXTENSION_NOT_FOUND); + } + if (!_sapp_tc.glx.QueryVersion(_sapp_tc.x11.display, &_sapp_tc.glx.major, &_sapp_tc.glx.minor)) { + _SAPP_PANIC(LINUX_GLX_QUERY_VERSION_FAILED); + } + if (_sapp_tc.glx.major == 1 && _sapp_tc.glx.minor < 3) { + _SAPP_PANIC(LINUX_GLX_VERSION_TOO_LOW); + } + const char* exts = _sapp_tc.glx.QueryExtensionsString(_sapp_tc.x11.display, _sapp_tc.x11.screen); + if (_sapp_tc_glx_extsupported("GLX_EXT_swap_control", exts)) { + _sapp_tc.glx.SwapIntervalEXT = (PFNGLXSWAPINTERVALEXTPROC) _sapp_tc_glx_getprocaddr("glXSwapIntervalEXT"); + _sapp_tc.glx.EXT_swap_control = 0 != _sapp_tc.glx.SwapIntervalEXT; + } + if (_sapp_tc_glx_extsupported("GLX_MESA_swap_control", exts)) { + _sapp_tc.glx.SwapIntervalMESA = (PFNGLXSWAPINTERVALMESAPROC) _sapp_tc_glx_getprocaddr("glXSwapIntervalMESA"); + _sapp_tc.glx.MESA_swap_control = 0 != _sapp_tc.glx.SwapIntervalMESA; + } + _sapp_tc.glx.ARB_multisample = _sapp_tc_glx_extsupported("GLX_ARB_multisample", exts); + if (_sapp_tc_glx_extsupported("GLX_ARB_create_context", exts)) { + _sapp_tc.glx.CreateContextAttribsARB = (PFNGLXCREATECONTEXTATTRIBSARBPROC) _sapp_tc_glx_getprocaddr("glXCreateContextAttribsARB"); + _sapp_tc.glx.ARB_create_context = 0 != _sapp_tc.glx.CreateContextAttribsARB; + } + _sapp_tc.glx.ARB_create_context_profile = _sapp_tc_glx_extsupported("GLX_ARB_create_context_profile", exts); +} + +_SOKOL_PRIVATE int _sapp_tc_glx_attrib(GLXFBConfig fbconfig, int attrib) { + int value; + _sapp_tc.glx.GetFBConfigAttrib(_sapp_tc.x11.display, fbconfig, attrib, &value); + return value; +} + +_SOKOL_PRIVATE GLXFBConfig _sapp_tc_glx_choosefbconfig(void) { + GLXFBConfig* native_configs; + _sapp_tc_gl_fbconfig* usable_configs; + const _sapp_tc_gl_fbconfig* closest; + int i, native_count, usable_count; + const char* vendor; + bool trust_window_bit = true; + + /* HACK: This is a (hopefully temporary) workaround for Chromium + (VirtualBox GL) not setting the window bit on any GLXFBConfigs + */ + vendor = _sapp_tc.glx.GetClientString(_sapp_tc.x11.display, GLX_VENDOR); + if (vendor && strcmp(vendor, "Chromium") == 0) { + trust_window_bit = false; + } + + native_configs = _sapp_tc.glx.GetFBConfigs(_sapp_tc.x11.display, _sapp_tc.x11.screen, &native_count); + if (!native_configs || !native_count) { + _SAPP_PANIC(LINUX_GLX_NO_GLXFBCONFIGS); + } + + usable_configs = (_sapp_tc_gl_fbconfig*) _sapp_tc_malloc_clear((size_t)native_count * sizeof(_sapp_tc_gl_fbconfig)); + usable_count = 0; + for (i = 0; i < native_count; i++) { + const GLXFBConfig n = native_configs[i]; + _sapp_tc_gl_fbconfig* u = usable_configs + usable_count; + _sapp_tc_gl_init_fbconfig(u); + + /* Only consider RGBA GLXFBConfigs */ + if (0 == (_sapp_tc_glx_attrib(n, GLX_RENDER_TYPE) & GLX_RGBA_BIT)) { + continue; + } + /* Only consider window GLXFBConfigs */ + if (0 == (_sapp_tc_glx_attrib(n, GLX_DRAWABLE_TYPE) & GLX_WINDOW_BIT)) { + if (trust_window_bit) { + continue; + } + } + u->red_bits = _sapp_tc_glx_attrib(n, GLX_RED_SIZE); + u->green_bits = _sapp_tc_glx_attrib(n, GLX_GREEN_SIZE); + u->blue_bits = _sapp_tc_glx_attrib(n, GLX_BLUE_SIZE); + u->alpha_bits = _sapp_tc_glx_attrib(n, GLX_ALPHA_SIZE); + u->depth_bits = _sapp_tc_glx_attrib(n, GLX_DEPTH_SIZE); + u->stencil_bits = _sapp_tc_glx_attrib(n, GLX_STENCIL_SIZE); + if (_sapp_tc_glx_attrib(n, GLX_DOUBLEBUFFER)) { + u->doublebuffer = true; + } + if (_sapp_tc.glx.ARB_multisample) { + u->samples = _sapp_tc_glx_attrib(n, GLX_SAMPLES); + } + u->handle = (uintptr_t) n; + usable_count++; + } + _sapp_tc_gl_fbconfig desired; + _sapp_tc_gl_init_fbconfig(&desired); + desired.red_bits = 8; + desired.green_bits = 8; + desired.blue_bits = 8; + desired.alpha_bits = 8; + desired.depth_bits = 24; + desired.stencil_bits = 8; + desired.doublebuffer = true; + desired.samples = _sapp_tc.sample_count > 1 ? _sapp_tc.sample_count : 0; + closest = _sapp_tc_gl_choose_fbconfig(&desired, usable_configs, usable_count); + GLXFBConfig result = 0; + if (closest) { + result = (GLXFBConfig) closest->handle; + } + XFree(native_configs); + _sapp_tc_free(usable_configs); + return result; +} + +_SOKOL_PRIVATE void _sapp_tc_glx_choose_visual(Visual** visual, int* depth) { + GLXFBConfig native = _sapp_tc_glx_choosefbconfig(); + if (0 == native) { + _SAPP_PANIC(LINUX_GLX_NO_SUITABLE_GLXFBCONFIG); + } + XVisualInfo* result = _sapp_tc.glx.GetVisualFromFBConfig(_sapp_tc.x11.display, native); + if (!result) { + _SAPP_PANIC(LINUX_GLX_GET_VISUAL_FROM_FBCONFIG_FAILED); + } + *visual = result->visual; + *depth = result->depth; + XFree(result); +} + +_SOKOL_PRIVATE void _sapp_tc_glx_make_current(void) { + _sapp_tc.glx.MakeCurrent(_sapp_tc.x11.display, _sapp_tc.glx.window, _sapp_tc.glx.ctx); + glGetIntegerv(GL_FRAMEBUFFER_BINDING, (GLint*)&_sapp_tc.gl.framebuffer); +} + +_SOKOL_PRIVATE void _sapp_tc_glx_create_context(void) { + GLXFBConfig native = _sapp_tc_glx_choosefbconfig(); + if (0 == native){ + _SAPP_PANIC(LINUX_GLX_NO_SUITABLE_GLXFBCONFIG); + } + if (!(_sapp_tc.glx.ARB_create_context && _sapp_tc.glx.ARB_create_context_profile)) { + _SAPP_PANIC(LINUX_GLX_REQUIRED_EXTENSIONS_MISSING); + } + _sapp_tc_x11_grab_error_handler(); + const int attribs[] = { + GLX_CONTEXT_MAJOR_VERSION_ARB, _sapp_tc.desc.gl.major_version, + GLX_CONTEXT_MINOR_VERSION_ARB, _sapp_tc.desc.gl.minor_version, + GLX_CONTEXT_PROFILE_MASK_ARB, GLX_CONTEXT_CORE_PROFILE_BIT_ARB, + GLX_CONTEXT_FLAGS_ARB, GLX_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB, + 0, 0 + }; + _sapp_tc.glx.ctx = _sapp_tc.glx.CreateContextAttribsARB(_sapp_tc.x11.display, native, NULL, True, attribs); + if (!_sapp_tc.glx.ctx) { + _SAPP_PANIC(LINUX_GLX_CREATE_CONTEXT_FAILED); + } + _sapp_tc_x11_release_error_handler(); + _sapp_tc.glx.window = _sapp_tc.glx.CreateWindow(_sapp_tc.x11.display, native, _sapp_tc.x11.window, NULL); + if (!_sapp_tc.glx.window) { + _SAPP_PANIC(LINUX_GLX_CREATE_WINDOW_FAILED); + } + _sapp_tc_glx_make_current(); +} + +_SOKOL_PRIVATE void _sapp_tc_glx_destroy_context(void) { + if (_sapp_tc.glx.window) { + _sapp_tc.glx.DestroyWindow(_sapp_tc.x11.display, _sapp_tc.glx.window); + _sapp_tc.glx.window = 0; + } + if (_sapp_tc.glx.ctx) { + _sapp_tc.glx.DestroyContext(_sapp_tc.x11.display, _sapp_tc.glx.ctx); + _sapp_tc.glx.ctx = 0; + } +} + +_SOKOL_PRIVATE void _sapp_tc_glx_swap_buffers(void) { + // Modified by tettou771 for TrussC: skip present support + if (_sapp_tc.skip_present) { _sapp_tc.skip_present = false; return; } + _sapp_tc.glx.SwapBuffers(_sapp_tc.x11.display, _sapp_tc.glx.window); +} + +_SOKOL_PRIVATE void _sapp_tc_glx_swapinterval(int interval) { + if (_sapp_tc.glx.EXT_swap_control) { + _sapp_tc.glx.SwapIntervalEXT(_sapp_tc.x11.display, _sapp_tc.glx.window, interval); + } else if (_sapp_tc.glx.MESA_swap_control) { + _sapp_tc.glx.SwapIntervalMESA(interval); + } +} + +#endif // _SAPP_GLX + +_SOKOL_PRIVATE void _sapp_tc_x11_send_event(Atom type, int a, int b, int c, int d, int e) { + _SAPP_STRUCT(XEvent, event); + event.type = ClientMessage; + event.xclient.window = _sapp_tc.x11.window; + event.xclient.format = 32; + event.xclient.message_type = type; + event.xclient.data.l[0] = a; + event.xclient.data.l[1] = b; + event.xclient.data.l[2] = c; + event.xclient.data.l[3] = d; + event.xclient.data.l[4] = e; + + XSendEvent(_sapp_tc.x11.display, _sapp_tc.x11.root, + False, + SubstructureNotifyMask | SubstructureRedirectMask, + &event); +} + +_SOKOL_PRIVATE bool _sapp_tc_x11_wait_for_event(int event_type, double timeout_sec, XEvent* out_event) { + _sapp_tc_timestamp_t ts; + _sapp_tc_timestamp_init(&ts); + while (!XCheckTypedWindowEvent(_sapp_tc.x11.display, _sapp_tc.x11.window, event_type, out_event)) { + struct pollfd fd = { ConnectionNumber(_sapp_tc.x11.display), POLLIN, 0 }; + poll(&fd, 1, timeout_sec * 1000); + if (_sapp_tc_timestamp_now(&ts) > timeout_sec) { + return false; + } + } + return true; +} + +_SOKOL_PRIVATE void _sapp_tc_x11_app_event(sapp_event_type type) { + if (_sapp_tc_events_enabled()) { + _sapp_tc_init_event(type); + _sapp_tc_call_event(&_sapp_tc.event); + } +} + +_SOKOL_PRIVATE void _sapp_tc_x11_update_dimensions(int x11_window_width, int x11_window_height) { + // NOTE: do *NOT* use _sapp_tc.dpi_scale for the window scale + const float window_scale = _sapp_tc.x11.dpi / 96.0f; + _sapp_tc.window_width = _sapp_tc_roundf_gzero(x11_window_width / window_scale); + _sapp_tc.window_height = _sapp_tc_roundf_gzero(x11_window_height / window_scale); + // NOTE: on Vulkan, updating the framebuffer dimensions is entirely handled + // by the swapchain management code + #if !defined(SOKOL_VULKAN) + int cur_fb_width = _sapp_tc.framebuffer_width; + int cur_fb_height = _sapp_tc.framebuffer_height; + _sapp_tc.framebuffer_width = _sapp_tc_roundf_gzero(_sapp_tc.window_width * _sapp_tc.dpi_scale); + _sapp_tc.framebuffer_height = _sapp_tc_roundf_gzero(_sapp_tc.window_height * _sapp_tc.dpi_scale); + bool dim_changed = (_sapp_tc.framebuffer_width != cur_fb_width) || (_sapp_tc.framebuffer_height != cur_fb_height); + if (dim_changed) { + #if defined(SOKOL_WGPU) + _sapp_tc_wgpu_swapchain_size_changed(); + #endif + if (!_sapp_tc.first_frame) { + _sapp_tc_x11_app_event(SAPP_EVENTTYPE_RESIZED); + } + } + #endif +} + +_SOKOL_PRIVATE void _sapp_tc_x11_update_dimensions_from_window_size(void) { + XWindowAttributes attribs; + XGetWindowAttributes(_sapp_tc.x11.display, _sapp_tc.x11.window, &attribs); + _sapp_tc_x11_update_dimensions(attribs.width, attribs.height); +} + +_SOKOL_PRIVATE void _sapp_tc_x11_set_fullscreen(bool enable) { + /* NOTE: this function must be called after XMapWindow (which happens in _sapp_tc_x11_show_window()) */ + if (_sapp_tc.x11.NET_WM_STATE && _sapp_tc.x11.NET_WM_STATE_FULLSCREEN) { + if (enable) { + const int _NET_WM_STATE_ADD = 1; + _sapp_tc_x11_send_event(_sapp_tc.x11.NET_WM_STATE, + _NET_WM_STATE_ADD, + _sapp_tc.x11.NET_WM_STATE_FULLSCREEN, + 0, 1, 0); + } else { + const int _NET_WM_STATE_REMOVE = 0; + _sapp_tc_x11_send_event(_sapp_tc.x11.NET_WM_STATE, + _NET_WM_STATE_REMOVE, + _sapp_tc.x11.NET_WM_STATE_FULLSCREEN, + 0, 1, 0); + } + } + XFlush(_sapp_tc.x11.display); +} + +_SOKOL_PRIVATE void _sapp_tc_x11_create_hidden_cursor(void) { + SOKOL_ASSERT(0 == _sapp_tc.x11.hidden_cursor); + const int w = 16; + const int h = 16; + XcursorImage* img = XcursorImageCreate(w, h); + SOKOL_ASSERT(img && (img->width == 16) && (img->height == 16) && img->pixels); + img->xhot = 0; + img->yhot = 0; + const size_t num_bytes = (size_t)(w * h) * sizeof(XcursorPixel); + _sapp_tc_clear(img->pixels, num_bytes); + _sapp_tc.x11.hidden_cursor = XcursorImageLoadCursor(_sapp_tc.x11.display, img); + XcursorImageDestroy(img); +} + + _SOKOL_PRIVATE void _sapp_tc_x11_create_standard_cursor(sapp_mouse_cursor cursor, const char* name, const char* theme, int size, uint32_t fallback_native) { + SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); + SOKOL_ASSERT(_sapp_tc.x11.display); + if (theme) { + XcursorImage* img = XcursorLibraryLoadImage(name, theme, size); + if (img) { + _sapp_tc.x11.standard_cursors[cursor] = XcursorImageLoadCursor(_sapp_tc.x11.display, img); + XcursorImageDestroy(img); + } + } + if (0 == _sapp_tc.x11.standard_cursors[cursor]) { + _sapp_tc.x11.standard_cursors[cursor] = XCreateFontCursor(_sapp_tc.x11.display, fallback_native); + } +} + +_SOKOL_PRIVATE void _sapp_tc_x11_create_standard_cursors(void) { + SOKOL_ASSERT(_sapp_tc.x11.display); + const char* cursor_theme = XcursorGetTheme(_sapp_tc.x11.display); + const int size = XcursorGetDefaultSize(_sapp_tc.x11.display); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_ARROW, "default", cursor_theme, size, XC_left_ptr); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_IBEAM, "text", cursor_theme, size, XC_xterm); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_CROSSHAIR, "crosshair", cursor_theme, size, XC_crosshair); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_POINTING_HAND, "pointer", cursor_theme, size, XC_hand2); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_RESIZE_EW, "ew-resize", cursor_theme, size, XC_sb_h_double_arrow); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_RESIZE_NS, "ns-resize", cursor_theme, size, XC_sb_v_double_arrow); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_RESIZE_NWSE, "nwse-resize", cursor_theme, size, 0); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_RESIZE_NESW, "nesw-resize", cursor_theme, size, 0); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_RESIZE_ALL, "all-scroll", cursor_theme, size, XC_fleur); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_NOT_ALLOWED, "not-allowed", cursor_theme, size, 0); + _sapp_tc_x11_create_hidden_cursor(); +} + +_SOKOL_PRIVATE void _sapp_tc_x11_destroy_standard_cursors(void) { + SOKOL_ASSERT(_sapp_tc.x11.display); + if (_sapp_tc.x11.hidden_cursor) { + XFreeCursor(_sapp_tc.x11.display, _sapp_tc.x11.hidden_cursor); + _sapp_tc.x11.hidden_cursor = 0; + } + for (int i = 0; i < _SAPP_MOUSECURSOR_NUM; i++) { + if (_sapp_tc.x11.standard_cursors[i]) { + XFreeCursor(_sapp_tc.x11.display, _sapp_tc.x11.standard_cursors[i]); + _sapp_tc.x11.standard_cursors[i] = 0; + } + } +} + +_SOKOL_PRIVATE bool _sapp_tc_x11_make_custom_mouse_cursor(sapp_mouse_cursor cursor, const sapp_image_desc* desc) { + SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); + SOKOL_ASSERT(0 == _sapp_tc.x11.custom_cursors[cursor]); + XcursorImage* img = XcursorImageCreate(desc->width, desc->height); + SOKOL_ASSERT(img && ((int) img->width == desc->width) && ((int) img->height == desc->height) && img->pixels); + img->xhot = (XcursorDim) desc->cursor_hotspot_x; + img->yhot = (XcursorDim) desc->cursor_hotspot_y; + const size_t dest_num_bytes = (size_t)(img->width * img->height) * sizeof(XcursorPixel); + SOKOL_ASSERT(dest_num_bytes == desc->pixels.size); + // Copy RGBA -> BGRA + for (size_t i = 0; i < dest_num_bytes; i += 4) { + ((uint8_t*) img->pixels)[i+0] = ((uint8_t*) desc->pixels.ptr)[i+2]; + ((uint8_t*) img->pixels)[i+1] = ((uint8_t*) desc->pixels.ptr)[i+1]; + ((uint8_t*) img->pixels)[i+2] = ((uint8_t*) desc->pixels.ptr)[i+0]; + ((uint8_t*) img->pixels)[i+3] = ((uint8_t*) desc->pixels.ptr)[i+3]; + } + _sapp_tc.x11.custom_cursors[cursor] = XcursorImageLoadCursor(_sapp_tc.x11.display, img); + XcursorImageDestroy(img); + return 0 != _sapp_tc.x11.custom_cursors[cursor]; +} + +_SOKOL_PRIVATE void _sapp_tc_x11_destroy_custom_mouse_cursor(sapp_mouse_cursor cursor) { + SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); + Cursor xcursor = _sapp_tc.x11.custom_cursors[cursor]; + _sapp_tc.x11.custom_cursors[cursor] = 0; + SOKOL_ASSERT(xcursor); + XFreeCursor(_sapp_tc.x11.display, xcursor); +} + +_SOKOL_PRIVATE void _sapp_tc_x11_toggle_fullscreen(void) { + _sapp_tc.fullscreen = !_sapp_tc.fullscreen; + _sapp_tc_x11_set_fullscreen(_sapp_tc.fullscreen); + _sapp_tc_x11_update_dimensions_from_window_size(); +} + +_SOKOL_PRIVATE void _sapp_tc_x11_update_cursor(sapp_mouse_cursor cursor, bool shown) { + SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); + if (shown) { + if (_sapp_tc.custom_cursor_bound[cursor]) { + Cursor xcursor = _sapp_tc.x11.custom_cursors[cursor]; + SOKOL_ASSERT(0 != xcursor); + XDefineCursor(_sapp_tc.x11.display, _sapp_tc.x11.window, xcursor); + } else if (_sapp_tc.x11.standard_cursors[cursor]) { + XDefineCursor(_sapp_tc.x11.display, _sapp_tc.x11.window, _sapp_tc.x11.standard_cursors[cursor]); + } else { + XUndefineCursor(_sapp_tc.x11.display, _sapp_tc.x11.window); + } + } else { + XDefineCursor(_sapp_tc.x11.display, _sapp_tc.x11.window, _sapp_tc.x11.hidden_cursor); + } + XFlush(_sapp_tc.x11.display); +} + +_SOKOL_PRIVATE void _sapp_tc_x11_lock_mouse(bool lock) { + if (lock == _sapp_tc.mouse.locked) { + return; + } + _sapp_tc.mouse.dx = 0.0f; + _sapp_tc.mouse.dy = 0.0f; + _sapp_tc.mouse.locked = lock; + if (_sapp_tc.mouse.locked) { + if (_sapp_tc.x11.xi.available) { + XIEventMask em; + unsigned char mask[XIMaskLen(XI_RawMotion)] = { 0 }; // XIMaskLen is a macro + em.deviceid = XIAllMasterDevices; + em.mask_len = sizeof(mask); + em.mask = mask; + XISetMask(mask, XI_RawMotion); + XISelectEvents(_sapp_tc.x11.display, _sapp_tc.x11.root, &em, 1); + } + XGrabPointer(_sapp_tc.x11.display, // display + _sapp_tc.x11.window, // grab_window + True, // owner_events + ButtonPressMask | ButtonReleaseMask | PointerMotionMask, // event_mask + GrabModeAsync, // pointer_mode + GrabModeAsync, // keyboard_mode + _sapp_tc.x11.window, // confine_to + _sapp_tc.x11.hidden_cursor, // cursor + CurrentTime); // time + } else { + if (_sapp_tc.x11.xi.available) { + XIEventMask em; + unsigned char mask[] = { 0 }; + em.deviceid = XIAllMasterDevices; + em.mask_len = sizeof(mask); + em.mask = mask; + XISelectEvents(_sapp_tc.x11.display, _sapp_tc.x11.root, &em, 1); + } + XWarpPointer(_sapp_tc.x11.display, None, _sapp_tc.x11.window, 0, 0, 0, 0, (int) _sapp_tc.mouse.x, _sapp_tc.mouse.y); + XUngrabPointer(_sapp_tc.x11.display, CurrentTime); + } + XFlush(_sapp_tc.x11.display); +} + +_SOKOL_PRIVATE void _sapp_tc_x11_set_clipboard_string(const char* str) { + SOKOL_ASSERT(_sapp_tc.clipboard.enabled && _sapp_tc.clipboard.buffer); + if (strlen(str) >= (size_t)_sapp_tc.clipboard.buf_size) { + _SAPP_ERROR(CLIPBOARD_STRING_TOO_BIG); + } + XSetSelectionOwner(_sapp_tc.x11.display, _sapp_tc.x11.CLIPBOARD, _sapp_tc.x11.window, CurrentTime); + if (XGetSelectionOwner(_sapp_tc.x11.display, _sapp_tc.x11.CLIPBOARD) != _sapp_tc.x11.window) { + _SAPP_ERROR(LINUX_X11_FAILED_TO_BECOME_OWNER_OF_CLIPBOARD); + } +} + +_SOKOL_PRIVATE const char* _sapp_tc_x11_get_clipboard_string(void) { + SOKOL_ASSERT(_sapp_tc.clipboard.enabled && _sapp_tc.clipboard.buffer); + Atom none = XInternAtom(_sapp_tc.x11.display, "SAPP_SELECTION", False); + Atom incremental = XInternAtom(_sapp_tc.x11.display, "INCR", False); + if (XGetSelectionOwner(_sapp_tc.x11.display, _sapp_tc.x11.CLIPBOARD) == _sapp_tc.x11.window) { + // Instead of doing a large number of X round-trips just to put this + // string into a window property and then read it back, just return it + return _sapp_tc.clipboard.buffer; + } + XConvertSelection(_sapp_tc.x11.display, + _sapp_tc.x11.CLIPBOARD, + _sapp_tc.x11.UTF8_STRING, + none, + _sapp_tc.x11.window, + CurrentTime); + XEvent event; + if (!_sapp_tc_x11_wait_for_event(SelectionNotify, 0.1, &event)) { + return NULL; + } + if (event.xselection.property == None) { + return NULL; + } + char* data = NULL; + Atom actualType; + int actualFormat; + unsigned long itemCount, bytesAfter; + const bool ret = XGetWindowProperty(_sapp_tc.x11.display, + event.xselection.requestor, + event.xselection.property, + 0, + LONG_MAX, + True, + _sapp_tc.x11.UTF8_STRING, + &actualType, + &actualFormat, + &itemCount, + &bytesAfter, + (unsigned char**) &data); + if (ret != Success || data == NULL) { + if (data != NULL) { + XFree(data); + } + return NULL; + } + if ((actualType == incremental) || (itemCount >= (size_t)_sapp_tc.clipboard.buf_size)) { + _SAPP_ERROR(CLIPBOARD_STRING_TOO_BIG); + XFree(data); + return NULL; + } + _sapp_tc_strcpy(data, _sapp_tc.clipboard.buffer, (size_t)_sapp_tc.clipboard.buf_size); + XFree(data); + return _sapp_tc.clipboard.buffer; +} + +_SOKOL_PRIVATE void _sapp_tc_x11_update_window_title(void) { + Xutf8SetWMProperties(_sapp_tc.x11.display, + _sapp_tc.x11.window, + _sapp_tc.window_title, _sapp_tc.window_title, + NULL, 0, NULL, NULL, NULL); + XChangeProperty(_sapp_tc.x11.display, _sapp_tc.x11.window, + _sapp_tc.x11.NET_WM_NAME, _sapp_tc.x11.UTF8_STRING, 8, + PropModeReplace, + (unsigned char*)_sapp_tc.window_title, + strlen(_sapp_tc.window_title)); + XChangeProperty(_sapp_tc.x11.display, _sapp_tc.x11.window, + _sapp_tc.x11.NET_WM_ICON_NAME, _sapp_tc.x11.UTF8_STRING, 8, + PropModeReplace, + (unsigned char*)_sapp_tc.window_title, + strlen(_sapp_tc.window_title)); + XFlush(_sapp_tc.x11.display); +} + +_SOKOL_PRIVATE void _sapp_tc_x11_set_icon(const sapp_icon_desc* icon_desc, int num_images) { + SOKOL_ASSERT((num_images > 0) && (num_images <= SAPP_MAX_ICONIMAGES)); + int long_count = 0; + for (int i = 0; i < num_images; i++) { + const sapp_image_desc* img_desc = &icon_desc->images[i]; + long_count += 2 + (img_desc->width * img_desc->height); + } + long* icon_data = (long*) _sapp_tc_malloc_clear((size_t)long_count * sizeof(long)); + SOKOL_ASSERT(icon_data); + long* dst = icon_data; + for (int img_index = 0; img_index < num_images; img_index++) { + const sapp_image_desc* img_desc = &icon_desc->images[img_index]; + const uint8_t* src = (const uint8_t*) img_desc->pixels.ptr; + *dst++ = img_desc->width; + *dst++ = img_desc->height; + const int num_pixels = img_desc->width * img_desc->height; + for (int pixel_index = 0; pixel_index < num_pixels; pixel_index++) { + *dst++ = ((long)(src[pixel_index * 4 + 0]) << 16) | + ((long)(src[pixel_index * 4 + 1]) << 8) | + ((long)(src[pixel_index * 4 + 2]) << 0) | + ((long)(src[pixel_index * 4 + 3]) << 24); + } + } + XChangeProperty(_sapp_tc.x11.display, _sapp_tc.x11.window, + _sapp_tc.x11.NET_WM_ICON, + XA_CARDINAL, 32, + PropModeReplace, + (unsigned char*)icon_data, + long_count); + _sapp_tc_free(icon_data); + XFlush(_sapp_tc.x11.display); +} + +_SOKOL_PRIVATE void _sapp_tc_x11_create_window(Visual* visual_or_null, int depth) { + Visual* visual = visual_or_null; + if (0 == visual_or_null) { + visual = DefaultVisual(_sapp_tc.x11.display, _sapp_tc.x11.screen); + depth = DefaultDepth(_sapp_tc.x11.display, _sapp_tc.x11.screen); + } + _sapp_tc.x11.colormap = XCreateColormap(_sapp_tc.x11.display, _sapp_tc.x11.root, visual, AllocNone); + _SAPP_STRUCT(XSetWindowAttributes, wa); + const uint32_t wamask = CWBorderPixel | CWColormap | CWEventMask; + wa.colormap = _sapp_tc.x11.colormap; + wa.border_pixel = 0; + wa.event_mask = StructureNotifyMask | KeyPressMask | KeyReleaseMask | + PointerMotionMask | ButtonPressMask | ButtonReleaseMask | + ExposureMask | FocusChangeMask | VisibilityChangeMask | + EnterWindowMask | LeaveWindowMask | PropertyChangeMask; + + int display_width = DisplayWidth(_sapp_tc.x11.display, _sapp_tc.x11.screen); + int display_height = DisplayHeight(_sapp_tc.x11.display, _sapp_tc.x11.screen); + // NOTE: do *NOT* use _sapp_tc.dpi_scale for the size multiplicator! + const float window_scale = _sapp_tc.x11.dpi / 96.0f; + int x11_window_width = _sapp_tc_roundf_gzero(_sapp_tc.window_width * window_scale); + int x11_window_height = _sapp_tc_roundf_gzero(_sapp_tc.window_height * window_scale); + if (0 == _sapp_tc.window_width) { + x11_window_width = (display_width * 4) / 5; + } + if (0 == _sapp_tc.window_height) { + x11_window_height = (display_height * 4) / 5; + } + _sapp_tc_x11_grab_error_handler(); + _sapp_tc.x11.window = XCreateWindow(_sapp_tc.x11.display, + _sapp_tc.x11.root, + 0, 0, + (uint32_t)x11_window_width, + (uint32_t)x11_window_height, + 0, /* border width */ + depth, /* color depth */ + InputOutput, + visual, + wamask, + &wa); + _sapp_tc_x11_release_error_handler(); + if (!_sapp_tc.x11.window) { + _SAPP_PANIC(LINUX_X11_CREATE_WINDOW_FAILED); + } + Atom protocols[] = { + _sapp_tc.x11.WM_DELETE_WINDOW + }; + XSetWMProtocols(_sapp_tc.x11.display, _sapp_tc.x11.window, protocols, 1); + + // NOTE: PPosition and PSize are obsolete and ignored + XSizeHints* hints = XAllocSizeHints(); + hints->flags = PWinGravity; + hints->win_gravity = CenterGravity; + XSetWMNormalHints(_sapp_tc.x11.display, _sapp_tc.x11.window, hints); + XFree(hints); + + // announce support for drag'n'drop + if (_sapp_tc.drop.enabled) { + const Atom version = _SAPP_X11_XDND_VERSION; + XChangeProperty(_sapp_tc.x11.display, _sapp_tc.x11.window, _sapp_tc.x11.xdnd.XdndAware, XA_ATOM, 32, PropModeReplace, (unsigned char*) &version, 1); + } + _sapp_tc_x11_update_window_title(); + _sapp_tc_x11_update_dimensions_from_window_size(); +} + +_SOKOL_PRIVATE void _sapp_tc_x11_destroy_window(void) { + if (_sapp_tc.x11.window) { + XUnmapWindow(_sapp_tc.x11.display, _sapp_tc.x11.window); + XDestroyWindow(_sapp_tc.x11.display, _sapp_tc.x11.window); + _sapp_tc.x11.window = 0; + } + if (_sapp_tc.x11.colormap) { + XFreeColormap(_sapp_tc.x11.display, _sapp_tc.x11.colormap); + _sapp_tc.x11.colormap = 0; + } + XFlush(_sapp_tc.x11.display); +} + +_SOKOL_PRIVATE bool _sapp_tc_x11_window_visible(void) { + XWindowAttributes wa; + XGetWindowAttributes(_sapp_tc.x11.display, _sapp_tc.x11.window, &wa); + return wa.map_state == IsViewable; +} + +_SOKOL_PRIVATE void _sapp_tc_x11_show_window(void) { + if (!_sapp_tc_x11_window_visible()) { + XMapWindow(_sapp_tc.x11.display, _sapp_tc.x11.window); + XEvent dummy; + _sapp_tc_x11_wait_for_event(VisibilityNotify, 0.1, &dummy); + XRaiseWindow(_sapp_tc.x11.display, _sapp_tc.x11.window); + XFlush(_sapp_tc.x11.display); + } +} + +_SOKOL_PRIVATE void _sapp_tc_x11_hide_window(void) { + XUnmapWindow(_sapp_tc.x11.display, _sapp_tc.x11.window); + XFlush(_sapp_tc.x11.display); +} + +_SOKOL_PRIVATE unsigned long _sapp_tc_x11_get_window_property(Window window, Atom property, Atom type, unsigned char** value) { + Atom actualType; + int actualFormat; + unsigned long itemCount, bytesAfter; + XGetWindowProperty(_sapp_tc.x11.display, + window, + property, + 0, + LONG_MAX, + False, + type, + &actualType, + &actualFormat, + &itemCount, + &bytesAfter, + value); + return itemCount; +} + +_SOKOL_PRIVATE int _sapp_tc_x11_get_window_state(void) { + int result = WithdrawnState; + struct { + CARD32 state; + Window icon; + } *state = NULL; + + if (_sapp_tc_x11_get_window_property(_sapp_tc.x11.window, _sapp_tc.x11.WM_STATE, _sapp_tc.x11.WM_STATE, (unsigned char**)&state) >= 2) { + result = (int)state->state; + } + if (state) { + XFree(state); + } + return result; +} + +_SOKOL_PRIVATE uint32_t _sapp_tc_x11_key_modifier_bit(sapp_keycode key) { + switch (key) { + case SAPP_KEYCODE_LEFT_SHIFT: + case SAPP_KEYCODE_RIGHT_SHIFT: + return SAPP_MODIFIER_SHIFT; + case SAPP_KEYCODE_LEFT_CONTROL: + case SAPP_KEYCODE_RIGHT_CONTROL: + return SAPP_MODIFIER_CTRL; + case SAPP_KEYCODE_LEFT_ALT: + case SAPP_KEYCODE_RIGHT_ALT: + return SAPP_MODIFIER_ALT; + case SAPP_KEYCODE_LEFT_SUPER: + case SAPP_KEYCODE_RIGHT_SUPER: + return SAPP_MODIFIER_SUPER; + default: + return 0; + } +} + +_SOKOL_PRIVATE uint32_t _sapp_tc_x11_button_modifier_bit(sapp_mousebutton btn) { + switch (btn) { + case SAPP_MOUSEBUTTON_LEFT: return SAPP_MODIFIER_LMB; + case SAPP_MOUSEBUTTON_RIGHT: return SAPP_MODIFIER_RMB; + case SAPP_MOUSEBUTTON_MIDDLE: return SAPP_MODIFIER_MMB; + default: return 0; + } +} + +_SOKOL_PRIVATE uint32_t _sapp_tc_x11_mods(uint32_t x11_mods) { + uint32_t mods = 0; + if (x11_mods & ShiftMask) { + mods |= SAPP_MODIFIER_SHIFT; + } + if (x11_mods & ControlMask) { + mods |= SAPP_MODIFIER_CTRL; + } + if (x11_mods & Mod1Mask) { + mods |= SAPP_MODIFIER_ALT; + } + if (x11_mods & Mod4Mask) { + mods |= SAPP_MODIFIER_SUPER; + } + if (x11_mods & Button1Mask) { + mods |= SAPP_MODIFIER_LMB; + } + if (x11_mods & Button2Mask) { + mods |= SAPP_MODIFIER_MMB; + } + if (x11_mods & Button3Mask) { + mods |= SAPP_MODIFIER_RMB; + } + return mods; +} + +_SOKOL_PRIVATE sapp_mousebutton _sapp_tc_x11_translate_button(const XEvent* event) { + switch (event->xbutton.button) { + case Button1: return SAPP_MOUSEBUTTON_LEFT; + case Button2: return SAPP_MOUSEBUTTON_MIDDLE; + case Button3: return SAPP_MOUSEBUTTON_RIGHT; + default: return SAPP_MOUSEBUTTON_INVALID; + } +} + +_SOKOL_PRIVATE void _sapp_tc_x11_mouse_update(int x, int y, bool clear_dxdy) { + if (!_sapp_tc.mouse.locked) { + const float new_x = (float)x; + const float new_y = (float)y; + if (clear_dxdy) { + _sapp_tc.mouse.dx = 0.0f; + _sapp_tc.mouse.dy = 0.0f; + } else if (_sapp_tc.mouse.pos_valid) { + _sapp_tc.mouse.dx = new_x - _sapp_tc.mouse.x; + _sapp_tc.mouse.dy = new_y - _sapp_tc.mouse.y; + } + _sapp_tc.mouse.x = new_x; + _sapp_tc.mouse.y = new_y; + _sapp_tc.mouse.pos_valid = true; + } +} + +_SOKOL_PRIVATE void _sapp_tc_x11_mouse_event(sapp_event_type type, sapp_mousebutton btn, uint32_t mods) { + if (_sapp_tc_events_enabled()) { + _sapp_tc_init_event(type); + _sapp_tc.event.mouse_button = btn; + _sapp_tc.event.modifiers = mods; + _sapp_tc_call_event(&_sapp_tc.event); + } +} + +_SOKOL_PRIVATE void _sapp_tc_x11_scroll_event(float x, float y, uint32_t mods) { + if (_sapp_tc_events_enabled()) { + _sapp_tc_init_event(SAPP_EVENTTYPE_MOUSE_SCROLL); + _sapp_tc.event.modifiers = mods; + _sapp_tc.event.scroll_x = x; + _sapp_tc.event.scroll_y = y; + _sapp_tc_call_event(&_sapp_tc.event); + } +} + +_SOKOL_PRIVATE void _sapp_tc_x11_key_event(sapp_event_type type, sapp_keycode key, bool repeat, uint32_t mods) { + if (_sapp_tc_events_enabled()) { + _sapp_tc_init_event(type); + _sapp_tc.event.key_code = key; + _sapp_tc.event.key_repeat = repeat; + _sapp_tc.event.modifiers = mods; + _sapp_tc_call_event(&_sapp_tc.event); + /* check if a CLIPBOARD_PASTED event must be sent too */ + if (_sapp_tc.clipboard.enabled && + (type == SAPP_EVENTTYPE_KEY_DOWN) && + (_sapp_tc.event.modifiers == SAPP_MODIFIER_CTRL) && + (_sapp_tc.event.key_code == SAPP_KEYCODE_V)) + { + _sapp_tc_init_event(SAPP_EVENTTYPE_CLIPBOARD_PASTED); + _sapp_tc_call_event(&_sapp_tc.event); + } + } +} + +_SOKOL_PRIVATE void _sapp_tc_x11_char_event(uint32_t chr, bool repeat, uint32_t mods) { + if (_sapp_tc_events_enabled()) { + _sapp_tc_init_event(SAPP_EVENTTYPE_CHAR); + _sapp_tc.event.char_code = chr; + _sapp_tc.event.key_repeat = repeat; + _sapp_tc.event.modifiers = mods; + _sapp_tc_call_event(&_sapp_tc.event); + } +} + +_SOKOL_PRIVATE sapp_keycode _sapp_tc_x11_translate_key(int scancode) { + if ((scancode >= 0) && (scancode < _SAPP_X11_MAX_X11_KEYCODES)) { + return _sapp_tc.keycodes[scancode]; + } else { + return SAPP_KEYCODE_INVALID; + } +} + +_SOKOL_PRIVATE int32_t _sapp_tc_x11_keysym_to_unicode(KeySym keysym) { + int min = 0; + int max = sizeof(_sapp_tc_x11_keysymtab) / sizeof(struct _sapp_tc_x11_codepair) - 1; + int mid; + + /* First check for Latin-1 characters (1:1 mapping) */ + if ((keysym >= 0x0020 && keysym <= 0x007e) || + (keysym >= 0x00a0 && keysym <= 0x00ff)) + { + return keysym; + } + + /* Also check for directly encoded 24-bit UCS characters */ + if ((keysym & 0xff000000) == 0x01000000) { + return keysym & 0x00ffffff; + } + + /* Binary search in table */ + while (max >= min) { + mid = (min + max) / 2; + if (_sapp_tc_x11_keysymtab[mid].keysym < keysym) { + min = mid + 1; + } else if (_sapp_tc_x11_keysymtab[mid].keysym > keysym) { + max = mid - 1; + } else { + return _sapp_tc_x11_keysymtab[mid].ucs; + } + } + + /* No matching Unicode value found */ + return -1; +} + +_SOKOL_PRIVATE bool _sapp_tc_x11_keypress_repeat(int keycode) { + bool repeat = false; + if ((keycode >= 0) && (keycode < _SAPP_X11_MAX_X11_KEYCODES)) { + repeat = _sapp_tc.x11.key_repeat[keycode]; + _sapp_tc.x11.key_repeat[keycode] = true; + } + return repeat; +} + +_SOKOL_PRIVATE void _sapp_tc_x11_keyrelease_repeat(int keycode) { + if ((keycode >= 0) && (keycode < _SAPP_X11_MAX_X11_KEYCODES)) { + _sapp_tc.x11.key_repeat[keycode] = false; + } +} + +_SOKOL_PRIVATE bool _sapp_tc_x11_parse_dropped_files_list(const char* src) { + SOKOL_ASSERT(src); + SOKOL_ASSERT(_sapp_tc.drop.buffer); + + _sapp_tc_clear_drop_buffer(); + _sapp_tc.drop.num_files = 0; + + /* + src is (potentially percent-encoded) string made of one or multiple paths + separated by \r\n, each path starting with 'file://' + */ + bool err = false; + int src_count = 0; + char src_chr = 0; + char* dst_ptr = _sapp_tc.drop.buffer; + const char* dst_end_ptr = dst_ptr + (_sapp_tc.drop.max_path_length - 1); // room for terminating 0 + while (0 != (src_chr = *src++)) { + src_count++; + char dst_chr = 0; + /* check leading 'file://' */ + if (src_count <= 7) { + if (((src_count == 1) && (src_chr != 'f')) || + ((src_count == 2) && (src_chr != 'i')) || + ((src_count == 3) && (src_chr != 'l')) || + ((src_count == 4) && (src_chr != 'e')) || + ((src_count == 5) && (src_chr != ':')) || + ((src_count == 6) && (src_chr != '/')) || + ((src_count == 7) && (src_chr != '/'))) + { + _SAPP_ERROR(LINUX_X11_DROPPED_FILE_URI_WRONG_SCHEME); + err = true; + break; + } + } else if (src_chr == '\r') { + // skip + } else if (src_chr == '\n') { + src_count = 0; + _sapp_tc.drop.num_files++; + // too many files is not an error + if (_sapp_tc.drop.num_files >= _sapp_tc.drop.max_files) { + break; + } + dst_ptr = _sapp_tc.drop.buffer + _sapp_tc.drop.num_files * _sapp_tc.drop.max_path_length; + dst_end_ptr = dst_ptr + (_sapp_tc.drop.max_path_length - 1); + } else if ((src_chr == '%') && src[0] && src[1]) { + // a percent-encoded byte (most likely UTF-8 multibyte sequence) + const char digits[3] = { src[0], src[1], 0 }; + src += 2; + dst_chr = (char) strtol(digits, 0, 16); + } else { + dst_chr = src_chr; + } + if (dst_chr) { + // dst_end_ptr already has adjustment for terminating zero + if (dst_ptr < dst_end_ptr) { + *dst_ptr++ = dst_chr; + } else { + _SAPP_ERROR(DROPPED_FILE_PATH_TOO_LONG); + err = true; + break; + } + } + } + if (err) { + _sapp_tc_clear_drop_buffer(); + _sapp_tc.drop.num_files = 0; + return false; + } else { + return true; + } +} + +_SOKOL_PRIVATE void _sapp_tc_x11_on_genericevent(XEvent* event) { + if (_sapp_tc.mouse.locked && _sapp_tc.x11.xi.available) { + if (event->xcookie.extension == _sapp_tc.x11.xi.major_opcode) { + if (XGetEventData(_sapp_tc.x11.display, &event->xcookie)) { + if (event->xcookie.evtype == XI_RawMotion) { + XIRawEvent* re = (XIRawEvent*) event->xcookie.data; + if (re->valuators.mask_len) { + const double* values = re->raw_values; + if (XIMaskIsSet(re->valuators.mask, 0)) { + _sapp_tc.mouse.dx = (float) *values; + values++; + } + if (XIMaskIsSet(re->valuators.mask, 1)) { + _sapp_tc.mouse.dy = (float) *values; + } + _sapp_tc_x11_mouse_event(SAPP_EVENTTYPE_MOUSE_MOVE, SAPP_MOUSEBUTTON_INVALID, _sapp_tc_x11_mods(event->xmotion.state)); + } + } + XFreeEventData(_sapp_tc.x11.display, &event->xcookie); + } + } + } +} + +_SOKOL_PRIVATE void _sapp_tc_x11_on_focusin(XEvent* event) { + // NOTE: ignoring NotifyGrab and NotifyUngrab is same behaviour as GLFW + if ((event->xfocus.mode != NotifyGrab) && (event->xfocus.mode != NotifyUngrab)) { + _sapp_tc_x11_app_event(SAPP_EVENTTYPE_FOCUSED); + } +} + +_SOKOL_PRIVATE void _sapp_tc_x11_on_focusout(XEvent* event) { + // if focus is lost for any reason, and we're in mouse locked mode, disable mouse lock + if (_sapp_tc.mouse.locked) { + _sapp_tc_x11_lock_mouse(false); + } + // NOTE: ignoring NotifyGrab and NotifyUngrab is same behaviour as GLFW + if ((event->xfocus.mode != NotifyGrab) && (event->xfocus.mode != NotifyUngrab)) { + _sapp_tc_x11_app_event(SAPP_EVENTTYPE_UNFOCUSED); + } +} + +_SOKOL_PRIVATE void _sapp_tc_x11_on_keypress(XEvent* event) { + int keycode = (int)event->xkey.keycode; + + const sapp_keycode key = _sapp_tc_x11_translate_key(keycode); + const bool repeat = _sapp_tc_x11_keypress_repeat(keycode); + uint32_t mods = _sapp_tc_x11_mods(event->xkey.state); + // X11 doesn't set modifier bit on key down, so emulate that + mods |= _sapp_tc_x11_key_modifier_bit(key); + if (key != SAPP_KEYCODE_INVALID) { + _sapp_tc_x11_key_event(SAPP_EVENTTYPE_KEY_DOWN, key, repeat, mods); + } + KeySym keysym; + XLookupString(&event->xkey, NULL, 0, &keysym, NULL); + int32_t chr = _sapp_tc_x11_keysym_to_unicode(keysym); + if (chr > 0) { + _sapp_tc_x11_char_event((uint32_t)chr, repeat, mods); + } +} + +_SOKOL_PRIVATE void _sapp_tc_x11_on_keyrelease(XEvent* event) { + int keycode = (int)event->xkey.keycode; + const sapp_keycode key = _sapp_tc_x11_translate_key(keycode); + _sapp_tc_x11_keyrelease_repeat(keycode); + if (key != SAPP_KEYCODE_INVALID) { + uint32_t mods = _sapp_tc_x11_mods(event->xkey.state); + // X11 doesn't clear modifier bit on key up, so emulate that + mods &= ~_sapp_tc_x11_key_modifier_bit(key); + _sapp_tc_x11_key_event(SAPP_EVENTTYPE_KEY_UP, key, false, mods); + } +} + +_SOKOL_PRIVATE void _sapp_tc_x11_on_buttonpress(XEvent* event) { + _sapp_tc_x11_mouse_update(event->xbutton.x, event->xbutton.y, false); + const sapp_mousebutton btn = _sapp_tc_x11_translate_button(event); + uint32_t mods = _sapp_tc_x11_mods(event->xbutton.state); + // X11 doesn't set modifier bit on button down, so emulate that + mods |= _sapp_tc_x11_button_modifier_bit(btn); + if (btn != SAPP_MOUSEBUTTON_INVALID) { + _sapp_tc_x11_mouse_event(SAPP_EVENTTYPE_MOUSE_DOWN, btn, mods); + _sapp_tc.x11.mouse_buttons |= (1 << btn); + } else { + // might be a scroll event + switch (event->xbutton.button) { + case 4: _sapp_tc_x11_scroll_event(0.0f, 1.0f, mods); break; + case 5: _sapp_tc_x11_scroll_event(0.0f, -1.0f, mods); break; + case 6: _sapp_tc_x11_scroll_event(1.0f, 0.0f, mods); break; + case 7: _sapp_tc_x11_scroll_event(-1.0f, 0.0f, mods); break; + } + } +} + +_SOKOL_PRIVATE void _sapp_tc_x11_on_buttonrelease(XEvent* event) { + _sapp_tc_x11_mouse_update(event->xbutton.x, event->xbutton.y, false); + const sapp_mousebutton btn = _sapp_tc_x11_translate_button(event); + if (btn != SAPP_MOUSEBUTTON_INVALID) { + uint32_t mods = _sapp_tc_x11_mods(event->xbutton.state); + // X11 doesn't clear modifier bit on button up, so emulate that + mods &= ~_sapp_tc_x11_button_modifier_bit(btn); + _sapp_tc_x11_mouse_event(SAPP_EVENTTYPE_MOUSE_UP, btn, mods); + _sapp_tc.x11.mouse_buttons &= ~(1 << btn); + } +} + +_SOKOL_PRIVATE void _sapp_tc_x11_on_enternotify(XEvent* event) { + // don't send enter/leave events while mouse button held down + if (0 == _sapp_tc.x11.mouse_buttons) { + _sapp_tc_x11_mouse_update(event->xcrossing.x, event->xcrossing.y, true); + _sapp_tc_x11_mouse_event(SAPP_EVENTTYPE_MOUSE_ENTER, SAPP_MOUSEBUTTON_INVALID, _sapp_tc_x11_mods(event->xcrossing.state)); + } +} + +_SOKOL_PRIVATE void _sapp_tc_x11_on_leavenotify(XEvent* event) { + if (0 == _sapp_tc.x11.mouse_buttons) { + _sapp_tc_x11_mouse_update(event->xcrossing.x, event->xcrossing.y, true); + _sapp_tc_x11_mouse_event(SAPP_EVENTTYPE_MOUSE_LEAVE, SAPP_MOUSEBUTTON_INVALID, _sapp_tc_x11_mods(event->xcrossing.state)); + } +} + +_SOKOL_PRIVATE void _sapp_tc_x11_on_motionnotify(XEvent* event) { + if (!_sapp_tc.mouse.locked) { + _sapp_tc_x11_mouse_update(event->xmotion.x, event->xmotion.y, false); + _sapp_tc_x11_mouse_event(SAPP_EVENTTYPE_MOUSE_MOVE, SAPP_MOUSEBUTTON_INVALID, _sapp_tc_x11_mods(event->xmotion.state)); + } +} + +_SOKOL_PRIVATE void _sapp_tc_x11_on_propertynotify(XEvent* event) { + if (event->xproperty.state == PropertyNewValue) { + if (event->xproperty.atom == _sapp_tc.x11.WM_STATE) { + const int state = _sapp_tc_x11_get_window_state(); + if (state != _sapp_tc.x11.window_state) { + _sapp_tc.x11.window_state = state; + if (state == IconicState) { + _sapp_tc_x11_app_event(SAPP_EVENTTYPE_ICONIFIED); + } else if (state == NormalState) { + _sapp_tc_x11_app_event(SAPP_EVENTTYPE_RESTORED); + } + } + } + } +} + +_SOKOL_PRIVATE void _sapp_tc_x11_on_selectionnotify(XEvent* event) { + if (event->xselection.property == _sapp_tc.x11.xdnd.XdndSelection) { + char* data = 0; + uint32_t result = _sapp_tc_x11_get_window_property(event->xselection.requestor, + event->xselection.property, + event->xselection.target, + (unsigned char**) &data); + if (_sapp_tc.drop.enabled && result) { + if (_sapp_tc_x11_parse_dropped_files_list(data)) { + _sapp_tc.mouse.dx = 0.0f; + _sapp_tc.mouse.dy = 0.0f; + if (_sapp_tc_events_enabled()) { + // FIXME: Figure out how to get modifier key state here. + // The XSelection event has no 'state' item, and + // XQueryKeymap() always returns a zeroed array. + _sapp_tc_init_event(SAPP_EVENTTYPE_FILES_DROPPED); + _sapp_tc_call_event(&_sapp_tc.event); + } + } + } + if (_sapp_tc.x11.xdnd.version >= 2) { + _SAPP_STRUCT(XEvent, reply); + reply.type = ClientMessage; + reply.xclient.window = _sapp_tc.x11.xdnd.source; + reply.xclient.message_type = _sapp_tc.x11.xdnd.XdndFinished; + reply.xclient.format = 32; + reply.xclient.data.l[0] = (long)_sapp_tc.x11.window; + reply.xclient.data.l[1] = result; + reply.xclient.data.l[2] = (long)_sapp_tc.x11.xdnd.XdndActionCopy; + XSendEvent(_sapp_tc.x11.display, _sapp_tc.x11.xdnd.source, False, NoEventMask, &reply); + XFlush(_sapp_tc.x11.display); + } + if (data) { + XFree(data); + } + } +} + +_SOKOL_PRIVATE void _sapp_tc_x11_on_clientmessage(XEvent* event) { + if (XFilterEvent(event, None)) { + return; + } + if (event->xclient.message_type == _sapp_tc.x11.WM_PROTOCOLS) { + const Atom protocol = (Atom)event->xclient.data.l[0]; + if (protocol == _sapp_tc.x11.WM_DELETE_WINDOW) { + _sapp_tc.quit_requested = true; + } + } else if (event->xclient.message_type == _sapp_tc.x11.xdnd.XdndEnter) { + const bool is_list = 0 != (event->xclient.data.l[1] & 1); + _sapp_tc.x11.xdnd.source = (Window)event->xclient.data.l[0]; + _sapp_tc.x11.xdnd.version = event->xclient.data.l[1] >> 24; + _sapp_tc.x11.xdnd.format = None; + if (_sapp_tc.x11.xdnd.version > _SAPP_X11_XDND_VERSION) { + return; + } + uint32_t count = 0; + Atom* formats = 0; + if (is_list) { + count = _sapp_tc_x11_get_window_property(_sapp_tc.x11.xdnd.source, _sapp_tc.x11.xdnd.XdndTypeList, XA_ATOM, (unsigned char**)&formats); + } else { + count = 3; + formats = (Atom*) event->xclient.data.l + 2; + } + for (uint32_t i = 0; i < count; i++) { + if (formats[i] == _sapp_tc.x11.xdnd.text_uri_list) { + _sapp_tc.x11.xdnd.format = _sapp_tc.x11.xdnd.text_uri_list; + break; + } + } + if (is_list && formats) { + XFree(formats); + } + } else if (event->xclient.message_type == _sapp_tc.x11.xdnd.XdndDrop) { + if (_sapp_tc.x11.xdnd.version > _SAPP_X11_XDND_VERSION) { + return; + } + Time time = CurrentTime; + if (_sapp_tc.x11.xdnd.format) { + if (_sapp_tc.x11.xdnd.version >= 1) { + time = (Time)event->xclient.data.l[2]; + } + XConvertSelection(_sapp_tc.x11.display, + _sapp_tc.x11.xdnd.XdndSelection, + _sapp_tc.x11.xdnd.format, + _sapp_tc.x11.xdnd.XdndSelection, + _sapp_tc.x11.window, + time); + } else if (_sapp_tc.x11.xdnd.version >= 2) { + _SAPP_STRUCT(XEvent, reply); + reply.type = ClientMessage; + reply.xclient.window = _sapp_tc.x11.xdnd.source; + reply.xclient.message_type = _sapp_tc.x11.xdnd.XdndFinished; + reply.xclient.format = 32; + reply.xclient.data.l[0] = (long)_sapp_tc.x11.window; + reply.xclient.data.l[1] = 0; // drag was rejected + reply.xclient.data.l[2] = None; + XSendEvent(_sapp_tc.x11.display, _sapp_tc.x11.xdnd.source, False, NoEventMask, &reply); + XFlush(_sapp_tc.x11.display); + } + } else if (event->xclient.message_type == _sapp_tc.x11.xdnd.XdndPosition) { + // drag operation has moved over the window + // FIXME: we could track the mouse position here, but + // this isn't implemented on other platforms either so far + if (_sapp_tc.x11.xdnd.version > _SAPP_X11_XDND_VERSION) { + return; + } + _SAPP_STRUCT(XEvent, reply); + reply.type = ClientMessage; + reply.xclient.window = _sapp_tc.x11.xdnd.source; + reply.xclient.message_type = _sapp_tc.x11.xdnd.XdndStatus; + reply.xclient.format = 32; + reply.xclient.data.l[0] = (long)_sapp_tc.x11.window; + if (_sapp_tc.x11.xdnd.format) { + /* reply that we are ready to copy the dragged data */ + reply.xclient.data.l[1] = 1; // accept with no rectangle + if (_sapp_tc.x11.xdnd.version >= 2) { + reply.xclient.data.l[4] = (long)_sapp_tc.x11.xdnd.XdndActionCopy; + } + } + XSendEvent(_sapp_tc.x11.display, _sapp_tc.x11.xdnd.source, False, NoEventMask, &reply); + XFlush(_sapp_tc.x11.display); + } +} + +_SOKOL_PRIVATE void _sapp_tc_x11_on_selectionrequest(XEvent* event) { + XSelectionRequestEvent* req = &event->xselectionrequest; + if (req->selection != _sapp_tc.x11.CLIPBOARD) { + return; + } + if (!_sapp_tc.clipboard.enabled) { + return; + } + SOKOL_ASSERT(_sapp_tc.clipboard.buffer); + _SAPP_STRUCT(XSelectionEvent, reply); + reply.type = SelectionNotify; + reply.display = req->display; + reply.requestor = req->requestor; + reply.selection = req->selection; + reply.target = req->target; + reply.property = req->property; + reply.time = req->time; + if (req->target == _sapp_tc.x11.UTF8_STRING) { + XChangeProperty(_sapp_tc.x11.display, + req->requestor, + req->property, + _sapp_tc.x11.UTF8_STRING, + 8, + PropModeReplace, + (unsigned char*) _sapp_tc.clipboard.buffer, + strlen(_sapp_tc.clipboard.buffer)); + } else if (req->target == _sapp_tc.x11.TARGETS) { + XChangeProperty(_sapp_tc.x11.display, + req->requestor, + req->property, + XA_ATOM, + 32, + PropModeReplace, + (unsigned char*) &_sapp_tc.x11.UTF8_STRING, + 1); + } else { + reply.property = None; + } + XSendEvent(_sapp_tc.x11.display, req->requestor, False, 0, (XEvent*) &reply); +} + +_SOKOL_PRIVATE void _sapp_tc_x11_process_event(XEvent* event) { + switch (event->type) { + case GenericEvent: + _sapp_tc_x11_on_genericevent(event); + break; + case FocusIn: + _sapp_tc_x11_on_focusin(event); + break; + case FocusOut: + _sapp_tc_x11_on_focusout(event); + break; + case KeyPress: + _sapp_tc_x11_on_keypress(event); + break; + case KeyRelease: + _sapp_tc_x11_on_keyrelease(event); + break; + case ButtonPress: + _sapp_tc_x11_on_buttonpress(event); + break; + case ButtonRelease: + _sapp_tc_x11_on_buttonrelease(event); + break; + case EnterNotify: + _sapp_tc_x11_on_enternotify(event); + break; + case LeaveNotify: + _sapp_tc_x11_on_leavenotify(event); + break; + case MotionNotify: + _sapp_tc_x11_on_motionnotify(event); + break; + case PropertyNotify: + _sapp_tc_x11_on_propertynotify(event); + break; + case SelectionNotify: + _sapp_tc_x11_on_selectionnotify(event); + break; + case SelectionRequest: + _sapp_tc_x11_on_selectionrequest(event); + break; + case DestroyNotify: + // not a bug + break; + case ClientMessage: + _sapp_tc_x11_on_clientmessage(event); + break; + } +} + +#if defined(_SAPP_EGL) + +_SOKOL_PRIVATE void _sapp_tc_egl_init(void) { + #if defined(SOKOL_GLCORE) + if (!eglBindAPI(EGL_OPENGL_API)) { + _SAPP_PANIC(LINUX_EGL_BIND_OPENGL_API_FAILED); + } + #else + if (!eglBindAPI(EGL_OPENGL_ES_API)) { + _SAPP_PANIC(LINUX_EGL_BIND_OPENGL_ES_API_FAILED); + } + #endif + + _sapp_tc.egl.display = eglGetDisplay((EGLNativeDisplayType)_sapp_tc.x11.display); + if (EGL_NO_DISPLAY == _sapp_tc.egl.display) { + _SAPP_PANIC(LINUX_EGL_GET_DISPLAY_FAILED); + } + + EGLint major, minor; + if (!eglInitialize(_sapp_tc.egl.display, &major, &minor)) { + _SAPP_PANIC(LINUX_EGL_INITIALIZE_FAILED); + } + + EGLint sample_count = _sapp_tc.desc.sample_count > 1 ? _sapp_tc.desc.sample_count : 0; + EGLint alpha_size = _sapp_tc.desc.alpha ? 8 : 0; + const EGLint config_attrs[] = { + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + #if defined(SOKOL_GLCORE) + EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, + #elif defined(SOKOL_GLES3) + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, + #endif + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, alpha_size, + EGL_DEPTH_SIZE, 24, + EGL_STENCIL_SIZE, 8, + EGL_SAMPLE_BUFFERS, _sapp_tc.desc.sample_count > 1 ? 1 : 0, + EGL_SAMPLES, sample_count, + EGL_NONE, + }; + + EGLConfig egl_configs[32]; + EGLint config_count; + if (!eglChooseConfig(_sapp_tc.egl.display, config_attrs, egl_configs, 32, &config_count) || config_count == 0) { + _SAPP_PANIC(LINUX_EGL_NO_CONFIGS); + } + + EGLConfig config = egl_configs[0]; + for (int i = 0; i < config_count; ++i) { + EGLConfig c = egl_configs[i]; + EGLint r, g, b, a, d, s, n; + if (eglGetConfigAttrib(_sapp_tc.egl.display, c, EGL_RED_SIZE, &r) && + eglGetConfigAttrib(_sapp_tc.egl.display, c, EGL_GREEN_SIZE, &g) && + eglGetConfigAttrib(_sapp_tc.egl.display, c, EGL_BLUE_SIZE, &b) && + eglGetConfigAttrib(_sapp_tc.egl.display, c, EGL_ALPHA_SIZE, &a) && + eglGetConfigAttrib(_sapp_tc.egl.display, c, EGL_DEPTH_SIZE, &d) && + eglGetConfigAttrib(_sapp_tc.egl.display, c, EGL_STENCIL_SIZE, &s) && + eglGetConfigAttrib(_sapp_tc.egl.display, c, EGL_SAMPLES, &n) && + (r == 8) && (g == 8) && (b == 8) && (a == alpha_size) && (d == 24) && (s == 8) && (n == sample_count)) { + config = c; + break; + } + } + + EGLint visual_id; + if (!eglGetConfigAttrib(_sapp_tc.egl.display, config, EGL_NATIVE_VISUAL_ID, &visual_id)) { + _SAPP_PANIC(LINUX_EGL_NO_NATIVE_VISUAL); + } + + _SAPP_STRUCT(XVisualInfo, visual_info_template); + visual_info_template.visualid = (VisualID)visual_id; + + int num_visuals; + XVisualInfo* visual_info = XGetVisualInfo(_sapp_tc.x11.display, VisualIDMask, &visual_info_template, &num_visuals); + if (!visual_info) { + _SAPP_PANIC(LINUX_EGL_GET_VISUAL_INFO_FAILED); + } + + _sapp_tc_x11_create_window(visual_info->visual, visual_info->depth); + XFree(visual_info); + + _sapp_tc.egl.surface = eglCreateWindowSurface(_sapp_tc.egl.display, config, (EGLNativeWindowType)_sapp_tc.x11.window, NULL); + if (EGL_NO_SURFACE == _sapp_tc.egl.surface) { + _SAPP_PANIC(LINUX_EGL_CREATE_WINDOW_SURFACE_FAILED); + } + + EGLint ctx_attrs[] = { + EGL_CONTEXT_MAJOR_VERSION, _sapp_tc.desc.gl.major_version, + EGL_CONTEXT_MINOR_VERSION, _sapp_tc.desc.gl.minor_version, + #if defined(SOKOL_GLCORE) + EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, + #endif + EGL_NONE, + }; + + _sapp_tc.egl.context = eglCreateContext(_sapp_tc.egl.display, config, EGL_NO_CONTEXT, ctx_attrs); + if (EGL_NO_CONTEXT == _sapp_tc.egl.context) { + _SAPP_PANIC(LINUX_EGL_CREATE_CONTEXT_FAILED); + } + + if (!eglMakeCurrent(_sapp_tc.egl.display, _sapp_tc.egl.surface, _sapp_tc.egl.surface, _sapp_tc.egl.context)) { + _SAPP_PANIC(LINUX_EGL_MAKE_CURRENT_FAILED); + } + glGetIntegerv(GL_FRAMEBUFFER_BINDING, (GLint*)&_sapp_tc.gl.framebuffer); + + eglSwapInterval(_sapp_tc.egl.display, _sapp_tc.swap_interval); +} + +_SOKOL_PRIVATE void _sapp_tc_egl_destroy(void) { + if (_sapp_tc.egl.display != EGL_NO_DISPLAY) { + eglMakeCurrent(_sapp_tc.egl.display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + + if (_sapp_tc.egl.context != EGL_NO_CONTEXT) { + eglDestroyContext(_sapp_tc.egl.display, _sapp_tc.egl.context); + _sapp_tc.egl.context = EGL_NO_CONTEXT; + } + + if (_sapp_tc.egl.surface != EGL_NO_SURFACE) { + eglDestroySurface(_sapp_tc.egl.display, _sapp_tc.egl.surface); + _sapp_tc.egl.surface = EGL_NO_SURFACE; + } + + eglTerminate(_sapp_tc.egl.display); + _sapp_tc.egl.display = EGL_NO_DISPLAY; + } +} + +#endif // _SAPP_EGL + +_SOKOL_PRIVATE void _sapp_tc_linux_frame(void) { + _sapp_tc_x11_update_dimensions_from_window_size(); + #if defined(SOKOL_WGPU) + _sapp_tc_wgpu_frame(); + #elif defined(SOKOL_VULKAN) + _sapp_tc_vk_frame(); + #else + _sapp_tc_frame(); + #if defined(_SAPP_GLX) + _sapp_tc_glx_swap_buffers(); + #elif defined(_SAPP_EGL) + // Modified by tettou771 for TrussC: skip present support + if (_sapp_tc.skip_present) { _sapp_tc.skip_present = false; } + else { eglSwapBuffers(_sapp_tc.egl.display, _sapp_tc.egl.surface); } + #endif + #endif +} + +_SOKOL_PRIVATE void _sapp_tc_linux_run(const sapp_desc* desc) { + /* The following lines are here to trigger a linker error instead of an + obscure runtime error if the user has forgotten to add -pthread to + the compiler or linker options. They have no other purpose. + */ + pthread_attr_t pthread_attr; + pthread_attr_init(&pthread_attr); + pthread_attr_destroy(&pthread_attr); + + _sapp_tc_init_state(desc); + _sapp_tc.x11.window_state = NormalState; + + XInitThreads(); + XrmInitialize(); + _sapp_tc.x11.display = XOpenDisplay(NULL); + if (!_sapp_tc.x11.display) { + _SAPP_PANIC(LINUX_X11_OPEN_DISPLAY_FAILED); + } + _sapp_tc.x11.screen = DefaultScreen(_sapp_tc.x11.display); + _sapp_tc.x11.root = DefaultRootWindow(_sapp_tc.x11.display); + _sapp_tc_x11_query_system_dpi(); + // NOTE: on Linux system-window-size to frame-buffer-size mapping is always 1:1 + _sapp_tc.dpi_scale = _sapp_tc.x11.dpi / 96.0f; + _sapp_tc_x11_init_extensions(); + _sapp_tc_x11_create_standard_cursors(); + XkbSetDetectableAutoRepeat(_sapp_tc.x11.display, true, NULL); + _sapp_tc_x11_init_keytable(); + #if defined(_SAPP_GLX) + _sapp_tc_glx_init(); + Visual* visual = 0; + int depth = 0; + _sapp_tc_glx_choose_visual(&visual, &depth); + _sapp_tc_x11_create_window(visual, depth); + _sapp_tc_glx_create_context(); + _sapp_tc_glx_swapinterval(_sapp_tc.swap_interval); + #elif defined(_SAPP_EGL) + _sapp_tc_egl_init(); + #elif defined(SOKOL_WGPU) + _sapp_tc_x11_create_window(0, 0); + _sapp_tc_wgpu_init(); + #elif defined(SOKOL_VULKAN) + _sapp_tc_x11_create_window(0, 0); + _sapp_tc_vk_init(); + #endif + sapp_set_icon(&desc->icon); + _sapp_tc.valid = true; + _sapp_tc_x11_show_window(); + if (_sapp_tc.fullscreen) { + _sapp_tc_x11_set_fullscreen(true); + } + + XFlush(_sapp_tc.x11.display); + while (!_sapp_tc.quit_ordered) { + _sapp_tc_timing_update(&_sapp_tc.timing, 0.0); + int count = XPending(_sapp_tc.x11.display); + while (count--) { + XEvent event; + XNextEvent(_sapp_tc.x11.display, &event); + _sapp_tc_x11_process_event(&event); + } + _sapp_tc_linux_frame(); + XFlush(_sapp_tc.x11.display); + // handle quit-requested, either from window or from sapp_request_quit() + if (_sapp_tc.quit_requested && !_sapp_tc.quit_ordered) { + // give user code a chance to intervene + _sapp_tc_x11_app_event(SAPP_EVENTTYPE_QUIT_REQUESTED); + /* if user code hasn't intervened, quit the app */ + if (_sapp_tc.quit_requested) { + _sapp_tc.quit_ordered = true; + } + } + } + _sapp_tc_call_cleanup(); + #if defined(_SAPP_GLX) + _sapp_tc_glx_destroy_context(); + #elif defined(_SAPP_EGL) + _sapp_tc_egl_destroy(); + #elif defined(SOKOL_WGPU) + _sapp_tc_wgpu_discard(); + #elif defined(SOKOL_VULKAN) + _sapp_tc_vk_discard(); + #endif + _sapp_tc_x11_destroy_window(); + _sapp_tc_x11_destroy_standard_cursors(); + XCloseDisplay(_sapp_tc.x11.display); + _sapp_tc_discard_state(); +} + +#if !defined(SOKOL_NO_ENTRY) +int main(int argc, char* argv[]) { + sapp_desc desc = sokol_main(argc, argv); + _sapp_tc_linux_run(&desc); + return 0; +} +#endif /* SOKOL_NO_ENTRY */ +#endif /* _SAPP_LINUX */ +/* ---- public API (lifted from sokol_app.h >>public, web-live branches) ---- */ +#if defined(SOKOL_NO_ENTRY) +SOKOL_API_IMPL void sapp_run(const sapp_desc* desc) { + SOKOL_ASSERT(desc); + #if defined(_SAPP_MACOS) + _sapp_tc_macos_run(desc); + #elif defined(_SAPP_IOS) + _sapp_tc_ios_run(desc); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_run(desc); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_run(desc); + #elif defined(_SAPP_LINUX) + _sapp_tc_linux_run(desc); + #else + #error "sapp_run() not supported on this platform" + #endif +} + +/* this is just a stub so the linker doesn't complain */ +sapp_desc sokol_main(int argc, char* argv[]) { + _SOKOL_UNUSED(argc); + _SOKOL_UNUSED(argv); + _SAPP_STRUCT(sapp_desc, desc); + return desc; +} +#else +/* likewise, in normal mode, sapp_run() is just an empty stub */ +SOKOL_API_IMPL void sapp_run(const sapp_desc* desc) { + _SOKOL_UNUSED(desc); +} +#endif + +SOKOL_API_IMPL bool sapp_isvalid(void) { + return _sapp_tc.valid; +} + +SOKOL_API_IMPL void* sapp_userdata(void) { + return _sapp_tc.desc.user_data; +} + +SOKOL_API_IMPL sapp_desc sapp_query_desc(void) { + return _sapp_tc.desc; +} + +SOKOL_API_IMPL uint64_t sapp_frame_count(void) { + return _sapp_tc.frame_count; +} + +SOKOL_API_IMPL double sapp_frame_duration(void) { + #if defined(_SAPP_MACOS) && defined(SOKOL_METAL) + return _sapp_tc_macos_mtl_timing_frame_duration(); + #elif defined(_SAPP_IOS) && defined(SOKOL_METAL) + return _sapp_tc_ios_mtl_timing_frame_duration(); + #else + return _sapp_tc_timing_get(&_sapp_tc.timing); + #endif +} + +SOKOL_API_IMPL double sapp_frame_duration_unfiltered(void) { + return _sapp_tc.timing.dt; +} + +SOKOL_API_IMPL int sapp_width(void) { + return (_sapp_tc.framebuffer_width > 0) ? _sapp_tc.framebuffer_width : 1; +} + +SOKOL_API_IMPL float sapp_widthf(void) { + return (float)sapp_width(); +} + +SOKOL_API_IMPL int sapp_height(void) { + return (_sapp_tc.framebuffer_height > 0) ? _sapp_tc.framebuffer_height : 1; +} + +SOKOL_API_IMPL float sapp_heightf(void) { + return (float)sapp_height(); +} + +SOKOL_API_IMPL sapp_pixel_format sapp_color_format(void) { + #if defined(SOKOL_WGPU) + switch (_sapp_tc.wgpu.render_format) { + case WGPUTextureFormat_RGBA8Unorm: + return SAPP_PIXELFORMAT_RGBA8; + case WGPUTextureFormat_BGRA8Unorm: + return SAPP_PIXELFORMAT_BGRA8; + default: + SOKOL_UNREACHABLE; + return SAPP_PIXELFORMAT_NONE; + } + #elif defined(SOKOL_VULKAN) + switch (_sapp_tc.vk.surface_format.format) { + case VK_FORMAT_R8G8B8A8_UNORM: + return SAPP_PIXELFORMAT_RGBA8; + case VK_FORMAT_B8G8R8A8_UNORM: + return SAPP_PIXELFORMAT_BGRA8; + default: + // FIXME! + SOKOL_UNREACHABLE; + return SAPP_PIXELFORMAT_NONE; + } + #elif defined(SOKOL_METAL) || defined(SOKOL_D3D11) + // Modified by tettou771 for TrussC: report 10-bit color format (RGB10A2) + return SAPP_PIXELFORMAT_RGB10A2; + #else + return SAPP_PIXELFORMAT_RGBA8; + #endif +} + +SOKOL_API_IMPL sapp_pixel_format sapp_depth_format(void) { + return SAPP_PIXELFORMAT_DEPTH_STENCIL; +} + +SOKOL_API_IMPL int sapp_sample_count(void) { + return _sapp_tc.sample_count; +} + +SOKOL_API_IMPL bool sapp_high_dpi(void) { + return _sapp_tc.desc.high_dpi && (_sapp_tc.dpi_scale >= 1.5f); +} + +SOKOL_API_IMPL float sapp_dpi_scale(void) { + return _sapp_tc.dpi_scale; +} + +SOKOL_API_IMPL const void* sapp_egl_get_display(void) { + SOKOL_ASSERT(_sapp_tc.valid); + #if defined(_SAPP_ANDROID) + return _sapp_tc.android.display; + #elif defined(_SAPP_LINUX) && defined(_SAPP_EGL) + return _sapp_tc.egl.display; + #else + return 0; + #endif +} + +SOKOL_API_IMPL const void* sapp_egl_get_context(void) { + SOKOL_ASSERT(_sapp_tc.valid); + #if defined(_SAPP_ANDROID) + return _sapp_tc.android.context; + #elif defined(_SAPP_LINUX) && defined(_SAPP_EGL) + return _sapp_tc.egl.context; + #else + return 0; + #endif +} + +SOKOL_API_IMPL void sapp_show_keyboard(bool show) { + #if defined(_SAPP_IOS) + _sapp_tc_ios_show_keyboard(show); + #elif defined(_SAPP_ANDROID) + _sapp_tc_android_show_keyboard(show); + #else + _SOKOL_UNUSED(show); + #endif +} + +SOKOL_API_IMPL bool sapp_keyboard_shown(void) { + return _sapp_tc.onscreen_keyboard_shown; +} + +SOKOL_API_IMPL bool sapp_is_fullscreen(void) { + return _sapp_tc.fullscreen; +} + +SOKOL_API_IMPL void sapp_toggle_fullscreen(void) { + #if defined(_SAPP_MACOS) + _sapp_tc_macos_toggle_fullscreen(); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_toggle_fullscreen(); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_toggle_fullscreen(); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_toggle_fullscreen(); + #endif +} + +_SOKOL_PRIVATE void _sapp_tc_update_cursor(sapp_mouse_cursor cursor, bool shown) { + #if defined(_SAPP_MACOS) + _sapp_tc_macos_update_cursor(cursor, shown); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_update_cursor(cursor, shown, false); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_update_cursor(cursor, shown); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_update_cursor(cursor, shown); + #endif + _sapp_tc.mouse.current_cursor = cursor; + _sapp_tc.mouse.shown = shown; +} + +/* NOTE that sapp_show_mouse() does not "stack" like the Win32 or macOS API functions! */ +SOKOL_API_IMPL void sapp_show_mouse(bool show) { + if (_sapp_tc.mouse.shown != show) { + _sapp_tc_update_cursor(_sapp_tc.mouse.current_cursor, show); + } +} + +SOKOL_API_IMPL bool sapp_mouse_shown(void) { + return _sapp_tc.mouse.shown; +} + +SOKOL_API_IMPL void sapp_lock_mouse(bool lock) { + #if defined(_SAPP_MACOS) + _sapp_tc_macos_lock_mouse(lock); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_lock_mouse(lock); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_lock_mouse(lock); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_lock_mouse(lock); + #else + _sapp_tc.mouse.locked = lock; + #endif +} + +SOKOL_API_IMPL bool sapp_mouse_locked(void) { + return _sapp_tc.mouse.locked; +} + +SOKOL_API_IMPL void sapp_set_mouse_cursor(sapp_mouse_cursor cursor) { + SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); + if (_sapp_tc.mouse.current_cursor != cursor) { + _sapp_tc_update_cursor(cursor, _sapp_tc.mouse.shown); + } +} + +SOKOL_API_IMPL sapp_mouse_cursor sapp_get_mouse_cursor(void) { + return _sapp_tc.mouse.current_cursor; +} + +SOKOL_API_IMPL sapp_mouse_cursor sapp_bind_mouse_cursor_image(sapp_mouse_cursor cursor, const sapp_image_desc* desc) { + SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); + // NOTE: It seems that for some reason, the hotspot doesn't work if it is one less + // than the dimension of the cursor image (or more), on windows. So for a cursor + // that is 32 by 32 px, a hotspot of x = 30 works, but not x = 31. + // The cursor simply dissapears in such cases. Asserting for all platforms to make + // the behaviour consistent. + SOKOL_ASSERT(desc->cursor_hotspot_x < desc->width - 1 && desc->cursor_hotspot_y < desc->height - 1); + SOKOL_ASSERT(desc->width * desc->height * 4 == (int) desc->pixels.size); + + sapp_unbind_mouse_cursor_image(cursor); + + bool res = false; + #if defined(_SAPP_MACOS) + res = _sapp_tc_macos_make_custom_mouse_cursor(cursor, desc); + #elif defined(_SAPP_EMSCRIPTEN) + res = _sapp_tc_emsc_make_custom_mouse_cursor(cursor, desc); + #elif defined(_SAPP_WIN32) + res = _sapp_tc_win32_make_custom_mouse_cursor(cursor, desc); + #elif defined(_SAPP_LINUX) + res = _sapp_tc_x11_make_custom_mouse_cursor(cursor, desc); + #else + _SOKOL_UNUSED(desc); + #endif + _sapp_tc.custom_cursor_bound[(int)cursor] = res; + + // Update the displayed cursor in case the current cursor is the one we just bound. + if (_sapp_tc.mouse.current_cursor == cursor) { + _sapp_tc_update_cursor(cursor, _sapp_tc.mouse.shown); + } + return cursor; // returning the passed-in cursor puerly for convenience, in case you want to asign the value to a variable. +} + +SOKOL_API_IMPL void sapp_unbind_mouse_cursor_image(sapp_mouse_cursor cursor) { + SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); + if (_sapp_tc.custom_cursor_bound[(int)cursor]) { + // if this is the active cursor, first restore it to its default image, + // this must be done before attempting to destroy any cursor image + // resources which at least on win32 would fail if the cursor is still in use + _sapp_tc.custom_cursor_bound[(int)cursor] = false; + if (_sapp_tc.mouse.current_cursor == cursor) { + _sapp_tc_update_cursor(cursor, _sapp_tc.mouse.shown); + } + #if defined(_SAPP_MACOS) + _sapp_tc_macos_destroy_custom_mouse_cursor(cursor); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_destroy_custom_mouse_cursor(cursor); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_destroy_custom_mouse_cursor(cursor); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_destroy_custom_mouse_cursor(cursor); + #endif + } +} + +SOKOL_API_IMPL void sapp_request_quit(void) { + _sapp_tc.quit_requested = true; +} + +SOKOL_API_IMPL void sapp_cancel_quit(void) { + _sapp_tc.quit_requested = false; +} + +SOKOL_API_IMPL void sapp_quit(void) { + _sapp_tc.quit_ordered = true; +} + +SOKOL_API_IMPL void sapp_consume_event(void) { + _sapp_tc.event_consumed = true; +} + +/* NOTE: on HTML5, sapp_set_clipboard_string() must be called from within event handler! */ +SOKOL_API_IMPL void sapp_set_clipboard_string(const char* str) { + if (!_sapp_tc.clipboard.enabled) { + return; + } + SOKOL_ASSERT(str); + #if defined(_SAPP_MACOS) + _sapp_tc_macos_set_clipboard_string(str); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_set_clipboard_string(str); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_set_clipboard_string(str); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_set_clipboard_string(str); + #else + /* not implemented */ + #endif + _sapp_tc_strcpy(str, _sapp_tc.clipboard.buffer, (size_t)_sapp_tc.clipboard.buf_size); +} + +SOKOL_API_IMPL const char* sapp_get_clipboard_string(void) { + if (!_sapp_tc.clipboard.enabled) { + return ""; + } + #if defined(_SAPP_MACOS) + return _sapp_tc_macos_get_clipboard_string(); + #elif defined(_SAPP_EMSCRIPTEN) + return _sapp_tc.clipboard.buffer; + #elif defined(_SAPP_WIN32) + return _sapp_tc_win32_get_clipboard_string(); + #elif defined(_SAPP_LINUX) + return _sapp_tc_x11_get_clipboard_string(); + #else + /* not implemented */ + return _sapp_tc.clipboard.buffer; + #endif +} + +SOKOL_API_IMPL void sapp_set_window_title(const char* title) { + SOKOL_ASSERT(title); + _sapp_tc_strcpy(title, _sapp_tc.window_title, sizeof(_sapp_tc.window_title)); + #if defined(_SAPP_MACOS) + _sapp_tc_macos_update_window_title(); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_update_window_title(); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_update_window_title(); + #endif +} + +SOKOL_API_IMPL void sapp_set_icon(const sapp_icon_desc* desc) { + SOKOL_ASSERT(desc); + if (desc->sokol_default) { + /* the sokol default-icon generator is not ported: the favicon is the + page's business unless the app provides real icon images */ + return; + } + const int num_images = _sapp_tc_icon_num_images(desc); + if (num_images == 0) { + return; + } + SOKOL_ASSERT((num_images > 0) && (num_images <= SAPP_MAX_ICONIMAGES)); + if (!_sapp_tc_validate_icon_desc(desc, num_images)) { + return; + } + #if defined(_SAPP_MACOS) + _sapp_tc_macos_set_icon(desc, num_images); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_set_icon(desc, num_images); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_set_icon(desc, num_images); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_set_icon(desc, num_images); + #endif +} + +SOKOL_API_IMPL int sapp_get_num_dropped_files(void) { + if (!_sapp_tc.drop.enabled) { + return 0; + } + return _sapp_tc.drop.num_files; +} + +SOKOL_API_IMPL const char* sapp_get_dropped_file_path(int index) { + SOKOL_ASSERT((index >= 0) && (index < _sapp_tc.drop.num_files)); + if (!_sapp_tc.drop.enabled) { + return ""; + } + SOKOL_ASSERT(_sapp_tc.drop.buffer); + if ((index < 0) || (index >= _sapp_tc.drop.max_files)) { + return ""; + } + return (const char*) _sapp_tc_dropped_file_path_ptr(index); +} + +SOKOL_API_IMPL uint32_t sapp_html5_get_dropped_file_size(int index) { + SOKOL_ASSERT((index >= 0) && (index < _sapp_tc.drop.num_files)); + #if defined(_SAPP_EMSCRIPTEN) + if (!_sapp_tc.drop.enabled) { + return 0; + } + return _sapp_tc_js_dropped_file_size(index); + #else + (void)index; + return 0; + #endif +} + +SOKOL_API_IMPL void sapp_html5_fetch_dropped_file(const sapp_html5_fetch_request* request) { + SOKOL_ASSERT(_sapp_tc.drop.enabled); + SOKOL_ASSERT(request); + SOKOL_ASSERT(request->callback); + SOKOL_ASSERT(request->buffer.ptr); + SOKOL_ASSERT(request->buffer.size > 0); + #if defined(_SAPP_EMSCRIPTEN) + const int index = request->dropped_file_index; + sapp_html5_fetch_error error_code = SAPP_HTML5_FETCH_ERROR_NO_ERROR; + if ((index < 0) || (index >= _sapp_tc.drop.num_files)) { + error_code = SAPP_HTML5_FETCH_ERROR_OTHER; + } + if (sapp_html5_get_dropped_file_size(index) > request->buffer.size) { + error_code = SAPP_HTML5_FETCH_ERROR_BUFFER_TOO_SMALL; + } + if (SAPP_HTML5_FETCH_ERROR_NO_ERROR != error_code) { + _sapp_tc_emsc_invoke_fetch_cb(index, + false, // success + (int)error_code, + request->callback, + 0, // fetched_size + (void*)request->buffer.ptr, + request->buffer.size, + request->user_data); + } else { + _sapp_tc_js_fetch_dropped_file(index, + request->callback, + (void*)request->buffer.ptr, + request->buffer.size, + request->user_data); + } + #else + (void)request; + #endif +} + +SOKOL_API_IMPL sapp_environment sapp_get_environment(void) { + SOKOL_ASSERT(_sapp_tc.valid); + _SAPP_STRUCT(sapp_environment, res); + res.defaults.color_format = sapp_color_format(); + res.defaults.depth_format = sapp_depth_format(); + res.defaults.sample_count = sapp_sample_count(); + #if defined(SOKOL_METAL) + #if defined(_SAPP_MACOS) + res.metal.device = (__bridge const void*) _sapp_tc.macos.mtl.device; + #else + res.metal.device = (__bridge const void*) _sapp_tc.ios.mtl.device; + #endif + #endif + #if defined(SOKOL_D3D11) + res.d3d11.device = (const void*) _sapp_tc.d3d11.device; + res.d3d11.device_context = (const void*) _sapp_tc.d3d11.device_context; + #endif + #if defined(SOKOL_WGPU) + res.wgpu.device = (const void*) _sapp_tc.wgpu.device; + #endif + #if defined(SOKOL_VULKAN) + res.vulkan.instance = (const void*) _sapp_tc.vk.instance; + res.vulkan.physical_device = (const void*) _sapp_tc.vk.physical_device; + res.vulkan.device = (const void*) _sapp_tc.vk.device; + res.vulkan.queue = (const void*) _sapp_tc.vk.queue; + res.vulkan.queue_family_index = _sapp_tc.vk.queue_family_index; + #endif + return res; +} + +SOKOL_API_IMPL sapp_swapchain sapp_get_swapchain(void) { + SOKOL_ASSERT(_sapp_tc.valid); + _SAPP_STRUCT(sapp_swapchain, res); + #if defined(SOKOL_METAL) + #if defined(_SAPP_MACOS) + res.metal.current_drawable = (__bridge const void*) _sapp_tc_macos_mtl_swapchain_next(); + res.metal.depth_stencil_texture = (__bridge const void*) _sapp_tc.macos.mtl.depth_tex; + res.metal.msaa_color_texture = (__bridge const void*) _sapp_tc.macos.mtl.msaa_tex; + #else + res.metal.current_drawable = (__bridge const void*) _sapp_tc_ios_mtl_swapchain_next(); + res.metal.depth_stencil_texture = (__bridge const void*) _sapp_tc.ios.mtl.depth_tex; + res.metal.msaa_color_texture = (__bridge const void*) _sapp_tc.ios.mtl.msaa_tex; + #endif + #endif + #if defined(SOKOL_D3D11) + SOKOL_ASSERT(_sapp_tc.d3d11.rtv); + if (_sapp_tc.sample_count > 1) { + SOKOL_ASSERT(_sapp_tc.d3d11.msaa_rtv); + res.d3d11.render_view = (const void*) _sapp_tc.d3d11.msaa_rtv; + res.d3d11.resolve_view = (const void*) _sapp_tc.d3d11.rtv; + } else { + res.d3d11.render_view = (const void*) _sapp_tc.d3d11.rtv; + } + res.d3d11.depth_stencil_view = (const void*) _sapp_tc.d3d11.dsv; + #endif + #if defined(SOKOL_WGPU) + SOKOL_ASSERT(0 == _sapp_tc.wgpu.swapchain_view); + _sapp_tc_wgpu_swapchain_next(); + // FIXME: swapchain_view being null must be allowed and should skip the frame + SOKOL_ASSERT(_sapp_tc.wgpu.swapchain_view); + if (_sapp_tc.sample_count > 1) { + SOKOL_ASSERT(_sapp_tc.wgpu.msaa_view); + res.wgpu.render_view = (const void*) _sapp_tc.wgpu.msaa_view; + res.wgpu.resolve_view = (const void*) _sapp_tc.wgpu.swapchain_view; + } else { + res.wgpu.render_view = (const void*) _sapp_tc.wgpu.swapchain_view; + } + res.wgpu.depth_stencil_view = (const void*) _sapp_tc.wgpu.depth_stencil_view; + #endif + #if defined(SOKOL_VULKAN) + _sapp_tc_vk_swapchain_next(); + // FIXME: swapchain_view being null must be allowed and should skip the frame + uint32_t img_idx = _sapp_tc.vk.cur_swapchain_image_index; + if (_sapp_tc.sample_count > 1) { + SOKOL_ASSERT(_sapp_tc.vk.msaa.img && _sapp_tc.vk.msaa.view); + res.vulkan.render_image = (const void*) _sapp_tc.vk.msaa.img; + res.vulkan.render_view = (const void*) _sapp_tc.vk.msaa.view; + res.vulkan.resolve_image = (const void*) _sapp_tc.vk.swapchain_images[img_idx]; + res.vulkan.resolve_view = (const void*) _sapp_tc.vk.swapchain_views[img_idx]; + } else { + res.vulkan.render_image = (const void*) _sapp_tc.vk.swapchain_images[img_idx]; + res.vulkan.render_view = (const void*) _sapp_tc.vk.swapchain_views[img_idx]; + } + res.vulkan.depth_stencil_image = (const void*) _sapp_tc.vk.depth.img; + res.vulkan.depth_stencil_view = (const void*) _sapp_tc.vk.depth.view; + // NOTE: using the current swapchain image index here is *NOT* a bug! The render_finished_semaphore *must* + // be associated with its swapchain image in case the swapchain implementation doesn't return swapchain images in order + res.vulkan.render_finished_semaphore = _sapp_tc.vk.sync[img_idx].render_finished_sem; + res.vulkan.present_complete_semaphore = _sapp_tc.vk.sync[_sapp_tc.vk.sync_slot].present_complete_sem; + #endif + #if defined(_SAPP_ANY_GL) + res.gl.framebuffer = _sapp_tc.gl.framebuffer; + #endif + res.width = sapp_width(); + res.height = sapp_height(); + res.color_format = sapp_color_format(); + res.depth_format = sapp_depth_format(); + res.sample_count = sapp_sample_count(); + return res; +} + +SOKOL_API_IMPL const void* sapp_macos_get_window(void) { + #if defined(_SAPP_MACOS) + const void* obj = (__bridge const void*) _sapp_tc.macos.window; + SOKOL_ASSERT(obj); + return obj; + #else + return 0; + #endif +} + +SOKOL_API_IMPL const void* sapp_ios_get_window(void) { + #if defined(_SAPP_IOS) + const void* obj = (__bridge const void*) _sapp_tc.ios.window; + SOKOL_ASSERT(obj); + return obj; + #else + return 0; + #endif +} + +// Modified by tettou771 for TrussC: runtime orientation control +SOKOL_API_IMPL void sapp_ios_set_supported_orientations(uint32_t mask) { + #if defined(_SAPP_IOS) + _sapp_tc.ios.supported_orientations = (NSUInteger)mask; + #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 160000 + if (@available(iOS 16.0, *)) { + [_sapp_tc.ios.view_ctrl setNeedsUpdateOfSupportedInterfaceOrientations]; + } + #endif + #else + (void)mask; + #endif +} + +SOKOL_API_IMPL const void* sapp_d3d11_get_swap_chain(void) { + SOKOL_ASSERT(_sapp_tc.valid); +#if defined(SOKOL_D3D11) + return _sapp_tc.d3d11.swap_chain; +#else + return 0; +#endif +} + +SOKOL_API_IMPL const void* sapp_win32_get_hwnd(void) { + SOKOL_ASSERT(_sapp_tc.valid); + #if defined(_SAPP_WIN32) + return _sapp_tc.win32.hwnd; + #else + return 0; + #endif +} + +SOKOL_API_IMPL int sapp_gl_get_major_version(void) { + SOKOL_ASSERT(_sapp_tc.valid); + #if defined(_SAPP_ANY_GL) + return _sapp_tc.desc.gl.major_version; + #else + return 0; + #endif +} + +SOKOL_API_IMPL int sapp_gl_get_minor_version(void) { + SOKOL_ASSERT(_sapp_tc.valid); + #if defined(_SAPP_ANY_GL) + return _sapp_tc.desc.gl.minor_version; + #else + return 0; + #endif +} + +SOKOL_API_IMPL bool sapp_gl_is_gles(void) { + #if defined(SOKOL_GLES3) + return true; + #else + return false; + #endif +} + +SOKOL_API_IMPL const void* sapp_x11_get_window(void) { + #if defined(_SAPP_LINUX) + return (void*)_sapp_tc.x11.window; + #else + return 0; + #endif +} + +SOKOL_API_IMPL const void* sapp_x11_get_display(void) { + #if defined(_SAPP_LINUX) + return (void*)_sapp_tc.x11.display; + #else + return 0; + #endif +} + +SOKOL_API_IMPL const void* sapp_android_get_native_activity(void) { + // NOTE: _sapp_tc.valid is not asserted here because sapp_android_get_native_activity() + // needs to be callable from within sokol_main() (see: https://github.com/floooh/sokol/issues/708) + #if defined(_SAPP_ANDROID) + return (void*)_sapp_tc.android.activity; + #else + return 0; + #endif +} + +SOKOL_API_IMPL void sapp_html5_ask_leave_site(bool ask) { + _sapp_tc.html5_ask_leave_site = ask; +} + +// Modified by tettou771 for TrussC: skip the next present call (fix D3D11 flickering) +SOKOL_API_IMPL void sapp_skip_present(void) { + _sapp_tc.skip_present = true; +} +// end of modification + + + +/* ---- other-backend getters (zero stubs, same set as the native sections) - */ +#if defined(__cplusplus) +extern "C" { +#endif +const void* sapp_metal_get_device(void) { return 0; } +const void* sapp_metal_get_current_drawable(void) { return 0; } +const void* sapp_metal_get_depth_stencil_texture(void) { return 0; } +const void* sapp_metal_get_msaa_color_texture(void) { return 0; } +const void* sapp_d3d11_get_device(void) { return 0; } +const void* sapp_d3d11_get_device_context(void) { return 0; } + +/* ---- multi-window API: explicit platform gap on GLES3 Linux (RasPi) ------ + this variant is the upstream single-window backend; multi-window lives in + the GLCORE branch */ +sapp_window sapp_create_window(const sapp_window_desc* desc) { + _SOKOL_UNUSED(desc); + fprintf(stderr, "sokol_app_tc.h: sapp_create_window() is not supported on GLES3 Linux (single-window platform)\n"); + sapp_window w = {0}; + return w; +} +void sapp_destroy_window(sapp_window win) { _SOKOL_UNUSED(win); } +bool sapp_window_valid(sapp_window win) { _SOKOL_UNUSED(win); return false; } +int sapp_window_width(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +int sapp_window_height(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +int sapp_window_framebuffer_width(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +int sapp_window_framebuffer_height(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +float sapp_window_dpi_scale(sapp_window win) { _SOKOL_UNUSED(win); return 1.0f; } +int sapp_window_sample_count(sapp_window win) { _SOKOL_UNUSED(win); return 1; } +bool sapp_window_occluded(sapp_window win) { _SOKOL_UNUSED(win); return false; } +void sapp_window_set_title(sapp_window win, const char* title) { _SOKOL_UNUSED(win); _SOKOL_UNUSED(title); } +double sapp_window_frame_duration(sapp_window win) { _SOKOL_UNUSED(win); return 1.0 / 60.0; } +const void* sapp_window_metal_current_drawable(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_metal_depth_stencil_texture(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_metal_msaa_color_texture(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_d3d11_render_view(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_d3d11_resolve_view(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_d3d11_depth_stencil_view(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_macos_get_window(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_win32_get_hwnd(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_x11_get_window(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +uint32_t sapp_window_gl_framebuffer(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +#if defined(__cplusplus) +} /* extern "C" */ +#endif + +#else +#error "sokol_app_tc.h: unknown 3D API selected for Linux, must be SOKOL_GLCORE or SOKOL_GLES3" +#endif /* linux backend select */ +#elif defined(__ANDROID__) +/*== Android (GLES3 / NativeActivity) ======================================= + Implements the public sokol_app.h API on Android plus the multi-window + API (as stubs -- the activity owns exactly ONE ANativeWindow, so a + second window is not representable; sapp_create_window() returns the + invalid handle and logs). + + The entry point is INVERTED relative to every other platform: this + section EXPORTS ANativeActivity_onCreate() (NativeActivity dlopens the + app .so and calls it on Android's UI thread), which spawns a dedicated + APP THREAD and returns. The app thread owns the ALooper, the EGL + display/context/surface, the frame loop and every TrussC callback + (init/frame/event/cleanup); the UI thread only forwards lifecycle + callbacks through a message pipe + mutex/cond handshake. sapp_run() + is NOT supported (SOKOL_NO_ENTRY is a compile error); sokol_main() + is defined by TrussC's platform/android/sokol_impl.cpp bridge. + + The EGL surface has an independent lifecycle: it is destroyed on + background and recreated on foreground (APP_CMD state machine); frames + only run while has_resumed && has_focus && a surface exists. + + Structure mirrors sokol_app.h's Android backend including all TrussC + patches: the Choreographer vsync-paced frame loop with ALooper poll + fallback (compiled in at __ANDROID_API__ >= 29), the BACK-key + no-shutdown fix (kiosk/screen-pinning survival -- BACK is consumed, + never tears down EGL), DPI scale from AConfiguration_getDensity()/160 + with fb/win-ratio fallback, and the skip_present early-return. Input + is multi-touch + the BACK key; orientation and immersive mode are + TrussC-side JNI (tcPlatform_android.cpp), not sokol's business. */ +#define _SAPP_ANDROID (1) +#if !defined(SOKOL_GLES3) +#error "sokol_app_tc.h: unknown 3D API selected for Android, must be SOKOL_GLES3" +#endif +#if defined(SOKOL_NO_ENTRY) +#error "sokol_app_tc.h: SOKOL_NO_ENTRY is not supported on Android" +#endif +/* GLES3 always -> the lifted code's GL forks are live */ +#define _SAPP_ANY_GL (1) + +#include +#include +#include +#include +#include /* [TrussC] AConfiguration for display density */ +#include +#if __ANDROID_API__ >= 29 + #include +#endif +#include +#include +#include + +#if defined(SOKOL_APP_IMPL_INCLUDED) +#error "sokol_app_tc.h owns the Android implementation of the sapp_* API; include sokol_app.h WITHOUT an implementation define in this TU" +#endif + +#ifndef SOKOL_ASSERT +#include +#define SOKOL_ASSERT(c) assert(c) +#endif +#ifndef _SOKOL_PRIVATE +#define _SOKOL_PRIVATE static +#endif +#ifndef _SOKOL_UNUSED +#define _SOKOL_UNUSED(x) (void)(x) +#endif +#ifndef SOKOL_API_IMPL +#define SOKOL_API_IMPL +#endif +#ifndef SOKOL_UNREACHABLE +#define SOKOL_UNREACHABLE SOKOL_ASSERT(false) +#endif + +#define _SAPP_MAX_TITLE_LENGTH (128) +#define _SAPP_FALLBACK_DEFAULT_WINDOW_WIDTH (640) +#define _SAPP_FALLBACK_DEFAULT_WINDOW_HEIGHT (480) +#define _sapp_tc_def(val, def) (((val) == 0) ? (def) : (val)) +#if defined(__cplusplus) +#define _SAPP_STRUCT(TYPE, NAME) TYPE NAME = {} +#define _SAPP_CLEAR_ARC_STRUCT(type, item) { item = type(); } +#else +#define _SAPP_STRUCT(TYPE, NAME) TYPE NAME = {0} +#define _SAPP_CLEAR_ARC_STRUCT(type, item) { _sapp_tc_clear(&item, sizeof(item)); } +#endif + +/* controlled failure instead of sokol_app.h's log-item machinery. + stderr goes nowhere on Android -- route to the system log (liblog). */ +#include +#define _SAPP_PANIC(code) do { __android_log_print(ANDROID_LOG_FATAL, "sokol_app_tc", "panic: " #code); abort(); } while (0) +#define _SAPP_ERROR(code) __android_log_print(ANDROID_LOG_ERROR, "sokol_app_tc", "error: " #code) +#define _SAPP_ERROR_MSG(code, msg) __android_log_print(ANDROID_LOG_ERROR, "sokol_app_tc", "error: " #code ": %s", msg) +#define _SAPP_WARN_MSG(code, msg) __android_log_print(ANDROID_LOG_WARN, "sokol_app_tc", "warn: " #code ": %s", msg) +#define _SAPP_INFO_MSG(code, msg) __android_log_print(ANDROID_LOG_INFO, "sokol_app_tc", "info: " #code ": %s", msg) +#define _SAPP_INFO(code) __android_log_print(ANDROID_LOG_INFO, "sokol_app_tc", "info: " #code) +#define _SAPP_WARN(code) __android_log_print(ANDROID_LOG_WARN, "sokol_app_tc", "warn: " #code) +typedef struct { + #if defined(_SAPP_APPLE) + struct { + mach_timebase_info_data_t timebase; + uint64_t start; + } mach; + #elif defined(_SAPP_EMSCRIPTEN) + int _dummy; + #elif defined(_SAPP_WIN32) + struct { + LARGE_INTEGER freq; + LARGE_INTEGER start; + } win; + #else // Linux, Android, ... + #ifdef CLOCK_MONOTONIC + #define _SAPP_CLOCK_MONOTONIC CLOCK_MONOTONIC + #else + // on some embedded platforms, CLOCK_MONOTONIC isn't defined + #define _SAPP_CLOCK_MONOTONIC (1) + #endif + struct { + uint64_t start; + } posix; + #endif +} _sapp_tc_timestamp_t; + +_SOKOL_PRIVATE int64_t _sapp_tc_int64_muldiv(int64_t value, int64_t numer, int64_t denom) { + int64_t q = value / denom; + int64_t r = value % denom; + return q * numer + r * numer / denom; +} + +_SOKOL_PRIVATE void _sapp_tc_timestamp_init(_sapp_tc_timestamp_t* ts) { + #if defined(_SAPP_APPLE) + mach_timebase_info(&ts->mach.timebase); + ts->mach.start = mach_absolute_time(); + #elif defined(_SAPP_EMSCRIPTEN) + (void)ts; + #elif defined(_SAPP_WIN32) + QueryPerformanceFrequency(&ts->win.freq); + QueryPerformanceCounter(&ts->win.start); + #else + struct timespec tspec; + clock_gettime(_SAPP_CLOCK_MONOTONIC, &tspec); + ts->posix.start = (uint64_t)tspec.tv_sec*1000000000 + (uint64_t)tspec.tv_nsec; + #endif +} + +_SOKOL_PRIVATE double _sapp_tc_timestamp_now(_sapp_tc_timestamp_t* ts) { + #if defined(_SAPP_APPLE) + const uint64_t traw = mach_absolute_time() - ts->mach.start; + const uint64_t now = (uint64_t) _sapp_tc_int64_muldiv((int64_t)traw, (int64_t)ts->mach.timebase.numer, (int64_t)ts->mach.timebase.denom); + return (double)now / 1000000000.0; + #elif defined(_SAPP_EMSCRIPTEN) + (void)ts; + SOKOL_ASSERT(false); + return 0.0; + #elif defined(_SAPP_WIN32) + LARGE_INTEGER qpc; + QueryPerformanceCounter(&qpc); + const uint64_t now = (uint64_t)_sapp_tc_int64_muldiv(qpc.QuadPart - ts->win.start.QuadPart, 1000000000, ts->win.freq.QuadPart); + return (double)now / 1000000000.0; + #else + struct timespec tspec; + clock_gettime(_SAPP_CLOCK_MONOTONIC, &tspec); + const uint64_t now = ((uint64_t)tspec.tv_sec*1000000000 + (uint64_t)tspec.tv_nsec) - ts->posix.start; + return (double)now / 1000000000.0; + #endif +} + +typedef struct { + _sapp_tc_timestamp_t timestamp; + double dt_min; // config: min clamp value for unfiltered time delta (seconds) + double dt_max; // config: max clamp value for unfiltered time delta (seconds) + double dt_threshold; // config: threshold time delta for 'resetting' filtering (default: 0.004s, 4ms) + double alpha; // config: smoothing constant, lower values smoother, higher values faster response + double last; // last absolute time in seconds + double dt; // unfiltered frame delta in seconds, clamped to dt_min/dt_max + double ema; // intermediate ema-filter result + double smooth_dt; // smoothed frame delta in seconds +} _sapp_tc_timing_t; + +_SOKOL_PRIVATE void _sapp_tc_timing_init(_sapp_tc_timing_t* t) { + _sapp_tc_timestamp_init(&t->timestamp); + t->dt_min = 0.000001; // 1 us + t->dt_max = 0.1; // 100 ms + t->dt_threshold = 0.004; // 4ms + t->alpha = 0.025; + t->dt = 1.0 / 60.0; // a 'likely' non-null value + t->ema = t->dt; + t->smooth_dt = t->dt; +} + +_SOKOL_PRIVATE double _sapp_tc_timing_clamp(_sapp_tc_timing_t* t, double dt) { + SOKOL_ASSERT((t->dt_min > 0.0) && (t->dt_max > 0.0) && (t->dt_max >= t->dt_min)); + if (dt < t->dt_min) { + return t->dt_min; + } else if (dt > t->dt_max) { + return t->dt_max; + } else { + return dt; + } +} + +_SOKOL_PRIVATE void _sapp_tc_timing_delta(_sapp_tc_timing_t* t, double dt) { + // first clamp raw dt against min/max (min avoid division by zero, max + // may avoids glitches and 'death-spirals' during debugging + dt = _sapp_tc_timing_clamp(t, dt); + t->dt = dt; + const double error = fabs(dt - t->smooth_dt); + if (error > t->dt_threshold) { + // 'reset' filter when new delta is outside threshold + t->ema = dt; + t->smooth_dt = dt; + } else { + // simple ema-filter with fixed alpha + t->ema = t->ema + t->alpha * (dt - t->ema); + t->smooth_dt = _sapp_tc_timing_clamp(t, t->ema); + } +} + +_SOKOL_PRIVATE void _sapp_tc_timing_update(_sapp_tc_timing_t* t, double external_now) { + double now; + if (external_now == 0.0) { + now = _sapp_tc_timestamp_now(&t->timestamp); + } else { + now = external_now; + } + if (t->last > 0.0) { + double dt = now - t->last; + _sapp_tc_timing_delta(t, dt); + } + t->last = now; + +} + +_SOKOL_PRIVATE double _sapp_tc_timing_get(_sapp_tc_timing_t* t) { + return t->smooth_dt; +} + +#if defined(_SAPP_ANDROID) +typedef enum { + _SOKOL_ANDROID_MSG_CREATE, + _SOKOL_ANDROID_MSG_RESUME, + _SOKOL_ANDROID_MSG_PAUSE, + _SOKOL_ANDROID_MSG_FOCUS, + _SOKOL_ANDROID_MSG_NO_FOCUS, + _SOKOL_ANDROID_MSG_SET_NATIVE_WINDOW, + _SOKOL_ANDROID_MSG_SET_INPUT_QUEUE, + _SOKOL_ANDROID_MSG_DESTROY, +} _sapp_tc_android_msg_t; + +typedef struct { + pthread_t thread; + pthread_mutex_t mutex; + pthread_cond_t cond; + int read_from_main_fd; + int write_from_main_fd; +} _sapp_tc_android_pt_t; + +typedef struct { + ANativeWindow* window; + AInputQueue* input; +} _sapp_tc_android_resources_t; + +typedef struct { + ANativeActivity* activity; + _sapp_tc_android_pt_t pt; + _sapp_tc_android_resources_t pending; + _sapp_tc_android_resources_t current; + ALooper* looper; + bool is_thread_started; + bool is_thread_stopping; + bool is_thread_stopped; + bool has_created; + bool has_resumed; + bool has_focus; + EGLConfig config; + EGLDisplay display; + EGLContext context; + EGLSurface surface; + #if __ANDROID_API__ >= 29 + AChoreographer* choreographer; + bool frame_callback_in_flight; + #endif +} _sapp_tc_android_t; + +#endif // _SAPP_ANDROID + +typedef struct { + bool enabled; + int buf_size; + char* buffer; +} _sapp_tc_clipboard_t; + +typedef struct { + bool enabled; + int max_files; + int max_path_length; + int num_files; + int buf_size; + char* buffer; +} _sapp_tc_drop_t; + +typedef struct { + float x, y; + float dx, dy; + bool shown; + bool locked; + bool pos_valid; + sapp_mouse_cursor current_cursor; +} _sapp_tc_mouse_t; + +typedef struct { + uint32_t framebuffer; +} _sapp_tc_gl_t; + +/* per-app state -- an Android-sized subset of sokol_app.h's _sapp_t with the + same member names, so the lifted implementation code reads unchanged. + mouse / clipboard / drop / html5 / cursor members are inert on Android + (touch-only, no OS clipboard or dnd surface) but kept so the shared lifted + helpers and the lifted public API compile verbatim. */ +typedef struct { + sapp_desc desc; + bool valid; + bool fullscreen; /* inert: Android is inherently fullscreen */ + bool first_frame; + bool init_called; + bool cleanup_called; + bool quit_requested; /* inert: lifecycle is activity-driven */ + bool quit_ordered; + bool event_consumed; + bool html5_ask_leave_site; /* inert */ + bool onscreen_keyboard_shown; /* never set on Android (pre-existing quirk) */ + bool skip_present; + int window_width; + int window_height; + int framebuffer_width; + int framebuffer_height; + int sample_count; + int swap_interval; + float dpi_scale; + uint64_t frame_count; + sapp_event event; + _sapp_tc_mouse_t mouse; + _sapp_tc_clipboard_t clipboard; + _sapp_tc_drop_t drop; + _sapp_tc_timing_t timing; + _sapp_tc_android_t android; + _sapp_tc_gl_t gl; + char html5_canvas_selector[_SAPP_MAX_TITLE_LENGTH]; + char window_title[_SAPP_MAX_TITLE_LENGTH]; + bool custom_cursor_bound[_SAPP_MOUSECURSOR_NUM]; +} _sapp_tc_t; +static _sapp_tc_t _sapp_tc; +_SOKOL_PRIVATE void _sapp_tc_clear(void* ptr, size_t size) { + SOKOL_ASSERT(ptr && (size > 0)); + memset(ptr, 0, size); +} + +_SOKOL_PRIVATE void* _sapp_tc_malloc(size_t size) { + SOKOL_ASSERT(size > 0); + void* ptr; + if (_sapp_tc.desc.allocator.alloc_fn) { + ptr = _sapp_tc.desc.allocator.alloc_fn(size, _sapp_tc.desc.allocator.user_data); + } else { + ptr = malloc(size); + } + if (0 == ptr) { + _SAPP_PANIC(MALLOC_FAILED); + } + return ptr; +} + +_SOKOL_PRIVATE void* _sapp_tc_malloc_clear(size_t size) { + void* ptr = _sapp_tc_malloc(size); + _sapp_tc_clear(ptr, size); + return ptr; +} + +_SOKOL_PRIVATE void _sapp_tc_free(void* ptr) { + if (_sapp_tc.desc.allocator.free_fn) { + _sapp_tc.desc.allocator.free_fn(ptr, _sapp_tc.desc.allocator.user_data); + } else { + free(ptr); + } +} + +// ██ ██ ███████ ██ ██████ ███████ ██████ ███████ +// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +// ███████ █████ ██ ██████ █████ ██████ ███████ +// ██ ██ ██ ██ ██ ██ ██ ██ ██ +// ██ ██ ███████ ███████ ██ ███████ ██ ██ ███████ +// +// >>helpers + +// round float to int and at least 1 +_SOKOL_PRIVATE int _sapp_tc_roundf_gzero(float f) { + int val = (int)roundf(f); + if (val <= 0) { + val = 1; + } + return val; +} + +_SOKOL_PRIVATE void _sapp_tc_call_init(void) { + if (_sapp_tc.desc.init_cb) { + _sapp_tc.desc.init_cb(); + } else if (_sapp_tc.desc.init_userdata_cb) { + _sapp_tc.desc.init_userdata_cb(_sapp_tc.desc.user_data); + } + _sapp_tc.init_called = true; +} + +_SOKOL_PRIVATE void _sapp_tc_call_frame(void) { + if (_sapp_tc.init_called && !_sapp_tc.cleanup_called) { + if (_sapp_tc.desc.frame_cb) { + _sapp_tc.desc.frame_cb(); + } else if (_sapp_tc.desc.frame_userdata_cb) { + _sapp_tc.desc.frame_userdata_cb(_sapp_tc.desc.user_data); + } + } +} + +_SOKOL_PRIVATE void _sapp_tc_call_cleanup(void) { + if (!_sapp_tc.cleanup_called) { + if (_sapp_tc.desc.cleanup_cb) { + _sapp_tc.desc.cleanup_cb(); + } else if (_sapp_tc.desc.cleanup_userdata_cb) { + _sapp_tc.desc.cleanup_userdata_cb(_sapp_tc.desc.user_data); + } + _sapp_tc.cleanup_called = true; + } +} + +_SOKOL_PRIVATE bool _sapp_tc_call_event(const sapp_event* e) { + if (!_sapp_tc.cleanup_called) { + if (_sapp_tc.desc.event_cb) { + _sapp_tc.desc.event_cb(e); + } else if (_sapp_tc.desc.event_userdata_cb) { + _sapp_tc.desc.event_userdata_cb(e, _sapp_tc.desc.user_data); + } + } + if (_sapp_tc.event_consumed) { + _sapp_tc.event_consumed = false; + return true; + } else { + return false; + } +} + + +_SOKOL_PRIVATE char* _sapp_tc_dropped_file_path_ptr(int index) { + SOKOL_ASSERT(_sapp_tc.drop.buffer); + SOKOL_ASSERT((index >= 0) && (index <= _sapp_tc.drop.max_files)); + int offset = index * _sapp_tc.drop.max_path_length; + SOKOL_ASSERT(offset < _sapp_tc.drop.buf_size); + return &_sapp_tc.drop.buffer[offset]; +} + +/* Copy a string (either zero-terminated or with explicit length) + into a fixed size buffer with guaranteed zero-termination. + + Return false if the string didn't fit into the buffer and had to be clamped. + + FIXME: Currently UTF-8 strings might become invalid if the string + is clamped, because the last zero-byte might be written into + the middle of a multi-byte sequence. +*/ +_SOKOL_PRIVATE bool _sapp_tc_strcpy_range(const char* src, size_t src_len, char* dst, size_t dst_buf_len) { + SOKOL_ASSERT(src && dst && (dst_buf_len > 0)); + if (0 == src_len) { + src_len = dst_buf_len; + } + char* const end = &(dst[dst_buf_len-1]); + char c = 0; + for (size_t i = 0; i < dst_buf_len; i++) { + c = *src; + if (i >= src_len) { + c = 0; + } + if (c != 0) { + src++; + } + *dst++ = c; + } + // truncated? + if (c != 0) { + *end = 0; + return false; + } else { + return true; + } +} + +_SOKOL_PRIVATE bool _sapp_tc_strcpy(const char* src, char* dst, size_t dst_buf_len) { + return _sapp_tc_strcpy_range(src, 0, dst, dst_buf_len); +} + +_SOKOL_PRIVATE sapp_desc _sapp_tc_desc_defaults(const sapp_desc* desc) { + SOKOL_ASSERT((desc->allocator.alloc_fn && desc->allocator.free_fn) || (!desc->allocator.alloc_fn && !desc->allocator.free_fn)); + sapp_desc res = *desc; + res.sample_count = _sapp_tc_def(res.sample_count, 1); + res.swap_interval = _sapp_tc_def(res.swap_interval, 1); + if (0 == res.gl.major_version) { + #if defined(SOKOL_GLCORE) + res.gl.major_version = 4; + #if defined(_SAPP_APPLE) + res.gl.minor_version = 1; + #else + res.gl.minor_version = 3; + #endif + #elif defined(SOKOL_GLES3) + res.gl.major_version = 3; + #if defined(_SAPP_ANDROID) || defined(_SAPP_LINUX) + res.gl.minor_version = 1; + #else + res.gl.minor_version = 0; + #endif + #endif + } + res.html5.canvas_selector = _sapp_tc_def(res.html5.canvas_selector, "#canvas"); + res.clipboard_size = _sapp_tc_def(res.clipboard_size, 8192); + res.max_dropped_files = _sapp_tc_def(res.max_dropped_files, 1); + res.max_dropped_file_path_length = _sapp_tc_def(res.max_dropped_file_path_length, 2048); + res.window_title = _sapp_tc_def(res.window_title, "sokol"); + return res; +} + +_SOKOL_PRIVATE void _sapp_tc_init_state(const sapp_desc* desc) { + SOKOL_ASSERT(desc); + SOKOL_ASSERT(desc->width >= 0); + SOKOL_ASSERT(desc->height >= 0); + SOKOL_ASSERT(desc->sample_count >= 0); + SOKOL_ASSERT(desc->swap_interval >= 0); + SOKOL_ASSERT(desc->clipboard_size >= 0); + SOKOL_ASSERT(desc->max_dropped_files >= 0); + SOKOL_ASSERT(desc->max_dropped_file_path_length >= 0); + _SAPP_CLEAR_ARC_STRUCT(_sapp_tc_t, _sapp_tc); + _sapp_tc.desc = _sapp_tc_desc_defaults(desc); + _sapp_tc.first_frame = true; + // NOTE: _sapp_tc.desc.width/height may be 0! Platform backends need to deal with this + _sapp_tc.window_width = _sapp_tc.desc.width; + _sapp_tc.window_height = _sapp_tc.desc.height; + _sapp_tc.framebuffer_width = _sapp_tc.window_width; + _sapp_tc.framebuffer_height = _sapp_tc.window_height; + _sapp_tc.sample_count = _sapp_tc.desc.sample_count; + _sapp_tc.swap_interval = _sapp_tc.desc.swap_interval; + _sapp_tc_strcpy(_sapp_tc.desc.html5.canvas_selector, _sapp_tc.html5_canvas_selector, sizeof(_sapp_tc.html5_canvas_selector)); + _sapp_tc.desc.html5.canvas_selector = _sapp_tc.html5_canvas_selector; + _sapp_tc.html5_ask_leave_site = _sapp_tc.desc.html5.ask_leave_site; + _sapp_tc.clipboard.enabled = _sapp_tc.desc.enable_clipboard; + if (_sapp_tc.clipboard.enabled) { + _sapp_tc.clipboard.buf_size = _sapp_tc.desc.clipboard_size; + _sapp_tc.clipboard.buffer = (char*) _sapp_tc_malloc_clear((size_t)_sapp_tc.clipboard.buf_size); + } + _sapp_tc.drop.enabled = _sapp_tc.desc.enable_dragndrop; + if (_sapp_tc.drop.enabled) { + _sapp_tc.drop.max_files = _sapp_tc.desc.max_dropped_files; + _sapp_tc.drop.max_path_length = _sapp_tc.desc.max_dropped_file_path_length; + _sapp_tc.drop.buf_size = _sapp_tc.drop.max_files * _sapp_tc.drop.max_path_length; + _sapp_tc.drop.buffer = (char*) _sapp_tc_malloc_clear((size_t)_sapp_tc.drop.buf_size); + } + _sapp_tc_strcpy(_sapp_tc.desc.window_title, _sapp_tc.window_title, sizeof(_sapp_tc.window_title)); + _sapp_tc.desc.window_title = _sapp_tc.window_title; + _sapp_tc.dpi_scale = 1.0f; + _sapp_tc.fullscreen = _sapp_tc.desc.fullscreen; + _sapp_tc.mouse.shown = true; + _sapp_tc_timing_init(&_sapp_tc.timing); +} + +_SOKOL_PRIVATE void _sapp_tc_discard_state(void) { + if (_sapp_tc.clipboard.enabled) { + SOKOL_ASSERT(_sapp_tc.clipboard.buffer); + _sapp_tc_free((void*)_sapp_tc.clipboard.buffer); + } + if (_sapp_tc.drop.enabled) { + SOKOL_ASSERT(_sapp_tc.drop.buffer); + _sapp_tc_free((void*)_sapp_tc.drop.buffer); + } + for (int i = 0; i < _SAPP_MOUSECURSOR_NUM; i++) { + sapp_unbind_mouse_cursor_image((sapp_mouse_cursor) i); + } + _SAPP_CLEAR_ARC_STRUCT(_sapp_tc_t, _sapp_tc); +} + +_SOKOL_PRIVATE void _sapp_tc_init_event(sapp_event_type type) { + _sapp_tc_clear(&_sapp_tc.event, sizeof(_sapp_tc.event)); + _sapp_tc.event.type = type; + _sapp_tc.event.frame_count = _sapp_tc.frame_count; + _sapp_tc.event.mouse_button = SAPP_MOUSEBUTTON_INVALID; + _sapp_tc.event.window_width = _sapp_tc.window_width; + _sapp_tc.event.window_height = _sapp_tc.window_height; + _sapp_tc.event.framebuffer_width = _sapp_tc.framebuffer_width; + _sapp_tc.event.framebuffer_height = _sapp_tc.framebuffer_height; + _sapp_tc.event.mouse_x = _sapp_tc.mouse.x; + _sapp_tc.event.mouse_y = _sapp_tc.mouse.y; + _sapp_tc.event.mouse_dx = _sapp_tc.mouse.dx; + _sapp_tc.event.mouse_dy = _sapp_tc.mouse.dy; +} + +_SOKOL_PRIVATE bool _sapp_tc_events_enabled(void) { + /* only send events when an event callback is set, and the init function was called */ + return (_sapp_tc.desc.event_cb || _sapp_tc.desc.event_userdata_cb) && _sapp_tc.init_called; +} + +_SOKOL_PRIVATE void _sapp_tc_clear_drop_buffer(void) { + if (_sapp_tc.drop.enabled) { + SOKOL_ASSERT(_sapp_tc.drop.buffer); + _sapp_tc_clear(_sapp_tc.drop.buffer, (size_t)_sapp_tc.drop.buf_size); + } +} + +_SOKOL_PRIVATE void _sapp_tc_frame(void) { + if (_sapp_tc.first_frame) { + _sapp_tc.first_frame = false; + _sapp_tc_call_init(); + } + _sapp_tc_call_frame(); + _sapp_tc.frame_count++; +} + +_SOKOL_PRIVATE bool _sapp_tc_image_validate(const sapp_image_desc* desc) { + SOKOL_ASSERT(desc->width > 0); + SOKOL_ASSERT(desc->height > 0); + SOKOL_ASSERT(desc->pixels.ptr != 0); + SOKOL_ASSERT(desc->pixels.size > 0); + const size_t wh_size = (size_t)(desc->width * desc->height) * sizeof(uint32_t); + if (wh_size != desc->pixels.size) { + _SAPP_ERROR(IMAGE_DATA_SIZE_MISMATCH); + return false; + } + return true; +} + +_SOKOL_PRIVATE int _sapp_tc_image_bestmatch(const sapp_image_desc image_descs[], int num_images, int width, int height) { + int least_diff = 0x7FFFFFFF; + int least_index = 0; + for (int i = 0; i < num_images; i++) { + int diff = (image_descs[i].width * image_descs[i].height) - (width * height); + if (diff < 0) { + diff = -diff; + } + if (diff < least_diff) { + least_diff = diff; + least_index = i; + } + } + return least_index; +} + +_SOKOL_PRIVATE int _sapp_tc_icon_num_images(const sapp_icon_desc* desc) { + int index = 0; + for (; index < SAPP_MAX_ICONIMAGES; index++) { + if (0 == desc->images[index].pixels.ptr) { + break; + } + } + return index; +} + +_SOKOL_PRIVATE bool _sapp_tc_validate_icon_desc(const sapp_icon_desc* desc, int num_images) { + SOKOL_ASSERT(num_images <= SAPP_MAX_ICONIMAGES); + for (int i = 0; i < num_images; i++) { + const sapp_image_desc* img_desc = &desc->images[i]; + if (!_sapp_tc_image_validate(img_desc)) { + return false; + } + } + return true; +} +#if defined(_SAPP_ANDROID) + +/* android loop thread */ +_SOKOL_PRIVATE bool _sapp_tc_android_init_egl(void) { + SOKOL_ASSERT(_sapp_tc.android.display == EGL_NO_DISPLAY); + SOKOL_ASSERT(_sapp_tc.android.context == EGL_NO_CONTEXT); + + EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + if (display == EGL_NO_DISPLAY) { + return false; + } + if (eglInitialize(display, NULL, NULL) == EGL_FALSE) { + return false; + } + EGLint alpha_size = _sapp_tc.desc.alpha ? 8 : 0; + const EGLint cfg_attributes[] = { + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, alpha_size, + EGL_DEPTH_SIZE, 16, + EGL_STENCIL_SIZE, 0, + EGL_NONE, + }; + EGLConfig available_cfgs[32]; + EGLint cfg_count; + eglChooseConfig(display, cfg_attributes, available_cfgs, 32, &cfg_count); + SOKOL_ASSERT(cfg_count > 0); + SOKOL_ASSERT(cfg_count <= 32); + + /* find config with 8-bit rgb buffer if available, ndk sample does not trust egl spec */ + EGLConfig config; + bool exact_cfg_found = false; + for (int i = 0; i < cfg_count; ++i) { + EGLConfig c = available_cfgs[i]; + EGLint r, g, b, a, d; + if (eglGetConfigAttrib(display, c, EGL_RED_SIZE, &r) == EGL_TRUE && + eglGetConfigAttrib(display, c, EGL_GREEN_SIZE, &g) == EGL_TRUE && + eglGetConfigAttrib(display, c, EGL_BLUE_SIZE, &b) == EGL_TRUE && + eglGetConfigAttrib(display, c, EGL_ALPHA_SIZE, &a) == EGL_TRUE && + eglGetConfigAttrib(display, c, EGL_DEPTH_SIZE, &d) == EGL_TRUE && + r == 8 && g == 8 && b == 8 && (alpha_size == 0 || a == alpha_size) && d == 16) { + exact_cfg_found = true; + config = c; + break; + } + } + if (!exact_cfg_found) { + config = available_cfgs[0]; + } + + EGLint ctx_attributes[] = { + EGL_CONTEXT_MAJOR_VERSION, _sapp_tc.desc.gl.major_version, + EGL_CONTEXT_MINOR_VERSION, _sapp_tc.desc.gl.minor_version, + EGL_NONE, + }; + EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, ctx_attributes); + if (context == EGL_NO_CONTEXT) { + return false; + } + + _sapp_tc.android.config = config; + _sapp_tc.android.display = display; + _sapp_tc.android.context = context; + return true; +} + +_SOKOL_PRIVATE void _sapp_tc_android_cleanup_egl(void) { + if (_sapp_tc.android.display != EGL_NO_DISPLAY) { + eglMakeCurrent(_sapp_tc.android.display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + if (_sapp_tc.android.surface != EGL_NO_SURFACE) { + eglDestroySurface(_sapp_tc.android.display, _sapp_tc.android.surface); + _sapp_tc.android.surface = EGL_NO_SURFACE; + } + if (_sapp_tc.android.context != EGL_NO_CONTEXT) { + eglDestroyContext(_sapp_tc.android.display, _sapp_tc.android.context); + _sapp_tc.android.context = EGL_NO_CONTEXT; + } + eglTerminate(_sapp_tc.android.display); + _sapp_tc.android.display = EGL_NO_DISPLAY; + } +} + +_SOKOL_PRIVATE bool _sapp_tc_android_init_egl_surface(ANativeWindow* window) { + SOKOL_ASSERT(_sapp_tc.android.display != EGL_NO_DISPLAY); + SOKOL_ASSERT(_sapp_tc.android.context != EGL_NO_CONTEXT); + SOKOL_ASSERT(_sapp_tc.android.surface == EGL_NO_SURFACE); + SOKOL_ASSERT(window); + + /* TODO: set window flags */ + /* ANativeActivity_setWindowFlags(activity, AWINDOW_FLAG_KEEP_SCREEN_ON, 0); */ + + /* create egl surface and make it current */ + EGLSurface surface = eglCreateWindowSurface(_sapp_tc.android.display, _sapp_tc.android.config, window, NULL); + if (surface == EGL_NO_SURFACE) { + return false; + } + if (eglMakeCurrent(_sapp_tc.android.display, surface, surface, _sapp_tc.android.context) == EGL_FALSE) { + return false; + } + _sapp_tc.android.surface = surface; + glGetIntegerv(GL_FRAMEBUFFER_BINDING, (GLint*)&_sapp_tc.gl.framebuffer); + return true; +} + +_SOKOL_PRIVATE void _sapp_tc_android_cleanup_egl_surface(void) { + if (_sapp_tc.android.display == EGL_NO_DISPLAY) { + return; + } + eglMakeCurrent(_sapp_tc.android.display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + if (_sapp_tc.android.surface != EGL_NO_SURFACE) { + eglDestroySurface(_sapp_tc.android.display, _sapp_tc.android.surface); + _sapp_tc.android.surface = EGL_NO_SURFACE; + } +} + +_SOKOL_PRIVATE void _sapp_tc_android_app_event(sapp_event_type type) { + if (_sapp_tc_events_enabled()) { + _sapp_tc_init_event(type); + _sapp_tc_call_event(&_sapp_tc.event); + } +} + +_SOKOL_PRIVATE void _sapp_tc_android_update_dimensions(ANativeWindow* window, bool force_update) { + SOKOL_ASSERT(_sapp_tc.android.display != EGL_NO_DISPLAY); + SOKOL_ASSERT(_sapp_tc.android.context != EGL_NO_CONTEXT); + SOKOL_ASSERT(_sapp_tc.android.surface != EGL_NO_SURFACE); + SOKOL_ASSERT(window); + + const int32_t win_w = ANativeWindow_getWidth(window); + const int32_t win_h = ANativeWindow_getHeight(window); + SOKOL_ASSERT(win_w >= 0 && win_h >= 0); + const bool win_changed = (win_w != _sapp_tc.window_width) || (win_h != _sapp_tc.window_height); + _sapp_tc.window_width = win_w; + _sapp_tc.window_height = win_h; + if (win_changed || force_update) { + if (!_sapp_tc.desc.high_dpi) { + const int32_t buf_w = win_w / 2; + const int32_t buf_h = win_h / 2; + EGLint format; + EGLBoolean egl_result = eglGetConfigAttrib(_sapp_tc.android.display, _sapp_tc.android.config, EGL_NATIVE_VISUAL_ID, &format); + SOKOL_ASSERT(egl_result == EGL_TRUE); _SOKOL_UNUSED(egl_result); + /* NOTE: calling ANativeWindow_setBuffersGeometry() with the same dimensions + as the ANativeWindow size results in weird display artefacts, that's + why it's only called when the buffer geometry is different from + the window size + */ + int32_t result = ANativeWindow_setBuffersGeometry(window, buf_w, buf_h, format); + SOKOL_ASSERT(result == 0); _SOKOL_UNUSED(result); + } + } + + /* query surface size */ + EGLint fb_w, fb_h; + EGLBoolean egl_result_w = eglQuerySurface(_sapp_tc.android.display, _sapp_tc.android.surface, EGL_WIDTH, &fb_w); + EGLBoolean egl_result_h = eglQuerySurface(_sapp_tc.android.display, _sapp_tc.android.surface, EGL_HEIGHT, &fb_h); + SOKOL_ASSERT(egl_result_w == EGL_TRUE); _SOKOL_UNUSED(egl_result_w); + SOKOL_ASSERT(egl_result_h == EGL_TRUE); _SOKOL_UNUSED(egl_result_h); + const bool fb_changed = (fb_w != _sapp_tc.framebuffer_width) || (fb_h != _sapp_tc.framebuffer_height); + _sapp_tc.framebuffer_width = fb_w; + _sapp_tc.framebuffer_height = fb_h; + /* [TrussC] Use actual display density for dpi_scale instead of fb/win ratio. + Android baseline is 160dpi (mdpi), so dpi_scale = density / 160. + This makes sapp_dpi_scale() behave consistently with macOS/Windows. */ + if (_sapp_tc.android.activity) { + AConfiguration* config = AConfiguration_new(); + AConfiguration_fromAssetManager(config, _sapp_tc.android.activity->assetManager); + int32_t density = AConfiguration_getDensity(config); + AConfiguration_delete(config); + if (density > 0) { + _sapp_tc.dpi_scale = (float)density / 160.0f; + } else { + _sapp_tc.dpi_scale = (float)fb_w / (float)win_w; + } + } else { + _sapp_tc.dpi_scale = (float)fb_w / (float)win_w; + } + if (win_changed || fb_changed || force_update) { + if (!_sapp_tc.first_frame) { + _sapp_tc_android_app_event(SAPP_EVENTTYPE_RESIZED); + } + } +} + +_SOKOL_PRIVATE void _sapp_tc_android_cleanup(void) { + if (_sapp_tc.android.surface != EGL_NO_SURFACE) { + /* egl context is bound, cleanup gracefully */ + if (_sapp_tc.init_called && !_sapp_tc.cleanup_called) { + _sapp_tc_call_cleanup(); + } + } + /* always try to cleanup by destroying egl context */ + _sapp_tc_android_cleanup_egl(); +} + +_SOKOL_PRIVATE void _sapp_tc_android_shutdown(void) { + /* try to cleanup while we still have a surface and can call cleanup_cb() */ + _sapp_tc_android_cleanup(); + /* request exit */ + ANativeActivity_finish(_sapp_tc.android.activity); +} + +_SOKOL_PRIVATE void _sapp_tc_android_frame(double external_now) { + SOKOL_ASSERT(_sapp_tc.android.display != EGL_NO_DISPLAY); + SOKOL_ASSERT(_sapp_tc.android.context != EGL_NO_CONTEXT); + SOKOL_ASSERT(_sapp_tc.android.surface != EGL_NO_SURFACE); + _sapp_tc_timing_update(&_sapp_tc.timing, external_now); + _sapp_tc_android_update_dimensions(_sapp_tc.android.current.window, false); + _sapp_tc_frame(); + // Modified by tettou771 for TrussC: skip present support + if (_sapp_tc.skip_present) { _sapp_tc.skip_present = false; return; } + eglSwapBuffers(_sapp_tc.android.display, _sapp_tc.android.surface); +} + +_SOKOL_PRIVATE bool _sapp_tc_android_touch_event(const AInputEvent* e) { + if (AInputEvent_getType(e) != AINPUT_EVENT_TYPE_MOTION) { + return false; + } + if (!_sapp_tc_events_enabled()) { + return false; + } + int32_t action_idx = AMotionEvent_getAction(e); + int32_t action = action_idx & AMOTION_EVENT_ACTION_MASK; + sapp_event_type type = SAPP_EVENTTYPE_INVALID; + switch (action) { + case AMOTION_EVENT_ACTION_DOWN: + case AMOTION_EVENT_ACTION_POINTER_DOWN: + type = SAPP_EVENTTYPE_TOUCHES_BEGAN; + break; + case AMOTION_EVENT_ACTION_MOVE: + type = SAPP_EVENTTYPE_TOUCHES_MOVED; + break; + case AMOTION_EVENT_ACTION_UP: + case AMOTION_EVENT_ACTION_POINTER_UP: + type = SAPP_EVENTTYPE_TOUCHES_ENDED; + break; + case AMOTION_EVENT_ACTION_CANCEL: + type = SAPP_EVENTTYPE_TOUCHES_CANCELLED; + break; + default: + break; + } + if (type == SAPP_EVENTTYPE_INVALID) { + return false; + } + int32_t idx = action_idx >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT; + _sapp_tc_init_event(type); + _sapp_tc.event.num_touches = (int)AMotionEvent_getPointerCount(e); + if (_sapp_tc.event.num_touches > SAPP_MAX_TOUCHPOINTS) { + _sapp_tc.event.num_touches = SAPP_MAX_TOUCHPOINTS; + } + for (int32_t i = 0; i < _sapp_tc.event.num_touches; i++) { + sapp_touchpoint* dst = &_sapp_tc.event.touches[i]; + dst->identifier = (uintptr_t)AMotionEvent_getPointerId(e, (size_t)i); + dst->pos_x = (AMotionEvent_getX(e, (size_t)i) / _sapp_tc.window_width) * _sapp_tc.framebuffer_width; + dst->pos_y = (AMotionEvent_getY(e, (size_t)i) / _sapp_tc.window_height) * _sapp_tc.framebuffer_height; + dst->android_tooltype = (sapp_android_tooltype) AMotionEvent_getToolType(e, (size_t)i); + if (action == AMOTION_EVENT_ACTION_POINTER_DOWN || + action == AMOTION_EVENT_ACTION_POINTER_UP) { + dst->changed = (i == idx); + } else { + dst->changed = true; + } + } + _sapp_tc_call_event(&_sapp_tc.event); + return true; +} + +_SOKOL_PRIVATE bool _sapp_tc_android_key_event(const AInputEvent* e) { + if (AInputEvent_getType(e) != AINPUT_EVENT_TYPE_KEY) { + return false; + } + if (AKeyEvent_getKeyCode(e) == AKEYCODE_BACK) { + /* [TrussC tettou771] Patched: BACK key no longer triggers shutdown. + Upstream calls _sapp_tc_android_shutdown() here, which destroys the + EGL surface and invokes cleanup_cb. Under screen pinning, the OS + blocks ANativeActivity_finish(), so the process keeps running but + the EGL context is gone — the app appears frozen. Consume the + event without shutdown. Long-term fix: implement + SAPP_EVENTTYPE_QUIT_REQUESTED for Android (already supported on + macOS/Windows/Linux). */ + return true; + } + return false; +} + +_SOKOL_PRIVATE int _sapp_tc_android_input_cb(int fd, int events, void* data) { + _SOKOL_UNUSED(fd); + _SOKOL_UNUSED(data); + if ((events & ALOOPER_EVENT_INPUT) == 0) { + _SAPP_ERROR(ANDROID_UNSUPPORTED_INPUT_EVENT_INPUT_CB); + return 1; + } + SOKOL_ASSERT(_sapp_tc.android.current.input); + AInputEvent* event = NULL; + while (AInputQueue_getEvent(_sapp_tc.android.current.input, &event) >= 0) { + if (AInputQueue_preDispatchEvent(_sapp_tc.android.current.input, event) != 0) { + continue; + } + int32_t handled = 0; + if (_sapp_tc_android_touch_event(event) || _sapp_tc_android_key_event(event)) { + handled = 1; + } + AInputQueue_finishEvent(_sapp_tc.android.current.input, event, handled); + } + return 1; +} + +_SOKOL_PRIVATE int _sapp_tc_android_main_cb(int fd, int events, void* data) { + _SOKOL_UNUSED(data); + if ((events & ALOOPER_EVENT_INPUT) == 0) { + _SAPP_ERROR(ANDROID_UNSUPPORTED_INPUT_EVENT_MAIN_CB); + return 1; + } + + _sapp_tc_android_msg_t msg; + if (read(fd, &msg, sizeof(msg)) != sizeof(msg)) { + _SAPP_ERROR(ANDROID_READ_MSG_FAILED); + return 1; + } + + pthread_mutex_lock(&_sapp_tc.android.pt.mutex); + switch (msg) { + case _SOKOL_ANDROID_MSG_CREATE: + { + _SAPP_INFO(ANDROID_MSG_CREATE); + SOKOL_ASSERT(!_sapp_tc.valid); + bool result = _sapp_tc_android_init_egl(); + SOKOL_ASSERT(result); _SOKOL_UNUSED(result); + _sapp_tc.valid = true; + _sapp_tc.android.has_created = true; + } + break; + case _SOKOL_ANDROID_MSG_RESUME: + _SAPP_INFO(ANDROID_MSG_RESUME); + _sapp_tc.android.has_resumed = true; + _sapp_tc_android_app_event(SAPP_EVENTTYPE_RESUMED); + break; + case _SOKOL_ANDROID_MSG_PAUSE: + _SAPP_INFO(ANDROID_MSG_PAUSE); + _sapp_tc.android.has_resumed = false; + _sapp_tc_android_app_event(SAPP_EVENTTYPE_SUSPENDED); + break; + case _SOKOL_ANDROID_MSG_FOCUS: + _SAPP_INFO(ANDROID_MSG_FOCUS); + _sapp_tc.android.has_focus = true; + break; + case _SOKOL_ANDROID_MSG_NO_FOCUS: + _SAPP_INFO(ANDROID_MSG_NO_FOCUS); + _sapp_tc.android.has_focus = false; + break; + case _SOKOL_ANDROID_MSG_SET_NATIVE_WINDOW: + _SAPP_INFO(ANDROID_MSG_SET_NATIVE_WINDOW); + if (_sapp_tc.android.current.window != _sapp_tc.android.pending.window) { + if (_sapp_tc.android.current.window != NULL) { + _sapp_tc_android_cleanup_egl_surface(); + } + if (_sapp_tc.android.pending.window != NULL) { + if (_sapp_tc_android_init_egl_surface(_sapp_tc.android.pending.window)) { + _sapp_tc_android_update_dimensions(_sapp_tc.android.pending.window, true); + } else { + _sapp_tc_android_shutdown(); + } + } + } + _sapp_tc.android.current.window = _sapp_tc.android.pending.window; + break; + case _SOKOL_ANDROID_MSG_SET_INPUT_QUEUE: + _SAPP_INFO(ANDROID_MSG_SET_INPUT_QUEUE); + if (_sapp_tc.android.current.input != _sapp_tc.android.pending.input) { + if (_sapp_tc.android.current.input != NULL) { + AInputQueue_detachLooper(_sapp_tc.android.current.input); + } + if (_sapp_tc.android.pending.input != NULL) { + AInputQueue_attachLooper( + _sapp_tc.android.pending.input, + _sapp_tc.android.looper, + ALOOPER_POLL_CALLBACK, + _sapp_tc_android_input_cb, + NULL); /* data */ + } + } + _sapp_tc.android.current.input = _sapp_tc.android.pending.input; + break; + case _SOKOL_ANDROID_MSG_DESTROY: + _SAPP_INFO(ANDROID_MSG_DESTROY); + _sapp_tc_android_cleanup(); + _sapp_tc.valid = false; + _sapp_tc.android.is_thread_stopping = true; + break; + default: + _SAPP_WARN(ANDROID_UNKNOWN_MSG); + break; + } + pthread_cond_broadcast(&_sapp_tc.android.pt.cond); /* signal "received" */ + pthread_mutex_unlock(&_sapp_tc.android.pt.mutex); + return 1; +} + +_SOKOL_PRIVATE bool _sapp_tc_android_should_update(void) { + bool is_in_front = _sapp_tc.android.has_resumed && _sapp_tc.android.has_focus; + bool has_surface = _sapp_tc.android.surface != EGL_NO_SURFACE; + return is_in_front && has_surface; +} + +#if __ANDROID_API__ >= 29 +_SOKOL_PRIVATE void _sapp_tc_android_frame_callback(int64_t frame_time_nanos, void* data) { + _SOKOL_UNUSED(data); + _sapp_tc.android.frame_callback_in_flight = false; + if (_sapp_tc.android.is_thread_stopping) { + return; + } + if (_sapp_tc_android_should_update()) { + // Post the next frame callback. We do this here rather than later so the runnable can be + // queued early in the looper. + AChoreographer_postFrameCallback64(_sapp_tc.android.choreographer, _sapp_tc_android_frame_callback, NULL); + _sapp_tc.android.frame_callback_in_flight = true; + _sapp_tc_android_frame((double)frame_time_nanos / 1.0e9); + } +} +#endif + +_SOKOL_PRIVATE void _sapp_tc_android_show_keyboard(bool shown) { + SOKOL_ASSERT(_sapp_tc.valid); + /* This seems to be broken in the NDK, but there is (a very cumbersome) workaround... */ + if (shown) { + ANativeActivity_showSoftInput(_sapp_tc.android.activity, ANATIVEACTIVITY_SHOW_SOFT_INPUT_FORCED); + } else { + ANativeActivity_hideSoftInput(_sapp_tc.android.activity, ANATIVEACTIVITY_HIDE_SOFT_INPUT_NOT_ALWAYS); + } +} + +_SOKOL_PRIVATE void* _sapp_tc_android_loop(void* arg) { + _SOKOL_UNUSED(arg); + _SAPP_INFO(ANDROID_LOOP_THREAD_STARTED); + + _sapp_tc.android.looper = ALooper_prepare(0 /* or ALOOPER_PREPARE_ALLOW_NON_CALLBACKS*/); + ALooper_addFd(_sapp_tc.android.looper, + _sapp_tc.android.pt.read_from_main_fd, + ALOOPER_POLL_CALLBACK, + ALOOPER_EVENT_INPUT, + _sapp_tc_android_main_cb, + NULL); /* data */ + + #if __ANDROID_API__ >= 29 + _sapp_tc.android.choreographer = AChoreographer_getInstance(); + if (_sapp_tc.android.choreographer != NULL) { + _SAPP_INFO(ANDROID_CHOREOGRAPHER_ENABLED); + } else { + _SAPP_INFO(ANDROID_CHOREOGRAPHER_UNAVAILABLE); + } + #else + _SAPP_INFO(ANDROID_CHOREOGRAPHER_UNAVAILABLE); + #endif + + /* signal start to main thread */ + pthread_mutex_lock(&_sapp_tc.android.pt.mutex); + _sapp_tc.android.is_thread_started = true; + pthread_cond_broadcast(&_sapp_tc.android.pt.cond); + pthread_mutex_unlock(&_sapp_tc.android.pt.mutex); + + /* main loop */ + while (!_sapp_tc.android.is_thread_stopping) { + #if __ANDROID_API__ >= 29 + if (_sapp_tc.android.choreographer != NULL) { + // Posts _sapp_tc_android_frame_callback with the choreographer to start our frame + // loop (for example, on first run or when resuming). When we have a choreographer, + // we'll get frame callbacks via _sapp_tc_android_frame_callback. + if (!_sapp_tc.android.frame_callback_in_flight && _sapp_tc_android_should_update()) { + AChoreographer_postFrameCallback64(_sapp_tc.android.choreographer, _sapp_tc_android_frame_callback, NULL); + _sapp_tc.android.frame_callback_in_flight = true; + } + // Blocks until the next event. We don't need a while loop here because we're + // already being driven by the outer while loop. + ALooper_pollOnce(-1, NULL, NULL, NULL); + continue; + } + #endif + // sokol frame -- fallback if not updating frames from choreographer callbacks + if (_sapp_tc_android_should_update()) { + _sapp_tc_android_frame(0.0); + } + + /* process all events (or stop early if app is requested to quit) */ + bool process_events = true; + while (process_events && !_sapp_tc.android.is_thread_stopping) { + bool block_until_event = !_sapp_tc.android.is_thread_stopping && !_sapp_tc_android_should_update(); + process_events = ALooper_pollOnce(block_until_event ? -1 : 0, NULL, NULL, NULL) == ALOOPER_POLL_CALLBACK; + } + } + + /* cleanup thread */ + if (_sapp_tc.android.current.input != NULL) { + AInputQueue_detachLooper(_sapp_tc.android.current.input); + } + + /* the following causes heap corruption on exit, why?? + ALooper_removeFd(_sapp_tc.android.looper, _sapp_tc.android.pt.read_from_main_fd); + ALooper_release(_sapp_tc.android.looper);*/ + + /* signal "destroyed" */ + pthread_mutex_lock(&_sapp_tc.android.pt.mutex); + _sapp_tc.android.is_thread_stopped = true; + pthread_cond_broadcast(&_sapp_tc.android.pt.cond); + pthread_mutex_unlock(&_sapp_tc.android.pt.mutex); + + _SAPP_INFO(ANDROID_LOOP_THREAD_DONE); + return NULL; +} + +/* android main/ui thread */ +_SOKOL_PRIVATE void _sapp_tc_android_msg(_sapp_tc_android_msg_t msg) { + if (write(_sapp_tc.android.pt.write_from_main_fd, &msg, sizeof(msg)) != sizeof(msg)) { + _SAPP_ERROR(ANDROID_WRITE_MSG_FAILED); + } +} + +_SOKOL_PRIVATE void _sapp_tc_android_on_start(ANativeActivity* activity) { + _SOKOL_UNUSED(activity); + _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONSTART); +} + +_SOKOL_PRIVATE void _sapp_tc_android_on_resume(ANativeActivity* activity) { + _SOKOL_UNUSED(activity); + _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONRESUME); + _sapp_tc_android_msg(_SOKOL_ANDROID_MSG_RESUME); +} + +_SOKOL_PRIVATE void* _sapp_tc_android_on_save_instance_state(ANativeActivity* activity, size_t* out_size) { + _SOKOL_UNUSED(activity); + _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONSAVEINSTANCESTATE); + *out_size = 0; + return NULL; +} + +_SOKOL_PRIVATE void _sapp_tc_android_on_window_focus_changed(ANativeActivity* activity, int has_focus) { + _SOKOL_UNUSED(activity); + _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONWINDOWFOCUSCHANGED); + if (has_focus) { + _sapp_tc_android_msg(_SOKOL_ANDROID_MSG_FOCUS); + } else { + _sapp_tc_android_msg(_SOKOL_ANDROID_MSG_NO_FOCUS); + } +} + +_SOKOL_PRIVATE void _sapp_tc_android_on_pause(ANativeActivity* activity) { + _SOKOL_UNUSED(activity); + _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONPAUSE); + _sapp_tc_android_msg(_SOKOL_ANDROID_MSG_PAUSE); +} + +_SOKOL_PRIVATE void _sapp_tc_android_on_stop(ANativeActivity* activity) { + _SOKOL_UNUSED(activity); + _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONSTOP); +} + +_SOKOL_PRIVATE void _sapp_tc_android_msg_set_native_window(ANativeWindow* window) { + pthread_mutex_lock(&_sapp_tc.android.pt.mutex); + _sapp_tc.android.pending.window = window; + _sapp_tc_android_msg(_SOKOL_ANDROID_MSG_SET_NATIVE_WINDOW); + while (_sapp_tc.android.current.window != window) { + pthread_cond_wait(&_sapp_tc.android.pt.cond, &_sapp_tc.android.pt.mutex); + } + pthread_mutex_unlock(&_sapp_tc.android.pt.mutex); +} + +_SOKOL_PRIVATE void _sapp_tc_android_on_native_window_created(ANativeActivity* activity, ANativeWindow* window) { + _SOKOL_UNUSED(activity); + _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONNATIVEWINDOWCREATED); + _sapp_tc_android_msg_set_native_window(window); +} + +_SOKOL_PRIVATE void _sapp_tc_android_on_native_window_destroyed(ANativeActivity* activity, ANativeWindow* window) { + _SOKOL_UNUSED(activity); + _SOKOL_UNUSED(window); + _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONNATIVEWINDOWDESTROYED); + _sapp_tc_android_msg_set_native_window(NULL); +} + +_SOKOL_PRIVATE void _sapp_tc_android_msg_set_input_queue(AInputQueue* input) { + pthread_mutex_lock(&_sapp_tc.android.pt.mutex); + _sapp_tc.android.pending.input = input; + _sapp_tc_android_msg(_SOKOL_ANDROID_MSG_SET_INPUT_QUEUE); + while (_sapp_tc.android.current.input != input) { + pthread_cond_wait(&_sapp_tc.android.pt.cond, &_sapp_tc.android.pt.mutex); + } + pthread_mutex_unlock(&_sapp_tc.android.pt.mutex); +} + +_SOKOL_PRIVATE void _sapp_tc_android_on_input_queue_created(ANativeActivity* activity, AInputQueue* queue) { + _SOKOL_UNUSED(activity); + _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONINPUTQUEUECREATED); + _sapp_tc_android_msg_set_input_queue(queue); +} + +_SOKOL_PRIVATE void _sapp_tc_android_on_input_queue_destroyed(ANativeActivity* activity, AInputQueue* queue) { + _SOKOL_UNUSED(activity); + _SOKOL_UNUSED(queue); + _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONINPUTQUEUEDESTROYED); + _sapp_tc_android_msg_set_input_queue(NULL); +} + +_SOKOL_PRIVATE void _sapp_tc_android_on_config_changed(ANativeActivity* activity) { + _SOKOL_UNUSED(activity); + _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONCONFIGURATIONCHANGED); + /* see android:configChanges in manifest */ +} + +_SOKOL_PRIVATE void _sapp_tc_android_on_low_memory(ANativeActivity* activity) { + _SOKOL_UNUSED(activity); + _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONLOWMEMORY); +} + +_SOKOL_PRIVATE void _sapp_tc_android_on_destroy(ANativeActivity* activity) { + /* + * For some reason even an empty app using nativeactivity.h will crash (WIN DEATH) + * on my device (Moto X 2nd gen) when the app is removed from the task view + * (TaskStackView: onTaskViewDismissed). + * + * However, if ANativeActivity_finish() is explicitly called from for example + * _sapp_tc_android_on_stop(), the crash disappears. Is this a bug in NativeActivity? + */ + _SOKOL_UNUSED(activity); + _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONDESTROY); + + /* send destroy msg */ + pthread_mutex_lock(&_sapp_tc.android.pt.mutex); + _sapp_tc_android_msg(_SOKOL_ANDROID_MSG_DESTROY); + while (!_sapp_tc.android.is_thread_stopped) { + pthread_cond_wait(&_sapp_tc.android.pt.cond, &_sapp_tc.android.pt.mutex); + } + pthread_mutex_unlock(&_sapp_tc.android.pt.mutex); + + /* clean up main thread */ + pthread_cond_destroy(&_sapp_tc.android.pt.cond); + pthread_mutex_destroy(&_sapp_tc.android.pt.mutex); + + close(_sapp_tc.android.pt.read_from_main_fd); + close(_sapp_tc.android.pt.write_from_main_fd); + + _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_DONE); + + /* this is a bit naughty, but causes a clean restart of the app (static globals are reset) */ + exit(0); +} + +JNIEXPORT +void ANativeActivity_onCreate(ANativeActivity* activity, void* saved_state, size_t saved_state_size) { + _SOKOL_UNUSED(saved_state); + _SOKOL_UNUSED(saved_state_size); + _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_ONCREATE); + + // the NativeActity pointer needs to be available inside sokol_main() + // (see https://github.com/floooh/sokol/issues/708), however _sapp_tc_init_state() + // will clear the global _sapp_tc_t struct, so we need to initialize the native + // activity pointer twice, once before sokol_main() and once after _sapp_tc_init_state() + _sapp_tc_clear(&_sapp_tc, sizeof(_sapp_tc)); + _sapp_tc.android.activity = activity; + sapp_desc desc = sokol_main(0, NULL); + _sapp_tc_init_state(&desc); + _sapp_tc.android.activity = activity; + + int pipe_fd[2]; + if (pipe(pipe_fd) != 0) { + _SAPP_ERROR(ANDROID_CREATE_THREAD_PIPE_FAILED); + return; + } + _sapp_tc.android.pt.read_from_main_fd = pipe_fd[0]; + _sapp_tc.android.pt.write_from_main_fd = pipe_fd[1]; + + pthread_mutex_init(&_sapp_tc.android.pt.mutex, NULL); + pthread_cond_init(&_sapp_tc.android.pt.cond, NULL); + + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + pthread_create(&_sapp_tc.android.pt.thread, &attr, _sapp_tc_android_loop, 0); + pthread_attr_destroy(&attr); + + /* wait until main loop has started */ + pthread_mutex_lock(&_sapp_tc.android.pt.mutex); + while (!_sapp_tc.android.is_thread_started) { + pthread_cond_wait(&_sapp_tc.android.pt.cond, &_sapp_tc.android.pt.mutex); + } + pthread_mutex_unlock(&_sapp_tc.android.pt.mutex); + + /* send create msg */ + pthread_mutex_lock(&_sapp_tc.android.pt.mutex); + _sapp_tc_android_msg(_SOKOL_ANDROID_MSG_CREATE); + while (!_sapp_tc.android.has_created) { + pthread_cond_wait(&_sapp_tc.android.pt.cond, &_sapp_tc.android.pt.mutex); + } + pthread_mutex_unlock(&_sapp_tc.android.pt.mutex); + + /* register for callbacks */ + activity->callbacks->onStart = _sapp_tc_android_on_start; + activity->callbacks->onResume = _sapp_tc_android_on_resume; + activity->callbacks->onSaveInstanceState = _sapp_tc_android_on_save_instance_state; + activity->callbacks->onWindowFocusChanged = _sapp_tc_android_on_window_focus_changed; + activity->callbacks->onPause = _sapp_tc_android_on_pause; + activity->callbacks->onStop = _sapp_tc_android_on_stop; + activity->callbacks->onDestroy = _sapp_tc_android_on_destroy; + activity->callbacks->onNativeWindowCreated = _sapp_tc_android_on_native_window_created; + /* activity->callbacks->onNativeWindowResized = _sapp_tc_android_on_native_window_resized; */ + /* activity->callbacks->onNativeWindowRedrawNeeded = _sapp_tc_android_on_native_window_redraw_needed; */ + activity->callbacks->onNativeWindowDestroyed = _sapp_tc_android_on_native_window_destroyed; + activity->callbacks->onInputQueueCreated = _sapp_tc_android_on_input_queue_created; + activity->callbacks->onInputQueueDestroyed = _sapp_tc_android_on_input_queue_destroyed; + /* activity->callbacks->onContentRectChanged = _sapp_tc_android_on_content_rect_changed; */ + /* activity->callbacks->onConfigurationChanged = _sapp_tc_android_on_config_changed; */ + activity->callbacks->onLowMemory = _sapp_tc_android_on_low_memory; + + _SAPP_INFO(ANDROID_NATIVE_ACTIVITY_CREATE_SUCCESS); + + /* NOT A BUG: do NOT call sapp_discard_state() */ +} + +#endif /* _SAPP_ANDROID */ +/* ---- public API (lifted from sokol_app.h >>public, web-live branches) ---- */ +#if defined(SOKOL_NO_ENTRY) +SOKOL_API_IMPL void sapp_run(const sapp_desc* desc) { + SOKOL_ASSERT(desc); + #if defined(_SAPP_MACOS) + _sapp_tc_macos_run(desc); + #elif defined(_SAPP_IOS) + _sapp_tc_ios_run(desc); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_run(desc); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_run(desc); + #elif defined(_SAPP_LINUX) + _sapp_tc_linux_run(desc); + #else + #error "sapp_run() not supported on this platform" + #endif +} + +/* this is just a stub so the linker doesn't complain */ +sapp_desc sokol_main(int argc, char* argv[]) { + _SOKOL_UNUSED(argc); + _SOKOL_UNUSED(argv); + _SAPP_STRUCT(sapp_desc, desc); + return desc; +} +#else +/* likewise, in normal mode, sapp_run() is just an empty stub */ +SOKOL_API_IMPL void sapp_run(const sapp_desc* desc) { + _SOKOL_UNUSED(desc); +} +#endif + +SOKOL_API_IMPL bool sapp_isvalid(void) { + return _sapp_tc.valid; +} + +SOKOL_API_IMPL void* sapp_userdata(void) { + return _sapp_tc.desc.user_data; +} + +SOKOL_API_IMPL sapp_desc sapp_query_desc(void) { + return _sapp_tc.desc; +} + +SOKOL_API_IMPL uint64_t sapp_frame_count(void) { + return _sapp_tc.frame_count; +} + +SOKOL_API_IMPL double sapp_frame_duration(void) { + #if defined(_SAPP_MACOS) && defined(SOKOL_METAL) + return _sapp_tc_macos_mtl_timing_frame_duration(); + #elif defined(_SAPP_IOS) && defined(SOKOL_METAL) + return _sapp_tc_ios_mtl_timing_frame_duration(); + #else + return _sapp_tc_timing_get(&_sapp_tc.timing); + #endif +} + +SOKOL_API_IMPL double sapp_frame_duration_unfiltered(void) { + return _sapp_tc.timing.dt; +} + +SOKOL_API_IMPL int sapp_width(void) { + return (_sapp_tc.framebuffer_width > 0) ? _sapp_tc.framebuffer_width : 1; +} + +SOKOL_API_IMPL float sapp_widthf(void) { + return (float)sapp_width(); +} + +SOKOL_API_IMPL int sapp_height(void) { + return (_sapp_tc.framebuffer_height > 0) ? _sapp_tc.framebuffer_height : 1; +} + +SOKOL_API_IMPL float sapp_heightf(void) { + return (float)sapp_height(); +} + +SOKOL_API_IMPL sapp_pixel_format sapp_color_format(void) { + #if defined(SOKOL_WGPU) + switch (_sapp_tc.wgpu.render_format) { + case WGPUTextureFormat_RGBA8Unorm: + return SAPP_PIXELFORMAT_RGBA8; + case WGPUTextureFormat_BGRA8Unorm: + return SAPP_PIXELFORMAT_BGRA8; + default: + SOKOL_UNREACHABLE; + return SAPP_PIXELFORMAT_NONE; + } + #elif defined(SOKOL_VULKAN) + switch (_sapp_tc.vk.surface_format.format) { + case VK_FORMAT_R8G8B8A8_UNORM: + return SAPP_PIXELFORMAT_RGBA8; + case VK_FORMAT_B8G8R8A8_UNORM: + return SAPP_PIXELFORMAT_BGRA8; + default: + // FIXME! + SOKOL_UNREACHABLE; + return SAPP_PIXELFORMAT_NONE; + } + #elif defined(SOKOL_METAL) || defined(SOKOL_D3D11) + // Modified by tettou771 for TrussC: report 10-bit color format (RGB10A2) + return SAPP_PIXELFORMAT_RGB10A2; + #else + return SAPP_PIXELFORMAT_RGBA8; + #endif +} + +SOKOL_API_IMPL sapp_pixel_format sapp_depth_format(void) { + return SAPP_PIXELFORMAT_DEPTH_STENCIL; +} + +SOKOL_API_IMPL int sapp_sample_count(void) { + return _sapp_tc.sample_count; +} + +SOKOL_API_IMPL bool sapp_high_dpi(void) { + return _sapp_tc.desc.high_dpi && (_sapp_tc.dpi_scale >= 1.5f); +} + +SOKOL_API_IMPL float sapp_dpi_scale(void) { + return _sapp_tc.dpi_scale; +} + +SOKOL_API_IMPL const void* sapp_egl_get_display(void) { + SOKOL_ASSERT(_sapp_tc.valid); + #if defined(_SAPP_ANDROID) + return _sapp_tc.android.display; + #elif defined(_SAPP_LINUX) && defined(_SAPP_EGL) + return _sapp_tc.egl.display; + #else + return 0; + #endif +} + +SOKOL_API_IMPL const void* sapp_egl_get_context(void) { + SOKOL_ASSERT(_sapp_tc.valid); + #if defined(_SAPP_ANDROID) + return _sapp_tc.android.context; + #elif defined(_SAPP_LINUX) && defined(_SAPP_EGL) + return _sapp_tc.egl.context; + #else + return 0; + #endif +} + +SOKOL_API_IMPL void sapp_show_keyboard(bool show) { + #if defined(_SAPP_IOS) + _sapp_tc_ios_show_keyboard(show); + #elif defined(_SAPP_ANDROID) + _sapp_tc_android_show_keyboard(show); + #else + _SOKOL_UNUSED(show); + #endif +} + +SOKOL_API_IMPL bool sapp_keyboard_shown(void) { + return _sapp_tc.onscreen_keyboard_shown; +} + +SOKOL_API_IMPL bool sapp_is_fullscreen(void) { + return _sapp_tc.fullscreen; +} + +SOKOL_API_IMPL void sapp_toggle_fullscreen(void) { + #if defined(_SAPP_MACOS) + _sapp_tc_macos_toggle_fullscreen(); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_toggle_fullscreen(); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_toggle_fullscreen(); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_toggle_fullscreen(); + #endif +} + +_SOKOL_PRIVATE void _sapp_tc_update_cursor(sapp_mouse_cursor cursor, bool shown) { + #if defined(_SAPP_MACOS) + _sapp_tc_macos_update_cursor(cursor, shown); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_update_cursor(cursor, shown, false); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_update_cursor(cursor, shown); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_update_cursor(cursor, shown); + #endif + _sapp_tc.mouse.current_cursor = cursor; + _sapp_tc.mouse.shown = shown; +} + +/* NOTE that sapp_show_mouse() does not "stack" like the Win32 or macOS API functions! */ +SOKOL_API_IMPL void sapp_show_mouse(bool show) { + if (_sapp_tc.mouse.shown != show) { + _sapp_tc_update_cursor(_sapp_tc.mouse.current_cursor, show); + } +} + +SOKOL_API_IMPL bool sapp_mouse_shown(void) { + return _sapp_tc.mouse.shown; +} + +SOKOL_API_IMPL void sapp_lock_mouse(bool lock) { + #if defined(_SAPP_MACOS) + _sapp_tc_macos_lock_mouse(lock); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_lock_mouse(lock); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_lock_mouse(lock); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_lock_mouse(lock); + #else + _sapp_tc.mouse.locked = lock; + #endif +} + +SOKOL_API_IMPL bool sapp_mouse_locked(void) { + return _sapp_tc.mouse.locked; +} + +SOKOL_API_IMPL void sapp_set_mouse_cursor(sapp_mouse_cursor cursor) { + SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); + if (_sapp_tc.mouse.current_cursor != cursor) { + _sapp_tc_update_cursor(cursor, _sapp_tc.mouse.shown); + } +} + +SOKOL_API_IMPL sapp_mouse_cursor sapp_get_mouse_cursor(void) { + return _sapp_tc.mouse.current_cursor; +} + +SOKOL_API_IMPL sapp_mouse_cursor sapp_bind_mouse_cursor_image(sapp_mouse_cursor cursor, const sapp_image_desc* desc) { + SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); + // NOTE: It seems that for some reason, the hotspot doesn't work if it is one less + // than the dimension of the cursor image (or more), on windows. So for a cursor + // that is 32 by 32 px, a hotspot of x = 30 works, but not x = 31. + // The cursor simply dissapears in such cases. Asserting for all platforms to make + // the behaviour consistent. + SOKOL_ASSERT(desc->cursor_hotspot_x < desc->width - 1 && desc->cursor_hotspot_y < desc->height - 1); + SOKOL_ASSERT(desc->width * desc->height * 4 == (int) desc->pixels.size); + + sapp_unbind_mouse_cursor_image(cursor); + + bool res = false; + #if defined(_SAPP_MACOS) + res = _sapp_tc_macos_make_custom_mouse_cursor(cursor, desc); + #elif defined(_SAPP_EMSCRIPTEN) + res = _sapp_tc_emsc_make_custom_mouse_cursor(cursor, desc); + #elif defined(_SAPP_WIN32) + res = _sapp_tc_win32_make_custom_mouse_cursor(cursor, desc); + #elif defined(_SAPP_LINUX) + res = _sapp_tc_x11_make_custom_mouse_cursor(cursor, desc); + #else + _SOKOL_UNUSED(desc); + #endif + _sapp_tc.custom_cursor_bound[(int)cursor] = res; + + // Update the displayed cursor in case the current cursor is the one we just bound. + if (_sapp_tc.mouse.current_cursor == cursor) { + _sapp_tc_update_cursor(cursor, _sapp_tc.mouse.shown); + } + return cursor; // returning the passed-in cursor puerly for convenience, in case you want to asign the value to a variable. +} + +SOKOL_API_IMPL void sapp_unbind_mouse_cursor_image(sapp_mouse_cursor cursor) { + SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); + if (_sapp_tc.custom_cursor_bound[(int)cursor]) { + // if this is the active cursor, first restore it to its default image, + // this must be done before attempting to destroy any cursor image + // resources which at least on win32 would fail if the cursor is still in use + _sapp_tc.custom_cursor_bound[(int)cursor] = false; + if (_sapp_tc.mouse.current_cursor == cursor) { + _sapp_tc_update_cursor(cursor, _sapp_tc.mouse.shown); + } + #if defined(_SAPP_MACOS) + _sapp_tc_macos_destroy_custom_mouse_cursor(cursor); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_destroy_custom_mouse_cursor(cursor); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_destroy_custom_mouse_cursor(cursor); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_destroy_custom_mouse_cursor(cursor); + #endif + } +} + +SOKOL_API_IMPL void sapp_request_quit(void) { + _sapp_tc.quit_requested = true; +} + +SOKOL_API_IMPL void sapp_cancel_quit(void) { + _sapp_tc.quit_requested = false; +} + +SOKOL_API_IMPL void sapp_quit(void) { + _sapp_tc.quit_ordered = true; +} + +SOKOL_API_IMPL void sapp_consume_event(void) { + _sapp_tc.event_consumed = true; +} + +/* NOTE: on HTML5, sapp_set_clipboard_string() must be called from within event handler! */ +SOKOL_API_IMPL void sapp_set_clipboard_string(const char* str) { + if (!_sapp_tc.clipboard.enabled) { + return; + } + SOKOL_ASSERT(str); + #if defined(_SAPP_MACOS) + _sapp_tc_macos_set_clipboard_string(str); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_set_clipboard_string(str); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_set_clipboard_string(str); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_set_clipboard_string(str); + #else + /* not implemented */ + #endif + _sapp_tc_strcpy(str, _sapp_tc.clipboard.buffer, (size_t)_sapp_tc.clipboard.buf_size); +} + +SOKOL_API_IMPL const char* sapp_get_clipboard_string(void) { + if (!_sapp_tc.clipboard.enabled) { + return ""; + } + #if defined(_SAPP_MACOS) + return _sapp_tc_macos_get_clipboard_string(); + #elif defined(_SAPP_EMSCRIPTEN) + return _sapp_tc.clipboard.buffer; + #elif defined(_SAPP_WIN32) + return _sapp_tc_win32_get_clipboard_string(); + #elif defined(_SAPP_LINUX) + return _sapp_tc_x11_get_clipboard_string(); + #else + /* not implemented */ + return _sapp_tc.clipboard.buffer; + #endif +} + +SOKOL_API_IMPL void sapp_set_window_title(const char* title) { + SOKOL_ASSERT(title); + _sapp_tc_strcpy(title, _sapp_tc.window_title, sizeof(_sapp_tc.window_title)); + #if defined(_SAPP_MACOS) + _sapp_tc_macos_update_window_title(); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_update_window_title(); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_update_window_title(); + #endif +} + +SOKOL_API_IMPL void sapp_set_icon(const sapp_icon_desc* desc) { + SOKOL_ASSERT(desc); + if (desc->sokol_default) { + /* the sokol default-icon generator is not ported: the favicon is the + page's business unless the app provides real icon images */ + return; + } + const int num_images = _sapp_tc_icon_num_images(desc); + if (num_images == 0) { + return; + } + SOKOL_ASSERT((num_images > 0) && (num_images <= SAPP_MAX_ICONIMAGES)); + if (!_sapp_tc_validate_icon_desc(desc, num_images)) { + return; + } + #if defined(_SAPP_MACOS) + _sapp_tc_macos_set_icon(desc, num_images); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_set_icon(desc, num_images); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_set_icon(desc, num_images); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_set_icon(desc, num_images); + #endif +} + +SOKOL_API_IMPL int sapp_get_num_dropped_files(void) { + if (!_sapp_tc.drop.enabled) { + return 0; + } + return _sapp_tc.drop.num_files; +} + +SOKOL_API_IMPL const char* sapp_get_dropped_file_path(int index) { + SOKOL_ASSERT((index >= 0) && (index < _sapp_tc.drop.num_files)); + if (!_sapp_tc.drop.enabled) { + return ""; + } + SOKOL_ASSERT(_sapp_tc.drop.buffer); + if ((index < 0) || (index >= _sapp_tc.drop.max_files)) { + return ""; + } + return (const char*) _sapp_tc_dropped_file_path_ptr(index); +} + +SOKOL_API_IMPL uint32_t sapp_html5_get_dropped_file_size(int index) { + SOKOL_ASSERT((index >= 0) && (index < _sapp_tc.drop.num_files)); + #if defined(_SAPP_EMSCRIPTEN) + if (!_sapp_tc.drop.enabled) { + return 0; + } + return _sapp_tc_js_dropped_file_size(index); + #else + (void)index; + return 0; + #endif +} + +SOKOL_API_IMPL void sapp_html5_fetch_dropped_file(const sapp_html5_fetch_request* request) { + SOKOL_ASSERT(_sapp_tc.drop.enabled); + SOKOL_ASSERT(request); + SOKOL_ASSERT(request->callback); + SOKOL_ASSERT(request->buffer.ptr); + SOKOL_ASSERT(request->buffer.size > 0); + #if defined(_SAPP_EMSCRIPTEN) + const int index = request->dropped_file_index; + sapp_html5_fetch_error error_code = SAPP_HTML5_FETCH_ERROR_NO_ERROR; + if ((index < 0) || (index >= _sapp_tc.drop.num_files)) { + error_code = SAPP_HTML5_FETCH_ERROR_OTHER; + } + if (sapp_html5_get_dropped_file_size(index) > request->buffer.size) { + error_code = SAPP_HTML5_FETCH_ERROR_BUFFER_TOO_SMALL; + } + if (SAPP_HTML5_FETCH_ERROR_NO_ERROR != error_code) { + _sapp_tc_emsc_invoke_fetch_cb(index, + false, // success + (int)error_code, + request->callback, + 0, // fetched_size + (void*)request->buffer.ptr, + request->buffer.size, + request->user_data); + } else { + _sapp_tc_js_fetch_dropped_file(index, + request->callback, + (void*)request->buffer.ptr, + request->buffer.size, + request->user_data); + } + #else + (void)request; + #endif +} + +SOKOL_API_IMPL sapp_environment sapp_get_environment(void) { + SOKOL_ASSERT(_sapp_tc.valid); + _SAPP_STRUCT(sapp_environment, res); + res.defaults.color_format = sapp_color_format(); + res.defaults.depth_format = sapp_depth_format(); + res.defaults.sample_count = sapp_sample_count(); + #if defined(SOKOL_METAL) + #if defined(_SAPP_MACOS) + res.metal.device = (__bridge const void*) _sapp_tc.macos.mtl.device; + #else + res.metal.device = (__bridge const void*) _sapp_tc.ios.mtl.device; + #endif + #endif + #if defined(SOKOL_D3D11) + res.d3d11.device = (const void*) _sapp_tc.d3d11.device; + res.d3d11.device_context = (const void*) _sapp_tc.d3d11.device_context; + #endif + #if defined(SOKOL_WGPU) + res.wgpu.device = (const void*) _sapp_tc.wgpu.device; + #endif + #if defined(SOKOL_VULKAN) + res.vulkan.instance = (const void*) _sapp_tc.vk.instance; + res.vulkan.physical_device = (const void*) _sapp_tc.vk.physical_device; + res.vulkan.device = (const void*) _sapp_tc.vk.device; + res.vulkan.queue = (const void*) _sapp_tc.vk.queue; + res.vulkan.queue_family_index = _sapp_tc.vk.queue_family_index; + #endif + return res; +} + +SOKOL_API_IMPL sapp_swapchain sapp_get_swapchain(void) { + SOKOL_ASSERT(_sapp_tc.valid); + _SAPP_STRUCT(sapp_swapchain, res); + #if defined(SOKOL_METAL) + #if defined(_SAPP_MACOS) + res.metal.current_drawable = (__bridge const void*) _sapp_tc_macos_mtl_swapchain_next(); + res.metal.depth_stencil_texture = (__bridge const void*) _sapp_tc.macos.mtl.depth_tex; + res.metal.msaa_color_texture = (__bridge const void*) _sapp_tc.macos.mtl.msaa_tex; + #else + res.metal.current_drawable = (__bridge const void*) _sapp_tc_ios_mtl_swapchain_next(); + res.metal.depth_stencil_texture = (__bridge const void*) _sapp_tc.ios.mtl.depth_tex; + res.metal.msaa_color_texture = (__bridge const void*) _sapp_tc.ios.mtl.msaa_tex; + #endif + #endif + #if defined(SOKOL_D3D11) + SOKOL_ASSERT(_sapp_tc.d3d11.rtv); + if (_sapp_tc.sample_count > 1) { + SOKOL_ASSERT(_sapp_tc.d3d11.msaa_rtv); + res.d3d11.render_view = (const void*) _sapp_tc.d3d11.msaa_rtv; + res.d3d11.resolve_view = (const void*) _sapp_tc.d3d11.rtv; + } else { + res.d3d11.render_view = (const void*) _sapp_tc.d3d11.rtv; + } + res.d3d11.depth_stencil_view = (const void*) _sapp_tc.d3d11.dsv; + #endif + #if defined(SOKOL_WGPU) + SOKOL_ASSERT(0 == _sapp_tc.wgpu.swapchain_view); + _sapp_tc_wgpu_swapchain_next(); + // FIXME: swapchain_view being null must be allowed and should skip the frame + SOKOL_ASSERT(_sapp_tc.wgpu.swapchain_view); + if (_sapp_tc.sample_count > 1) { + SOKOL_ASSERT(_sapp_tc.wgpu.msaa_view); + res.wgpu.render_view = (const void*) _sapp_tc.wgpu.msaa_view; + res.wgpu.resolve_view = (const void*) _sapp_tc.wgpu.swapchain_view; + } else { + res.wgpu.render_view = (const void*) _sapp_tc.wgpu.swapchain_view; + } + res.wgpu.depth_stencil_view = (const void*) _sapp_tc.wgpu.depth_stencil_view; + #endif + #if defined(SOKOL_VULKAN) + _sapp_tc_vk_swapchain_next(); + // FIXME: swapchain_view being null must be allowed and should skip the frame + uint32_t img_idx = _sapp_tc.vk.cur_swapchain_image_index; + if (_sapp_tc.sample_count > 1) { + SOKOL_ASSERT(_sapp_tc.vk.msaa.img && _sapp_tc.vk.msaa.view); + res.vulkan.render_image = (const void*) _sapp_tc.vk.msaa.img; + res.vulkan.render_view = (const void*) _sapp_tc.vk.msaa.view; + res.vulkan.resolve_image = (const void*) _sapp_tc.vk.swapchain_images[img_idx]; + res.vulkan.resolve_view = (const void*) _sapp_tc.vk.swapchain_views[img_idx]; + } else { + res.vulkan.render_image = (const void*) _sapp_tc.vk.swapchain_images[img_idx]; + res.vulkan.render_view = (const void*) _sapp_tc.vk.swapchain_views[img_idx]; + } + res.vulkan.depth_stencil_image = (const void*) _sapp_tc.vk.depth.img; + res.vulkan.depth_stencil_view = (const void*) _sapp_tc.vk.depth.view; + // NOTE: using the current swapchain image index here is *NOT* a bug! The render_finished_semaphore *must* + // be associated with its swapchain image in case the swapchain implementation doesn't return swapchain images in order + res.vulkan.render_finished_semaphore = _sapp_tc.vk.sync[img_idx].render_finished_sem; + res.vulkan.present_complete_semaphore = _sapp_tc.vk.sync[_sapp_tc.vk.sync_slot].present_complete_sem; + #endif + #if defined(_SAPP_ANY_GL) + res.gl.framebuffer = _sapp_tc.gl.framebuffer; + #endif + res.width = sapp_width(); + res.height = sapp_height(); + res.color_format = sapp_color_format(); + res.depth_format = sapp_depth_format(); + res.sample_count = sapp_sample_count(); + return res; +} + +SOKOL_API_IMPL const void* sapp_macos_get_window(void) { + #if defined(_SAPP_MACOS) + const void* obj = (__bridge const void*) _sapp_tc.macos.window; + SOKOL_ASSERT(obj); + return obj; + #else + return 0; + #endif +} + +SOKOL_API_IMPL const void* sapp_ios_get_window(void) { + #if defined(_SAPP_IOS) + const void* obj = (__bridge const void*) _sapp_tc.ios.window; + SOKOL_ASSERT(obj); + return obj; + #else + return 0; + #endif +} + +// Modified by tettou771 for TrussC: runtime orientation control +SOKOL_API_IMPL void sapp_ios_set_supported_orientations(uint32_t mask) { + #if defined(_SAPP_IOS) + _sapp_tc.ios.supported_orientations = (NSUInteger)mask; + #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 160000 + if (@available(iOS 16.0, *)) { + [_sapp_tc.ios.view_ctrl setNeedsUpdateOfSupportedInterfaceOrientations]; + } + #endif + #else + (void)mask; + #endif +} + +SOKOL_API_IMPL const void* sapp_d3d11_get_swap_chain(void) { + SOKOL_ASSERT(_sapp_tc.valid); +#if defined(SOKOL_D3D11) + return _sapp_tc.d3d11.swap_chain; +#else + return 0; +#endif +} + +SOKOL_API_IMPL const void* sapp_win32_get_hwnd(void) { + SOKOL_ASSERT(_sapp_tc.valid); + #if defined(_SAPP_WIN32) + return _sapp_tc.win32.hwnd; + #else + return 0; + #endif +} + +SOKOL_API_IMPL int sapp_gl_get_major_version(void) { + SOKOL_ASSERT(_sapp_tc.valid); + #if defined(_SAPP_ANY_GL) + return _sapp_tc.desc.gl.major_version; + #else + return 0; + #endif +} + +SOKOL_API_IMPL int sapp_gl_get_minor_version(void) { + SOKOL_ASSERT(_sapp_tc.valid); + #if defined(_SAPP_ANY_GL) + return _sapp_tc.desc.gl.minor_version; + #else + return 0; + #endif +} + +SOKOL_API_IMPL bool sapp_gl_is_gles(void) { + #if defined(SOKOL_GLES3) + return true; + #else + return false; + #endif +} + +SOKOL_API_IMPL const void* sapp_x11_get_window(void) { + #if defined(_SAPP_LINUX) + return (void*)_sapp_tc.x11.window; + #else + return 0; + #endif +} + +SOKOL_API_IMPL const void* sapp_x11_get_display(void) { + #if defined(_SAPP_LINUX) + return (void*)_sapp_tc.x11.display; + #else + return 0; + #endif +} + +SOKOL_API_IMPL const void* sapp_android_get_native_activity(void) { + // NOTE: _sapp_tc.valid is not asserted here because sapp_android_get_native_activity() + // needs to be callable from within sokol_main() (see: https://github.com/floooh/sokol/issues/708) + #if defined(_SAPP_ANDROID) + return (void*)_sapp_tc.android.activity; + #else + return 0; + #endif +} + +SOKOL_API_IMPL void sapp_html5_ask_leave_site(bool ask) { + _sapp_tc.html5_ask_leave_site = ask; +} + +// Modified by tettou771 for TrussC: skip the next present call (fix D3D11 flickering) +SOKOL_API_IMPL void sapp_skip_present(void) { + _sapp_tc.skip_present = true; +} +// end of modification + + + +/* ---- other-backend getters (zero stubs, same set as the native sections) - */ +#if defined(__cplusplus) +extern "C" { +#endif +const void* sapp_metal_get_device(void) { return 0; } +const void* sapp_metal_get_current_drawable(void) { return 0; } +const void* sapp_metal_get_depth_stencil_texture(void) { return 0; } +const void* sapp_metal_get_msaa_color_texture(void) { return 0; } +const void* sapp_d3d11_get_device(void) { return 0; } +const void* sapp_d3d11_get_device_context(void) { return 0; } + +/* ---- multi-window API: explicit platform gap on Android ------------------ + the activity owns exactly one ANativeWindow; a second window is not + representable */ +sapp_window sapp_create_window(const sapp_window_desc* desc) { + _SOKOL_UNUSED(desc); + __android_log_print(ANDROID_LOG_ERROR, "sokol_app_tc", "sapp_create_window() is not supported on Android (single-window platform)"); + sapp_window w = {0}; + return w; +} +void sapp_destroy_window(sapp_window win) { _SOKOL_UNUSED(win); } +bool sapp_window_valid(sapp_window win) { _SOKOL_UNUSED(win); return false; } +int sapp_window_width(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +int sapp_window_height(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +int sapp_window_framebuffer_width(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +int sapp_window_framebuffer_height(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +float sapp_window_dpi_scale(sapp_window win) { _SOKOL_UNUSED(win); return 1.0f; } +int sapp_window_sample_count(sapp_window win) { _SOKOL_UNUSED(win); return 1; } +bool sapp_window_occluded(sapp_window win) { _SOKOL_UNUSED(win); return false; } +void sapp_window_set_title(sapp_window win, const char* title) { _SOKOL_UNUSED(win); _SOKOL_UNUSED(title); } +double sapp_window_frame_duration(sapp_window win) { _SOKOL_UNUSED(win); return 1.0 / 60.0; } +const void* sapp_window_metal_current_drawable(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_metal_depth_stencil_texture(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_metal_msaa_color_texture(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_d3d11_render_view(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_d3d11_resolve_view(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_d3d11_depth_stencil_view(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_macos_get_window(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_win32_get_hwnd(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_x11_get_window(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +uint32_t sapp_window_gl_framebuffer(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +#if defined(__cplusplus) +} /* extern "C" */ +#endif + +#elif defined(__EMSCRIPTEN__) +/*== Emscripten (web) ======================================================= + Implements the public sokol_app.h API on the web plus the multi-window + API (as stubs -- the whole backend binds to ONE element, so a + second window is not representable; sapp_create_window() returns the + invalid handle and logs). + + Unlike every native backend there is NO owned loop: sapp_run() sets + everything up, hands a per-frame callback to + emscripten_request_animation_frame_loop() (or emscripten_set_main_loop()) + and RETURNS IMMEDIATELY. Rendering, events and the quit dance all happen + later inside browser-driven callbacks; "quit" stops the rAF loop and + tears down sokol state -- the tab and the wasm module stay alive. + + Structure mirrors sokol_app.h's emscripten backend (HTML5 event + callbacks on the canvas selector, code-string keymap, deferred + pointer-lock on user gesture, execCommand copy + paste-event clipboard, + dragndrop with FileReader fetch, CSS cursors + BMP-in-JS custom cursors, + favicon icon, resize tracking via CSS size x devicePixelRatio). Both + graphics backends are supported: SOKOL_WGPU (async adapter/device chain + gated by init_done, browser auto-presents the surface at rAF boundaries) + and SOKOL_GLES3 (WebGL2 context, swapchain = default framebuffer). + + The TrussC keyboard-on-canvas patch is native here: key handlers are + registered on the canvas selector (NOT the window), so other page + elements (e.g. an editor besides the canvas) receive keyboard input + independently. The canvas element needs a tabindex attribute and focus + for keyboard events to arrive (the TrussC web shell provides both). */ +#if defined(SOKOL_WGPU) +#include /* emdawnwebgpu (modern Dawn API), NOT -sUSE_WEBGPU */ +#elif defined(SOKOL_GLES3) +#include +#else +#error "sokol_app_tc.h: web build requires SOKOL_WGPU or SOKOL_GLES3" +#endif +#include +#include +#include + +#if defined(SOKOL_APP_IMPL_INCLUDED) +#error "sokol_app_tc.h owns the web implementation of the sapp_* API; include sokol_app.h WITHOUT an implementation define in this TU" +#endif + +#ifndef SOKOL_ASSERT +#include +#define SOKOL_ASSERT(c) assert(c) +#endif +#ifndef _SOKOL_PRIVATE +#define _SOKOL_PRIVATE static +#endif +#ifndef _SOKOL_UNUSED +#define _SOKOL_UNUSED(x) (void)(x) +#endif + +/* the lifted upstream code keys web-only forks on this */ +#define _SAPP_EMSCRIPTEN (1) +#if defined(SOKOL_GLES3) +#define _SAPP_ANY_GL (1) +#endif +/* NOTE: _SAPP_WGPU_HAS_WAIT stays UNDEFINED on web -- the WGPU + adapter/device/swapchain chain is async-callback driven */ + +#define _SAPP_MAX_TITLE_LENGTH (128) +#define _SAPP_FALLBACK_DEFAULT_WINDOW_WIDTH (640) +#define _SAPP_FALLBACK_DEFAULT_WINDOW_HEIGHT (480) +#define _sapp_tc_def(val, def) (((val) == 0) ? (def) : (val)) +#if defined(__cplusplus) +#define _SAPP_STRUCT(TYPE, NAME) TYPE NAME = {} +#define _SAPP_CLEAR_ARC_STRUCT(type, item) { item = type(); } +#else +#define _SAPP_STRUCT(TYPE, NAME) TYPE NAME = {0} +#define _SAPP_CLEAR_ARC_STRUCT(type, item) { _sapp_tc_clear(&item, sizeof(item)); } +#endif + +/* controlled failure instead of sokol_app.h's log-item machinery */ +#define _SAPP_PANIC(code) do { fprintf(stderr, "sokol_app_tc.h: panic: " #code "\n"); abort(); } while (0) +#define _SAPP_ERROR(code) fprintf(stderr, "sokol_app_tc.h: error: " #code "\n") +#define _SAPP_ERROR_MSG(code, msg) fprintf(stderr, "sokol_app_tc.h: error: " #code ": %s\n", msg) +#define _SAPP_WARN_MSG(code, msg) fprintf(stderr, "sokol_app_tc.h: warn: " #code ": %s\n", msg) +#define _SAPP_INFO_MSG(code, msg) fprintf(stderr, "sokol_app_tc.h: info: " #code ": %s\n", msg) + +/* the internal monotonic clock is deliberately unused on web: all timing + comes from the timestamp the browser hands to the frame callback */ +typedef struct { int _dummy; } _sapp_tc_timestamp_t; +_SOKOL_PRIVATE void _sapp_tc_timestamp_init(_sapp_tc_timestamp_t* ts) { _SOKOL_UNUSED(ts); } +_SOKOL_PRIVATE double _sapp_tc_timestamp_now(_sapp_tc_timestamp_t* ts) { _SOKOL_UNUSED(ts); SOKOL_ASSERT(false); return 0.0; } +#ifndef SOKOL_API_IMPL +#define SOKOL_API_IMPL +#endif +#ifndef SOKOL_UNREACHABLE +#define SOKOL_UNREACHABLE SOKOL_ASSERT(false) +#endif + +typedef struct { + _sapp_tc_timestamp_t timestamp; + double dt_min; // config: min clamp value for unfiltered time delta (seconds) + double dt_max; // config: max clamp value for unfiltered time delta (seconds) + double dt_threshold; // config: threshold time delta for 'resetting' filtering (default: 0.004s, 4ms) + double alpha; // config: smoothing constant, lower values smoother, higher values faster response + double last; // last absolute time in seconds + double dt; // unfiltered frame delta in seconds, clamped to dt_min/dt_max + double ema; // intermediate ema-filter result + double smooth_dt; // smoothed frame delta in seconds +} _sapp_tc_timing_t; + +_SOKOL_PRIVATE void _sapp_tc_timing_init(_sapp_tc_timing_t* t) { + _sapp_tc_timestamp_init(&t->timestamp); + t->dt_min = 0.000001; // 1 us + t->dt_max = 0.1; // 100 ms + t->dt_threshold = 0.004; // 4ms + t->alpha = 0.025; + t->dt = 1.0 / 60.0; // a 'likely' non-null value + t->ema = t->dt; + t->smooth_dt = t->dt; +} + +_SOKOL_PRIVATE double _sapp_tc_timing_clamp(_sapp_tc_timing_t* t, double dt) { + SOKOL_ASSERT((t->dt_min > 0.0) && (t->dt_max > 0.0) && (t->dt_max >= t->dt_min)); + if (dt < t->dt_min) { + return t->dt_min; + } else if (dt > t->dt_max) { + return t->dt_max; + } else { + return dt; + } +} + +_SOKOL_PRIVATE void _sapp_tc_timing_delta(_sapp_tc_timing_t* t, double dt) { + // first clamp raw dt against min/max (min avoid division by zero, max + // may avoids glitches and 'death-spirals' during debugging + dt = _sapp_tc_timing_clamp(t, dt); + t->dt = dt; + const double error = fabs(dt - t->smooth_dt); + if (error > t->dt_threshold) { + // 'reset' filter when new delta is outside threshold + t->ema = dt; + t->smooth_dt = dt; + } else { + // simple ema-filter with fixed alpha + t->ema = t->ema + t->alpha * (dt - t->ema); + t->smooth_dt = _sapp_tc_timing_clamp(t, t->ema); + } +} + +_SOKOL_PRIVATE void _sapp_tc_timing_update(_sapp_tc_timing_t* t, double external_now) { + double now; + if (external_now == 0.0) { + now = _sapp_tc_timestamp_now(&t->timestamp); + } else { + now = external_now; + } + if (t->last > 0.0) { + double dt = now - t->last; + _sapp_tc_timing_delta(t, dt); + } + t->last = now; + +} + +_SOKOL_PRIVATE double _sapp_tc_timing_get(_sapp_tc_timing_t* t) { + return t->smooth_dt; +} + +// ███████ ████████ ██████ ██ ██ ██████ ████████ ███████ +// ██ ██ ██ ██ ██ ██ ██ ██ ██ +// ███████ ██ ██████ ██ ██ ██ ██ ███████ +// ██ ██ ██ ██ ██ ██ ██ ██ ██ +// ███████ ██ ██ ██ ██████ ██████ ██ ███████ + +#if defined(SOKOL_WGPU) +typedef struct { + WGPUInstance instance; + WGPUAdapter adapter; + WGPUDevice device; + WGPUSurface surface; + WGPUTextureFormat render_format; + WGPUTexture msaa_tex; + WGPUTextureView msaa_view; + WGPUTexture depth_stencil_tex; + WGPUTextureView depth_stencil_view; + WGPUTextureView swapchain_view; + bool init_done; +} _sapp_tc_wgpu_t; +#endif + +#if defined(_SAPP_EMSCRIPTEN) + +typedef struct { + bool mouse_lock_requested; + uint16_t mouse_buttons; +} _sapp_tc_emsc_t; +#endif // _SAPP_EMSCRIPTEN + +#if defined(_SAPP_ANY_GL) +typedef struct { + uint32_t framebuffer; +} _sapp_tc_gl_t; +#endif + +typedef struct { + bool enabled; + int buf_size; + char* buffer; +} _sapp_tc_clipboard_t; + +typedef struct { + bool enabled; + int max_files; + int max_path_length; + int num_files; + int buf_size; + char* buffer; +} _sapp_tc_drop_t; + +typedef struct { + float x, y; + float dx, dy; + bool shown; + bool locked; + bool pos_valid; + sapp_mouse_cursor current_cursor; +} _sapp_tc_mouse_t; + +/* per-app state -- a web-sized subset of sokol_app.h's _sapp_t with the same + member names, so the lifted implementation code reads unchanged. A large + part of the web backend's real state lives in JavaScript on the Module + object (Module.sapp_emsc_target = the canvas, Module.sokol_dropped_files, + Module.__sapp_tc_custom_cursors), reachable only from the EM_JS blocks. */ +typedef struct { + sapp_desc desc; + bool valid; + bool fullscreen; + bool first_frame; + bool init_called; + bool cleanup_called; + bool quit_requested; + bool quit_ordered; + bool event_consumed; + bool html5_ask_leave_site; + bool onscreen_keyboard_shown; + bool skip_present; /* stored for API parity; the browser auto-presents at rAF (ignored) */ + int window_width; + int window_height; + int framebuffer_width; + int framebuffer_height; + int sample_count; + int swap_interval; /* stored for API parity; the browser paces via rAF (ignored) */ + float dpi_scale; + uint64_t frame_count; + sapp_event event; + _sapp_tc_mouse_t mouse; + _sapp_tc_clipboard_t clipboard; + _sapp_tc_drop_t drop; + _sapp_tc_timing_t timing; + #if defined(SOKOL_WGPU) + _sapp_tc_wgpu_t wgpu; + #endif + _sapp_tc_emsc_t emsc; + #if defined(_SAPP_ANY_GL) + _sapp_tc_gl_t gl; + #endif + char html5_canvas_selector[_SAPP_MAX_TITLE_LENGTH]; + char window_title[_SAPP_MAX_TITLE_LENGTH]; + bool custom_cursor_bound[_SAPP_MOUSECURSOR_NUM]; +} _sapp_tc_t; +static _sapp_tc_t _sapp_tc; + +_SOKOL_PRIVATE void _sapp_tc_clear(void* ptr, size_t size) { + SOKOL_ASSERT(ptr && (size > 0)); + memset(ptr, 0, size); +} + +_SOKOL_PRIVATE void* _sapp_tc_malloc(size_t size) { + SOKOL_ASSERT(size > 0); + void* ptr; + if (_sapp_tc.desc.allocator.alloc_fn) { + ptr = _sapp_tc.desc.allocator.alloc_fn(size, _sapp_tc.desc.allocator.user_data); + } else { + ptr = malloc(size); + } + if (0 == ptr) { + _SAPP_PANIC(MALLOC_FAILED); + } + return ptr; +} + +_SOKOL_PRIVATE void* _sapp_tc_malloc_clear(size_t size) { + void* ptr = _sapp_tc_malloc(size); + _sapp_tc_clear(ptr, size); + return ptr; +} + +_SOKOL_PRIVATE void _sapp_tc_free(void* ptr) { + if (_sapp_tc.desc.allocator.free_fn) { + _sapp_tc.desc.allocator.free_fn(ptr, _sapp_tc.desc.allocator.user_data); + } else { + free(ptr); + } +} + +// ██ ██ ███████ ██ ██████ ███████ ██████ ███████ +// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +// ███████ █████ ██ ██████ █████ ██████ ███████ +// ██ ██ ██ ██ ██ ██ ██ ██ ██ +// ██ ██ ███████ ███████ ██ ███████ ██ ██ ███████ +// +// >>helpers + +// round float to int and at least 1 +_SOKOL_PRIVATE int _sapp_tc_roundf_gzero(float f) { + int val = (int)roundf(f); + if (val <= 0) { + val = 1; + } + return val; +} + +_SOKOL_PRIVATE void _sapp_tc_call_init(void) { + if (_sapp_tc.desc.init_cb) { + _sapp_tc.desc.init_cb(); + } else if (_sapp_tc.desc.init_userdata_cb) { + _sapp_tc.desc.init_userdata_cb(_sapp_tc.desc.user_data); + } + _sapp_tc.init_called = true; +} + +_SOKOL_PRIVATE void _sapp_tc_call_frame(void) { + if (_sapp_tc.init_called && !_sapp_tc.cleanup_called) { + if (_sapp_tc.desc.frame_cb) { + _sapp_tc.desc.frame_cb(); + } else if (_sapp_tc.desc.frame_userdata_cb) { + _sapp_tc.desc.frame_userdata_cb(_sapp_tc.desc.user_data); + } + } +} + +_SOKOL_PRIVATE void _sapp_tc_call_cleanup(void) { + if (!_sapp_tc.cleanup_called) { + if (_sapp_tc.desc.cleanup_cb) { + _sapp_tc.desc.cleanup_cb(); + } else if (_sapp_tc.desc.cleanup_userdata_cb) { + _sapp_tc.desc.cleanup_userdata_cb(_sapp_tc.desc.user_data); + } + _sapp_tc.cleanup_called = true; + } +} + +_SOKOL_PRIVATE bool _sapp_tc_call_event(const sapp_event* e) { + if (!_sapp_tc.cleanup_called) { + if (_sapp_tc.desc.event_cb) { + _sapp_tc.desc.event_cb(e); + } else if (_sapp_tc.desc.event_userdata_cb) { + _sapp_tc.desc.event_userdata_cb(e, _sapp_tc.desc.user_data); + } + } + if (_sapp_tc.event_consumed) { + _sapp_tc.event_consumed = false; + return true; + } else { + return false; + } +} + + +_SOKOL_PRIVATE char* _sapp_tc_dropped_file_path_ptr(int index) { + SOKOL_ASSERT(_sapp_tc.drop.buffer); + SOKOL_ASSERT((index >= 0) && (index <= _sapp_tc.drop.max_files)); + int offset = index * _sapp_tc.drop.max_path_length; + SOKOL_ASSERT(offset < _sapp_tc.drop.buf_size); + return &_sapp_tc.drop.buffer[offset]; +} + +/* Copy a string (either zero-terminated or with explicit length) + into a fixed size buffer with guaranteed zero-termination. + + Return false if the string didn't fit into the buffer and had to be clamped. + + FIXME: Currently UTF-8 strings might become invalid if the string + is clamped, because the last zero-byte might be written into + the middle of a multi-byte sequence. +*/ +_SOKOL_PRIVATE bool _sapp_tc_strcpy_range(const char* src, size_t src_len, char* dst, size_t dst_buf_len) { + SOKOL_ASSERT(src && dst && (dst_buf_len > 0)); + if (0 == src_len) { + src_len = dst_buf_len; + } + char* const end = &(dst[dst_buf_len-1]); + char c = 0; + for (size_t i = 0; i < dst_buf_len; i++) { + c = *src; + if (i >= src_len) { + c = 0; + } + if (c != 0) { + src++; + } + *dst++ = c; + } + // truncated? + if (c != 0) { + *end = 0; + return false; + } else { + return true; + } +} + +_SOKOL_PRIVATE bool _sapp_tc_strcpy(const char* src, char* dst, size_t dst_buf_len) { + return _sapp_tc_strcpy_range(src, 0, dst, dst_buf_len); +} + +_SOKOL_PRIVATE sapp_desc _sapp_tc_desc_defaults(const sapp_desc* desc) { + SOKOL_ASSERT((desc->allocator.alloc_fn && desc->allocator.free_fn) || (!desc->allocator.alloc_fn && !desc->allocator.free_fn)); + sapp_desc res = *desc; + res.sample_count = _sapp_tc_def(res.sample_count, 1); + res.swap_interval = _sapp_tc_def(res.swap_interval, 1); + if (0 == res.gl.major_version) { + #if defined(SOKOL_GLCORE) + res.gl.major_version = 4; + #if defined(_SAPP_APPLE) + res.gl.minor_version = 1; + #else + res.gl.minor_version = 3; + #endif + #elif defined(SOKOL_GLES3) + res.gl.major_version = 3; + #if defined(_SAPP_ANDROID) || defined(_SAPP_LINUX) + res.gl.minor_version = 1; + #else + res.gl.minor_version = 0; + #endif + #endif + } + res.html5.canvas_selector = _sapp_tc_def(res.html5.canvas_selector, "#canvas"); + res.clipboard_size = _sapp_tc_def(res.clipboard_size, 8192); + res.max_dropped_files = _sapp_tc_def(res.max_dropped_files, 1); + res.max_dropped_file_path_length = _sapp_tc_def(res.max_dropped_file_path_length, 2048); + res.window_title = _sapp_tc_def(res.window_title, "sokol"); + return res; +} + +_SOKOL_PRIVATE void _sapp_tc_init_state(const sapp_desc* desc) { + SOKOL_ASSERT(desc); + SOKOL_ASSERT(desc->width >= 0); + SOKOL_ASSERT(desc->height >= 0); + SOKOL_ASSERT(desc->sample_count >= 0); + SOKOL_ASSERT(desc->swap_interval >= 0); + SOKOL_ASSERT(desc->clipboard_size >= 0); + SOKOL_ASSERT(desc->max_dropped_files >= 0); + SOKOL_ASSERT(desc->max_dropped_file_path_length >= 0); + _SAPP_CLEAR_ARC_STRUCT(_sapp_tc_t, _sapp_tc); + _sapp_tc.desc = _sapp_tc_desc_defaults(desc); + _sapp_tc.first_frame = true; + // NOTE: _sapp_tc.desc.width/height may be 0! Platform backends need to deal with this + _sapp_tc.window_width = _sapp_tc.desc.width; + _sapp_tc.window_height = _sapp_tc.desc.height; + _sapp_tc.framebuffer_width = _sapp_tc.window_width; + _sapp_tc.framebuffer_height = _sapp_tc.window_height; + _sapp_tc.sample_count = _sapp_tc.desc.sample_count; + _sapp_tc.swap_interval = _sapp_tc.desc.swap_interval; + _sapp_tc_strcpy(_sapp_tc.desc.html5.canvas_selector, _sapp_tc.html5_canvas_selector, sizeof(_sapp_tc.html5_canvas_selector)); + _sapp_tc.desc.html5.canvas_selector = _sapp_tc.html5_canvas_selector; + _sapp_tc.html5_ask_leave_site = _sapp_tc.desc.html5.ask_leave_site; + _sapp_tc.clipboard.enabled = _sapp_tc.desc.enable_clipboard; + if (_sapp_tc.clipboard.enabled) { + _sapp_tc.clipboard.buf_size = _sapp_tc.desc.clipboard_size; + _sapp_tc.clipboard.buffer = (char*) _sapp_tc_malloc_clear((size_t)_sapp_tc.clipboard.buf_size); + } + _sapp_tc.drop.enabled = _sapp_tc.desc.enable_dragndrop; + if (_sapp_tc.drop.enabled) { + _sapp_tc.drop.max_files = _sapp_tc.desc.max_dropped_files; + _sapp_tc.drop.max_path_length = _sapp_tc.desc.max_dropped_file_path_length; + _sapp_tc.drop.buf_size = _sapp_tc.drop.max_files * _sapp_tc.drop.max_path_length; + _sapp_tc.drop.buffer = (char*) _sapp_tc_malloc_clear((size_t)_sapp_tc.drop.buf_size); + } + _sapp_tc_strcpy(_sapp_tc.desc.window_title, _sapp_tc.window_title, sizeof(_sapp_tc.window_title)); + _sapp_tc.desc.window_title = _sapp_tc.window_title; + _sapp_tc.dpi_scale = 1.0f; + _sapp_tc.fullscreen = _sapp_tc.desc.fullscreen; + _sapp_tc.mouse.shown = true; + _sapp_tc_timing_init(&_sapp_tc.timing); +} + +_SOKOL_PRIVATE void _sapp_tc_discard_state(void) { + if (_sapp_tc.clipboard.enabled) { + SOKOL_ASSERT(_sapp_tc.clipboard.buffer); + _sapp_tc_free((void*)_sapp_tc.clipboard.buffer); + } + if (_sapp_tc.drop.enabled) { + SOKOL_ASSERT(_sapp_tc.drop.buffer); + _sapp_tc_free((void*)_sapp_tc.drop.buffer); + } + for (int i = 0; i < _SAPP_MOUSECURSOR_NUM; i++) { + sapp_unbind_mouse_cursor_image((sapp_mouse_cursor) i); + } + _SAPP_CLEAR_ARC_STRUCT(_sapp_tc_t, _sapp_tc); +} + +_SOKOL_PRIVATE void _sapp_tc_init_event(sapp_event_type type) { + _sapp_tc_clear(&_sapp_tc.event, sizeof(_sapp_tc.event)); + _sapp_tc.event.type = type; + _sapp_tc.event.frame_count = _sapp_tc.frame_count; + _sapp_tc.event.mouse_button = SAPP_MOUSEBUTTON_INVALID; + _sapp_tc.event.window_width = _sapp_tc.window_width; + _sapp_tc.event.window_height = _sapp_tc.window_height; + _sapp_tc.event.framebuffer_width = _sapp_tc.framebuffer_width; + _sapp_tc.event.framebuffer_height = _sapp_tc.framebuffer_height; + _sapp_tc.event.mouse_x = _sapp_tc.mouse.x; + _sapp_tc.event.mouse_y = _sapp_tc.mouse.y; + _sapp_tc.event.mouse_dx = _sapp_tc.mouse.dx; + _sapp_tc.event.mouse_dy = _sapp_tc.mouse.dy; +} + +_SOKOL_PRIVATE bool _sapp_tc_events_enabled(void) { + /* only send events when an event callback is set, and the init function was called */ + return (_sapp_tc.desc.event_cb || _sapp_tc.desc.event_userdata_cb) && _sapp_tc.init_called; +} + +_SOKOL_PRIVATE void _sapp_tc_clear_drop_buffer(void) { + if (_sapp_tc.drop.enabled) { + SOKOL_ASSERT(_sapp_tc.drop.buffer); + _sapp_tc_clear(_sapp_tc.drop.buffer, (size_t)_sapp_tc.drop.buf_size); + } +} + +_SOKOL_PRIVATE void _sapp_tc_frame(void) { + if (_sapp_tc.first_frame) { + _sapp_tc.first_frame = false; + _sapp_tc_call_init(); + } + _sapp_tc_call_frame(); + _sapp_tc.frame_count++; +} + +_SOKOL_PRIVATE bool _sapp_tc_image_validate(const sapp_image_desc* desc) { + SOKOL_ASSERT(desc->width > 0); + SOKOL_ASSERT(desc->height > 0); + SOKOL_ASSERT(desc->pixels.ptr != 0); + SOKOL_ASSERT(desc->pixels.size > 0); + const size_t wh_size = (size_t)(desc->width * desc->height) * sizeof(uint32_t); + if (wh_size != desc->pixels.size) { + _SAPP_ERROR(IMAGE_DATA_SIZE_MISMATCH); + return false; + } + return true; +} + +_SOKOL_PRIVATE int _sapp_tc_image_bestmatch(const sapp_image_desc image_descs[], int num_images, int width, int height) { + int least_diff = 0x7FFFFFFF; + int least_index = 0; + for (int i = 0; i < num_images; i++) { + int diff = (image_descs[i].width * image_descs[i].height) - (width * height); + if (diff < 0) { + diff = -diff; + } + if (diff < least_diff) { + least_diff = diff; + least_index = i; + } + } + return least_index; +} + +_SOKOL_PRIVATE int _sapp_tc_icon_num_images(const sapp_icon_desc* desc) { + int index = 0; + for (; index < SAPP_MAX_ICONIMAGES; index++) { + if (0 == desc->images[index].pixels.ptr) { + break; + } + } + return index; +} + +_SOKOL_PRIVATE bool _sapp_tc_validate_icon_desc(const sapp_icon_desc* desc, int num_images) { + SOKOL_ASSERT(num_images <= SAPP_MAX_ICONIMAGES); + for (int i = 0; i < num_images; i++) { + const sapp_image_desc* img_desc = &desc->images[i]; + if (!_sapp_tc_image_validate(img_desc)) { + return false; + } + } + return true; +} + +// >>wgpu +#if defined(SOKOL_WGPU) + +_SOKOL_PRIVATE WGPUStringView _sapp_tc_wgpu_stringview(const char* str) { + WGPUStringView res; + if (str) { + res.data = str; + res.length = strlen(str); + } else { + res.data = 0; + res.length = 0; + } + return res; +} + +_SOKOL_PRIVATE WGPUCallbackMode _sapp_tc_wgpu_callbackmode(void) { + #if defined(_SAPP_WGPU_HAS_WAIT) + return WGPUCallbackMode_WaitAnyOnly; + #else + return WGPUCallbackMode_AllowProcessEvents; + #endif +} + +_SOKOL_PRIVATE void _sapp_tc_wgpu_await(WGPUFuture future) { + #if defined(_SAPP_WGPU_HAS_WAIT) + SOKOL_ASSERT(_sapp_tc.wgpu.instance); + _SAPP_STRUCT(WGPUFutureWaitInfo, wait_info); + wait_info.future = future; + WGPUWaitStatus res = wgpuInstanceWaitAny(_sapp_tc.wgpu.instance, 1, &wait_info, UINT64_MAX); + SOKOL_ASSERT(res == WGPUWaitStatus_Success); _SOKOL_UNUSED(res); + #else + // this code path should never be called + _SOKOL_UNUSED(future); + SOKOL_ASSERT(false); + #endif +} + +_SOKOL_PRIVATE WGPUTextureFormat _sapp_tc_wgpu_pick_render_format(size_t count, const WGPUTextureFormat* formats) { + // NOTE: only accept non-SRGB formats until sokol_app.h gets proper SRGB support + SOKOL_ASSERT((count > 0) && formats); + for (size_t i = 0; i < count; i++) { + const WGPUTextureFormat fmt = formats[i]; + switch (fmt) { + case WGPUTextureFormat_RGBA8Unorm: + case WGPUTextureFormat_BGRA8Unorm: + return fmt; + default: break; + } + } + // FIXME: fallback might still return an SRGB format + return formats[0]; +} + +_SOKOL_PRIVATE void _sapp_tc_wgpu_create_swapchain(bool called_from_resize) { + SOKOL_ASSERT(_sapp_tc.wgpu.instance); + SOKOL_ASSERT(_sapp_tc.wgpu.device); + SOKOL_ASSERT(0 == _sapp_tc.wgpu.msaa_tex); + SOKOL_ASSERT(0 == _sapp_tc.wgpu.msaa_view); + SOKOL_ASSERT(0 == _sapp_tc.wgpu.depth_stencil_tex); + SOKOL_ASSERT(0 == _sapp_tc.wgpu.depth_stencil_view); + + if (!called_from_resize) { + SOKOL_ASSERT(0 == _sapp_tc.wgpu.surface); + _SAPP_STRUCT(WGPUSurfaceDescriptor, surf_desc); + #if defined (_SAPP_EMSCRIPTEN) + _SAPP_STRUCT(WGPUEmscriptenSurfaceSourceCanvasHTMLSelector, html_canvas_desc); + html_canvas_desc.chain.sType = WGPUSType_EmscriptenSurfaceSourceCanvasHTMLSelector; + html_canvas_desc.selector = _sapp_tc_wgpu_stringview(_sapp_tc.html5_canvas_selector); + surf_desc.nextInChain = &html_canvas_desc.chain; + #elif defined(_SAPP_MACOS) + _SAPP_STRUCT(WGPUSurfaceSourceMetalLayer, from_metal_layer); + from_metal_layer.chain.sType = WGPUSType_SurfaceSourceMetalLayer; + from_metal_layer.layer = _sapp_tc.macos.view.layer; + surf_desc.nextInChain = &from_metal_layer.chain; + #elif defined(_SAPP_WIN32) + _SAPP_STRUCT(WGPUSurfaceSourceWindowsHWND, from_hwnd); + from_hwnd.chain.sType = WGPUSType_SurfaceSourceWindowsHWND; + from_hwnd.hinstance = GetModuleHandleW(NULL); + from_hwnd.hwnd = _sapp_tc.win32.hwnd; + surf_desc.nextInChain = &from_hwnd.chain; + #elif defined(_SAPP_LINUX) + _SAPP_STRUCT(WGPUSurfaceSourceXlibWindow, from_xlib); + from_xlib.chain.sType = WGPUSType_SurfaceSourceXlibWindow; + from_xlib.display = _sapp_tc.x11.display; + from_xlib.window = _sapp_tc.x11.window; + surf_desc.nextInChain = &from_xlib.chain; + #else + #error "sokol_app.h: unsupported WebGPU platform" + #endif + _sapp_tc.wgpu.surface = wgpuInstanceCreateSurface(_sapp_tc.wgpu.instance, &surf_desc); + if (0 == _sapp_tc.wgpu.surface) { + _SAPP_PANIC(WGPU_SWAPCHAIN_CREATE_SURFACE_FAILED); + } + _SAPP_STRUCT(WGPUSurfaceCapabilities, surf_caps); + WGPUStatus caps_status = wgpuSurfaceGetCapabilities(_sapp_tc.wgpu.surface, _sapp_tc.wgpu.adapter, &surf_caps); + if (caps_status != WGPUStatus_Success) { + _SAPP_PANIC(WGPU_SWAPCHAIN_SURFACE_GET_CAPABILITIES_FAILED); + } + _sapp_tc.wgpu.render_format = _sapp_tc_wgpu_pick_render_format(surf_caps.formatCount, surf_caps.formats); + } + + SOKOL_ASSERT(_sapp_tc.wgpu.surface); + _SAPP_STRUCT(WGPUSurfaceConfiguration, surf_conf); + surf_conf.device = _sapp_tc.wgpu.device; + surf_conf.format = _sapp_tc.wgpu.render_format; + surf_conf.usage = WGPUTextureUsage_RenderAttachment; + surf_conf.width = (uint32_t)_sapp_tc.framebuffer_width; + surf_conf.height = (uint32_t)_sapp_tc.framebuffer_height; + surf_conf.alphaMode = WGPUCompositeAlphaMode_Opaque; + #if defined(_SAPP_EMSCRIPTEN) + // FIXME: make this further configurable? + if (_sapp_tc.desc.html5.premultiplied_alpha) { + surf_conf.alphaMode = WGPUCompositeAlphaMode_Premultiplied; + } + #endif + surf_conf.presentMode = WGPUPresentMode_Fifo; + wgpuSurfaceConfigure(_sapp_tc.wgpu.surface, &surf_conf); + + _SAPP_STRUCT(WGPUTextureDescriptor, ds_desc); + ds_desc.usage = WGPUTextureUsage_RenderAttachment; + ds_desc.dimension = WGPUTextureDimension_2D; + ds_desc.size.width = (uint32_t)_sapp_tc.framebuffer_width; + ds_desc.size.height = (uint32_t)_sapp_tc.framebuffer_height; + ds_desc.size.depthOrArrayLayers = 1; + ds_desc.format = WGPUTextureFormat_Depth32FloatStencil8; + ds_desc.mipLevelCount = 1; + ds_desc.sampleCount = (uint32_t)_sapp_tc.sample_count; + _sapp_tc.wgpu.depth_stencil_tex = wgpuDeviceCreateTexture(_sapp_tc.wgpu.device, &ds_desc); + if (0 == _sapp_tc.wgpu.depth_stencil_tex) { + _SAPP_PANIC(WGPU_SWAPCHAIN_CREATE_DEPTH_STENCIL_TEXTURE_FAILED); + } + _sapp_tc.wgpu.depth_stencil_view = wgpuTextureCreateView(_sapp_tc.wgpu.depth_stencil_tex, 0); + if (0 == _sapp_tc.wgpu.depth_stencil_view) { + _SAPP_PANIC(WGPU_SWAPCHAIN_CREATE_DEPTH_STENCIL_VIEW_FAILED); + } + + if (_sapp_tc.sample_count > 1) { + _SAPP_STRUCT(WGPUTextureDescriptor, msaa_desc); + msaa_desc.usage = WGPUTextureUsage_RenderAttachment; + msaa_desc.dimension = WGPUTextureDimension_2D; + msaa_desc.size.width = (uint32_t)_sapp_tc.framebuffer_width; + msaa_desc.size.height = (uint32_t)_sapp_tc.framebuffer_height; + msaa_desc.size.depthOrArrayLayers = 1; + msaa_desc.format = _sapp_tc.wgpu.render_format; + msaa_desc.mipLevelCount = 1; + msaa_desc.sampleCount = (uint32_t)_sapp_tc.sample_count; + _sapp_tc.wgpu.msaa_tex = wgpuDeviceCreateTexture(_sapp_tc.wgpu.device, &msaa_desc); + if (0 == _sapp_tc.wgpu.msaa_tex) { + _SAPP_PANIC(WGPU_SWAPCHAIN_CREATE_MSAA_TEXTURE_FAILED); + } + _sapp_tc.wgpu.msaa_view = wgpuTextureCreateView(_sapp_tc.wgpu.msaa_tex, 0); + if (0 == _sapp_tc.wgpu.msaa_view) { + _SAPP_PANIC(WGPU_SWAPCHAIN_CREATE_MSAA_VIEW_FAILED); + } + } +} + +_SOKOL_PRIVATE void _sapp_tc_wgpu_discard_swapchain(bool called_from_resize) { + if (_sapp_tc.wgpu.msaa_view) { + wgpuTextureViewRelease(_sapp_tc.wgpu.msaa_view); + _sapp_tc.wgpu.msaa_view = 0; + } + if (_sapp_tc.wgpu.msaa_tex) { + wgpuTextureRelease(_sapp_tc.wgpu.msaa_tex); + _sapp_tc.wgpu.msaa_tex = 0; + } + if (_sapp_tc.wgpu.depth_stencil_view) { + wgpuTextureViewRelease(_sapp_tc.wgpu.depth_stencil_view); + _sapp_tc.wgpu.depth_stencil_view = 0; + } + if (_sapp_tc.wgpu.depth_stencil_tex) { + wgpuTextureRelease(_sapp_tc.wgpu.depth_stencil_tex); + _sapp_tc.wgpu.depth_stencil_tex = 0; + } + if (!called_from_resize) { + if (_sapp_tc.wgpu.surface) { + wgpuSurfaceRelease(_sapp_tc.wgpu.surface); + _sapp_tc.wgpu.surface = 0; + } + } +} + +_SOKOL_PRIVATE void _sapp_tc_wgpu_swapchain_next(void) { + SOKOL_ASSERT(0 == _sapp_tc.wgpu.swapchain_view); + _SAPP_STRUCT(WGPUSurfaceTexture, surf_tex); + wgpuSurfaceGetCurrentTexture(_sapp_tc.wgpu.surface, &surf_tex); + switch (surf_tex.status) { + case WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal: + case WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal: + // all ok + break; + case WGPUSurfaceGetCurrentTextureStatus_Timeout: + case WGPUSurfaceGetCurrentTextureStatus_Outdated: + case WGPUSurfaceGetCurrentTextureStatus_Lost: + if (surf_tex.texture) { + wgpuTextureRelease(surf_tex.texture); + } + _sapp_tc_wgpu_discard_swapchain(false); + _sapp_tc_wgpu_create_swapchain(false); + // FIXME: currently this will assert in the caller + return; + case WGPUSurfaceGetCurrentTextureStatus_Error: + default: + _SAPP_PANIC(WGPU_SWAPCHAIN_GETCURRENTTEXTURE_FAILED); + break; + } + _sapp_tc.wgpu.swapchain_view = wgpuTextureCreateView(surf_tex.texture, 0); + SOKOL_ASSERT(_sapp_tc.wgpu.swapchain_view); +} + +_SOKOL_PRIVATE void _sapp_tc_wgpu_swapchain_size_changed(void) { + if (_sapp_tc.wgpu.surface) { + _sapp_tc_wgpu_discard_swapchain(true); + _sapp_tc_wgpu_create_swapchain(true); + } +} + +_SOKOL_PRIVATE void _sapp_tc_wgpu_device_lost_cb(const WGPUDevice* dev, WGPUDeviceLostReason reason, WGPUStringView msg, void* ud1, void* ud2) { + _SOKOL_UNUSED(dev); _SOKOL_UNUSED(reason); _SOKOL_UNUSED(ud1); _SOKOL_UNUSED(ud2); + // NOTE: on wgpuInstanceRelease(), the device lost callback is always called with + // WGPUDeviceLostReason_CallbackCancelled (even though no device should exist at that point) + if (reason != WGPUDeviceLostReason_CallbackCancelled) { + SOKOL_ASSERT(msg.data && (msg.length > 0)); + char buf[1024]; + _sapp_tc_strcpy_range(msg.data, msg.length, buf, sizeof(buf)); + _SAPP_ERROR_MSG(WGPU_DEVICE_LOST, buf); + } +} + +// NOTE: emdawnwebgpu doesn't seem to have a device logging callback +#if !defined(_SAPP_EMSCRIPTEN) +_SOKOL_PRIVATE void _sapp_tc_wgpu_device_logging_cb(WGPULoggingType log_type, WGPUStringView msg, void* ud1, void* ud2) { + _SOKOL_UNUSED(log_type); _SOKOL_UNUSED(ud1); _SOKOL_UNUSED(ud2); + SOKOL_ASSERT(msg.data && (msg.length > 0)); + char buf[1024]; + _sapp_tc_strcpy_range(msg.data, msg.length, buf, sizeof(buf)); + switch (log_type) { + case WGPULoggingType_Warning: + _SAPP_WARN_MSG(WGPU_DEVICE_LOG, buf); + break; + case WGPULoggingType_Error: + _SAPP_ERROR_MSG(WGPU_DEVICE_LOG, buf); + break; + default: + _SAPP_INFO_MSG(WGPU_DEVICE_LOG, buf); + break; + } +} +#endif + +_SOKOL_PRIVATE void _sapp_tc_wgpu_uncaptured_error_cb(const WGPUDevice* dev, WGPUErrorType err_type, WGPUStringView msg, void* ud1, void* ud2) { + _SOKOL_UNUSED(dev); _SOKOL_UNUSED(ud1); _SOKOL_UNUSED(ud2); + if (err_type != WGPUErrorType_NoError) { + SOKOL_ASSERT(msg.data && (msg.length > 0)); + char buf[1024]; + _sapp_tc_strcpy_range(msg.data, msg.length, buf, sizeof(buf)); + _SAPP_ERROR_MSG(WGPU_DEVICE_UNCAPTURED_ERROR, buf); + } +} + +_SOKOL_PRIVATE void _sapp_tc_wgpu_request_device_cb(WGPURequestDeviceStatus status, WGPUDevice device, WGPUStringView msg, void* userdata1, void* userdata2) { + _SOKOL_UNUSED(msg); + _SOKOL_UNUSED(userdata1); + _SOKOL_UNUSED(userdata2); + SOKOL_ASSERT(!_sapp_tc.wgpu.init_done); + if (status != WGPURequestDeviceStatus_Success) { + if (status == WGPURequestDeviceStatus_Error) { + _SAPP_PANIC(WGPU_REQUEST_DEVICE_STATUS_ERROR); + } else { + _SAPP_PANIC(WGPU_REQUEST_DEVICE_STATUS_UNKNOWN); + } + } + SOKOL_ASSERT(device); + _sapp_tc.wgpu.device = device; + #if !defined(_SAPP_EMSCRIPTEN) + _SAPP_STRUCT(WGPULoggingCallbackInfo, cb_info); + cb_info.callback = _sapp_tc_wgpu_device_logging_cb; + wgpuDeviceSetLoggingCallback(_sapp_tc.wgpu.device, cb_info); + #endif + _sapp_tc_wgpu_create_swapchain(false); + _sapp_tc.wgpu.init_done = true; +} + +_SOKOL_PRIVATE void _sapp_tc_wgpu_create_device_and_swapchain(void) { + SOKOL_ASSERT(_sapp_tc.wgpu.adapter); + size_t cur_feature_index = 1; + #define _SAPP_WGPU_MAX_REQUESTED_FEATURES (16) + WGPUFeatureName requiredFeatures[_SAPP_WGPU_MAX_REQUESTED_FEATURES] = { + WGPUFeatureName_Depth32FloatStencil8, + }; + // check for optional features we're interested in + if (wgpuAdapterHasFeature(_sapp_tc.wgpu.adapter, WGPUFeatureName_TextureCompressionBC)) { + SOKOL_ASSERT(cur_feature_index < _SAPP_WGPU_MAX_REQUESTED_FEATURES); + requiredFeatures[cur_feature_index++] = WGPUFeatureName_TextureCompressionBC; + } + if (wgpuAdapterHasFeature(_sapp_tc.wgpu.adapter, WGPUFeatureName_TextureCompressionETC2)) { + SOKOL_ASSERT(cur_feature_index < _SAPP_WGPU_MAX_REQUESTED_FEATURES); + requiredFeatures[cur_feature_index++] = WGPUFeatureName_TextureCompressionETC2; + } + if (wgpuAdapterHasFeature(_sapp_tc.wgpu.adapter, WGPUFeatureName_TextureCompressionASTC)) { + SOKOL_ASSERT(cur_feature_index < _SAPP_WGPU_MAX_REQUESTED_FEATURES); + requiredFeatures[cur_feature_index++] = WGPUFeatureName_TextureCompressionASTC; + } + if (wgpuAdapterHasFeature(_sapp_tc.wgpu.adapter, WGPUFeatureName_DualSourceBlending)) { + SOKOL_ASSERT(cur_feature_index < _SAPP_WGPU_MAX_REQUESTED_FEATURES); + requiredFeatures[cur_feature_index++] = WGPUFeatureName_DualSourceBlending; + } + if (wgpuAdapterHasFeature(_sapp_tc.wgpu.adapter, WGPUFeatureName_ShaderF16)) { + SOKOL_ASSERT(cur_feature_index < _SAPP_WGPU_MAX_REQUESTED_FEATURES); + requiredFeatures[cur_feature_index++] = WGPUFeatureName_ShaderF16; + } + if (wgpuAdapterHasFeature(_sapp_tc.wgpu.adapter, WGPUFeatureName_Float32Filterable)) { + SOKOL_ASSERT(cur_feature_index < _SAPP_WGPU_MAX_REQUESTED_FEATURES); + requiredFeatures[cur_feature_index++] = WGPUFeatureName_Float32Filterable; + } + if (wgpuAdapterHasFeature(_sapp_tc.wgpu.adapter, WGPUFeatureName_Float32Blendable)) { + SOKOL_ASSERT(cur_feature_index < _SAPP_WGPU_MAX_REQUESTED_FEATURES); + requiredFeatures[cur_feature_index++] = WGPUFeatureName_Float32Blendable; + } + if (wgpuAdapterHasFeature(_sapp_tc.wgpu.adapter, WGPUFeatureName_TextureFormatsTier2)) { + SOKOL_ASSERT(cur_feature_index < _SAPP_WGPU_MAX_REQUESTED_FEATURES); + requiredFeatures[cur_feature_index++] = WGPUFeatureName_TextureFormatsTier2; + } + #undef _SAPP_WGPU_MAX_REQUESTED_FEATURES + + WGPULimits adapterLimits = WGPU_LIMITS_INIT; + wgpuAdapterGetLimits(_sapp_tc.wgpu.adapter, &adapterLimits); + + WGPULimits requiredLimits = WGPU_LIMITS_INIT; + requiredLimits.maxColorAttachments = adapterLimits.maxColorAttachments; + requiredLimits.maxSampledTexturesPerShaderStage = adapterLimits.maxSampledTexturesPerShaderStage; + requiredLimits.maxStorageBuffersPerShaderStage = adapterLimits.maxStorageBuffersPerShaderStage; + requiredLimits.maxStorageTexturesPerShaderStage = adapterLimits.maxStorageTexturesPerShaderStage; + + _SAPP_STRUCT(WGPURequestDeviceCallbackInfo, cb_info); + cb_info.mode = _sapp_tc_wgpu_callbackmode(); + cb_info.callback = _sapp_tc_wgpu_request_device_cb; + + _SAPP_STRUCT(WGPUDeviceDescriptor, dev_desc); + dev_desc.requiredFeatureCount = cur_feature_index; + dev_desc.requiredFeatures = requiredFeatures; + dev_desc.requiredLimits = &requiredLimits; + dev_desc.deviceLostCallbackInfo.mode = WGPUCallbackMode_AllowProcessEvents; + dev_desc.deviceLostCallbackInfo.callback = _sapp_tc_wgpu_device_lost_cb; + dev_desc.uncapturedErrorCallbackInfo.callback = _sapp_tc_wgpu_uncaptured_error_cb; + WGPUFuture future = wgpuAdapterRequestDevice(_sapp_tc.wgpu.adapter, &dev_desc, cb_info); + #if defined(_SAPP_WGPU_HAS_WAIT) + _sapp_tc_wgpu_await(future); + #else + _SOKOL_UNUSED(future); + #endif +} + +_SOKOL_PRIVATE void _sapp_tc_wgpu_request_adapter_cb(WGPURequestAdapterStatus status, WGPUAdapter adapter, WGPUStringView msg, void* userdata1, void* userdata2) { + _SOKOL_UNUSED(msg); + _SOKOL_UNUSED(userdata1); + _SOKOL_UNUSED(userdata2); + if (status != WGPURequestAdapterStatus_Success) { + switch (status) { + case WGPURequestAdapterStatus_Unavailable: _SAPP_PANIC(WGPU_REQUEST_ADAPTER_STATUS_UNAVAILABLE); break; + case WGPURequestAdapterStatus_Error: _SAPP_PANIC(WGPU_REQUEST_ADAPTER_STATUS_ERROR); break; + default: _SAPP_PANIC(WGPU_REQUEST_ADAPTER_STATUS_UNKNOWN); break; + } + } + SOKOL_ASSERT(adapter); + _sapp_tc.wgpu.adapter = adapter; + #if !defined(_SAPP_WGPU_HAS_WAIT) + // chain device creation + _sapp_tc_wgpu_create_device_and_swapchain(); + #endif +} + +_SOKOL_PRIVATE void _sapp_tc_wgpu_create_adapter(void) { + SOKOL_ASSERT(_sapp_tc.wgpu.instance); + // FIXME: power preference? + _SAPP_STRUCT(WGPURequestAdapterCallbackInfo, cb_info); + cb_info.mode = _sapp_tc_wgpu_callbackmode(); + cb_info.callback = _sapp_tc_wgpu_request_adapter_cb; + WGPUFuture future = wgpuInstanceRequestAdapter(_sapp_tc.wgpu.instance, 0, cb_info); + #if defined(_SAPP_WGPU_HAS_WAIT) + _sapp_tc_wgpu_await(future); + #else + _SOKOL_UNUSED(future); + #endif +} + +_SOKOL_PRIVATE void _sapp_tc_wgpu_init(void) { + SOKOL_ASSERT(0 == _sapp_tc.wgpu.instance); + SOKOL_ASSERT(!_sapp_tc.wgpu.init_done); + + _SAPP_STRUCT(WGPUInstanceDescriptor, desc); + #if defined(_SAPP_WGPU_HAS_WAIT) + WGPUInstanceFeatureName inst_features[1] = { + WGPUInstanceFeatureName_TimedWaitAny, + }; + desc.requiredFeatureCount = 1; + desc.requiredFeatures = inst_features; + #endif + _sapp_tc.wgpu.instance = wgpuCreateInstance(&desc); + if (0 == _sapp_tc.wgpu.instance) { + _SAPP_PANIC(WGPU_CREATE_INSTANCE_FAILED); + } + // NOTE: on Emscripten, device and swapchain creation are chained in the callacks + _sapp_tc_wgpu_create_adapter(); + #if defined(_SAPP_WGPU_HAS_WAIT) + _sapp_tc_wgpu_create_device_and_swapchain(); + SOKOL_ASSERT(_sapp_tc.wgpu.init_done); + #endif +} + +_SOKOL_PRIVATE void _sapp_tc_wgpu_discard(void) { + _sapp_tc_wgpu_discard_swapchain(false); + if (_sapp_tc.wgpu.device) { + wgpuDeviceRelease(_sapp_tc.wgpu.device); + _sapp_tc.wgpu.device = 0; + } + if (_sapp_tc.wgpu.adapter) { + wgpuAdapterRelease(_sapp_tc.wgpu.adapter); + _sapp_tc.wgpu.adapter = 0; + } + if (_sapp_tc.wgpu.instance) { + wgpuInstanceRelease(_sapp_tc.wgpu.instance); + _sapp_tc.wgpu.instance = 0; + } +} + +_SOKOL_PRIVATE void _sapp_tc_wgpu_frame(void) { + wgpuInstanceProcessEvents(_sapp_tc.wgpu.instance); + if (_sapp_tc.wgpu.init_done) { + _sapp_tc_frame(); + if (_sapp_tc.wgpu.swapchain_view) { + wgpuTextureViewRelease(_sapp_tc.wgpu.swapchain_view); + _sapp_tc.wgpu.swapchain_view = 0; + } + #if !defined(_SAPP_EMSCRIPTEN) + // Modified by tettou771 for TrussC: skip present support + if (!_sapp_tc.skip_present) { + wgpuSurfacePresent(_sapp_tc.wgpu.surface); + } else { + _sapp_tc.skip_present = false; + } + #endif + } +} +#endif // SOKOL_WGPU + +#if defined(_SAPP_EMSCRIPTEN) + +#if defined(EM_JS_DEPS) +EM_JS_DEPS(sokol_app_tc, "$withStackSave,$stringToUTF8OnStack,$findCanvasEventTarget") +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*_sapp_tc_html5_fetch_callback) (const sapp_html5_fetch_response*); + +EMSCRIPTEN_KEEPALIVE void _sapp_tc_emsc_onpaste(const char* str) { + if (_sapp_tc.clipboard.enabled) { + _sapp_tc_strcpy(str, _sapp_tc.clipboard.buffer, (size_t)_sapp_tc.clipboard.buf_size); + if (_sapp_tc_events_enabled()) { + _sapp_tc_init_event(SAPP_EVENTTYPE_CLIPBOARD_PASTED); + _sapp_tc_call_event(&_sapp_tc.event); + } + } +} + +/* https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload */ +EMSCRIPTEN_KEEPALIVE int _sapp_tc_html5_get_ask_leave_site(void) { + return _sapp_tc.html5_ask_leave_site ? 1 : 0; +} + +EMSCRIPTEN_KEEPALIVE void _sapp_tc_emsc_begin_drop(int num) { + if (!_sapp_tc.drop.enabled) { + return; + } + if (num < 0) { + num = 0; + } + if (num > _sapp_tc.drop.max_files) { + num = _sapp_tc.drop.max_files; + } + _sapp_tc.drop.num_files = num; + _sapp_tc_clear_drop_buffer(); +} + +EMSCRIPTEN_KEEPALIVE void _sapp_tc_emsc_drop(int i, const char* name) { + /* NOTE: name is only the filename part, not a path */ + if (!_sapp_tc.drop.enabled) { + return; + } + if (0 == name) { + return; + } + SOKOL_ASSERT(_sapp_tc.drop.num_files <= _sapp_tc.drop.max_files); + if ((i < 0) || (i >= _sapp_tc.drop.num_files)) { + return; + } + if (!_sapp_tc_strcpy(name, _sapp_tc_dropped_file_path_ptr(i), (size_t)_sapp_tc.drop.max_path_length)) { + _SAPP_ERROR(DROPPED_FILE_PATH_TOO_LONG); + _sapp_tc.drop.num_files = 0; + } +} + +EMSCRIPTEN_KEEPALIVE void _sapp_tc_emsc_end_drop(int x, int y, int mods) { + if (!_sapp_tc.drop.enabled) { + return; + } + if (0 == _sapp_tc.drop.num_files) { + /* there was an error copying the filenames */ + _sapp_tc_clear_drop_buffer(); + return; + + } + if (_sapp_tc_events_enabled()) { + _sapp_tc.mouse.x = (float)x * _sapp_tc.dpi_scale; + _sapp_tc.mouse.y = (float)y * _sapp_tc.dpi_scale; + _sapp_tc.mouse.dx = 0.0f; + _sapp_tc.mouse.dy = 0.0f; + _sapp_tc_init_event(SAPP_EVENTTYPE_FILES_DROPPED); + // see _sapp_tc_js_add_dragndrop_listeners for mods constants + if (mods & 1) { _sapp_tc.event.modifiers |= SAPP_MODIFIER_SHIFT; } + if (mods & 2) { _sapp_tc.event.modifiers |= SAPP_MODIFIER_CTRL; } + if (mods & 4) { _sapp_tc.event.modifiers |= SAPP_MODIFIER_ALT; } + if (mods & 8) { _sapp_tc.event.modifiers |= SAPP_MODIFIER_SUPER; } + _sapp_tc_call_event(&_sapp_tc.event); + } +} + +EMSCRIPTEN_KEEPALIVE void _sapp_tc_emsc_invoke_fetch_cb(int index, int success, int error_code, _sapp_tc_html5_fetch_callback callback, uint32_t fetched_size, void* buf_ptr, uint32_t buf_size, void* user_data) { + _SAPP_STRUCT(sapp_html5_fetch_response, response); + response.succeeded = (0 != success); + response.error_code = (sapp_html5_fetch_error) error_code; + response.file_index = index; + response.data.ptr = buf_ptr; + response.data.size = fetched_size; + response.buffer.ptr = buf_ptr; + response.buffer.size = buf_size; + response.user_data = user_data; + callback(&response); +} + +// will be called after the request/exitFullscreen promise rejects +// to restore the _sapp_tc.fullscreen flag to the actual fullscreen state +EMSCRIPTEN_KEEPALIVE void _sapp_tc_emsc_set_fullscreen_flag(int f) { + _sapp_tc.fullscreen = (bool)f; +} + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +EM_JS(void, _sapp_tc_js_add_beforeunload_listener, (void), { + Module.sokol_beforeunload = (event) => { + if (__sapp_tc_html5_get_ask_leave_site() != 0) { + event.preventDefault(); + event.returnValue = ' '; + } + }; + window.addEventListener('beforeunload', Module.sokol_beforeunload); +}) + +EM_JS(void, _sapp_tc_js_remove_beforeunload_listener, (void), { + window.removeEventListener('beforeunload', Module.sokol_beforeunload); +}) + +EM_JS(void, _sapp_tc_js_add_clipboard_listener, (void), { + Module.sokol_paste = (event) => { + const pasted_str = event.clipboardData.getData('text'); + withStackSave(() => { + const cstr = stringToUTF8OnStack(pasted_str); + __sapp_tc_emsc_onpaste(cstr); + }); + }; + window.addEventListener('paste', Module.sokol_paste); +}) + +EM_JS(void, _sapp_tc_js_remove_clipboard_listener, (void), { + window.removeEventListener('paste', Module.sokol_paste); +}) + +EM_JS(void, _sapp_tc_js_write_clipboard, (const char* c_str), { + const str = UTF8ToString(c_str); + const ta = document.createElement('textarea'); + ta.setAttribute('autocomplete', 'off'); + ta.setAttribute('autocorrect', 'off'); + ta.setAttribute('autocapitalize', 'off'); + ta.setAttribute('spellcheck', 'false'); + ta.style.left = -100 + 'px'; + ta.style.top = -100 + 'px'; + ta.style.height = 1; + ta.style.width = 1; + ta.value = str; + document.body.appendChild(ta); + ta.select(); + document.execCommand('copy'); + document.body.removeChild(ta); +}) + +_SOKOL_PRIVATE void _sapp_tc_emsc_set_clipboard_string(const char* str) { + _sapp_tc_js_write_clipboard(str); +} + +EM_JS(void, _sapp_tc_js_add_dragndrop_listeners, (void), { + Module.sokol_drop_files = []; + Module.sokol_dragenter = (event) => { + event.stopPropagation(); + event.preventDefault(); + }; + Module.sokol_dragleave = (event) => { + event.stopPropagation(); + event.preventDefault(); + }; + Module.sokol_dragover = (event) => { + event.stopPropagation(); + event.preventDefault(); + }; + Module.sokol_drop = (event) => { + event.stopPropagation(); + event.preventDefault(); + const files = event.dataTransfer.files; + Module.sokol_dropped_files = files; + __sapp_tc_emsc_begin_drop(files.length); + for (let i = 0; i < files.length; i++) { + withStackSave(() => { + const cstr = stringToUTF8OnStack(files[i].name); + __sapp_tc_emsc_drop(i, cstr); + }); + } + let mods = 0; + if (event.shiftKey) { mods |= 1; } + if (event.ctrlKey) { mods |= 2; } + if (event.altKey) { mods |= 4; } + if (event.metaKey) { mods |= 8; } + // FIXME? see computation of targetX/targetY in emscripten via getClientBoundingRect + __sapp_tc_emsc_end_drop(event.clientX, event.clientY, mods); + }; + \x2F\x2A\x2A @suppress {missingProperties} \x2A\x2F + const canvas = Module.sapp_emsc_target; + canvas.addEventListener('dragenter', Module.sokol_dragenter, false); + canvas.addEventListener('dragleave', Module.sokol_dragleave, false); + canvas.addEventListener('dragover', Module.sokol_dragover, false); + canvas.addEventListener('drop', Module.sokol_drop, false); +}) + +EM_JS(uint32_t, _sapp_tc_js_dropped_file_size, (int index), { + \x2F\x2A\x2A @suppress {missingProperties} \x2A\x2F + const files = Module.sokol_dropped_files; + if ((index < 0) || (index >= files.length)) { + return 0; + } else { + return files[index].size; + } +}) + +EM_JS(void, _sapp_tc_js_fetch_dropped_file, (int index, _sapp_tc_html5_fetch_callback callback, void* buf_ptr, uint32_t buf_size, void* user_data), { + const reader = new FileReader(); + reader.onload = (loadEvent) => { + const content = loadEvent.target.result; + if (content.byteLength > buf_size) { + // SAPP_HTML5_FETCH_ERROR_BUFFER_TOO_SMALL + __sapp_tc_emsc_invoke_fetch_cb(index, 0, 1, callback, 0, buf_ptr, buf_size, user_data); + } else { + HEAPU8.set(new Uint8Array(content), buf_ptr); + __sapp_tc_emsc_invoke_fetch_cb(index, 1, 0, callback, content.byteLength, buf_ptr, buf_size, user_data); + } + }; + reader.onerror = () => { + // SAPP_HTML5_FETCH_ERROR_OTHER + __sapp_tc_emsc_invoke_fetch_cb(index, 0, 2, callback, 0, buf_ptr, buf_size, user_data); + }; + \x2F\x2A\x2A @suppress {missingProperties} \x2A\x2F + const files = Module.sokol_dropped_files; + reader.readAsArrayBuffer(files[index]); +}) + +EM_JS(void, _sapp_tc_js_remove_dragndrop_listeners, (void), { + \x2F\x2A\x2A @suppress {missingProperties} \x2A\x2F + const canvas = Module.sapp_emsc_target; + canvas.removeEventListener('dragenter', Module.sokol_dragenter); + canvas.removeEventListener('dragleave', Module.sokol_dragleave); + canvas.removeEventListener('dragover', Module.sokol_dragover); + canvas.removeEventListener('drop', Module.sokol_drop); +}) + +EM_JS(void, _sapp_tc_js_init, (const char* c_str_target_selector, const char* c_str_document_title), { + if (c_str_document_title !== 0) { + document.title = UTF8ToString(c_str_document_title); + } + const target_selector_str = UTF8ToString(c_str_target_selector); + if (Module['canvas'] !== undefined) { + if (typeof Module['canvas'] === 'object') { + specialHTMLTargets[target_selector_str] = Module['canvas']; + } else { + console.warn("sokol_app.h: Module['canvas'] is set but is not an object"); + } + } + Module.sapp_emsc_target = findCanvasEventTarget(target_selector_str); + if (!Module.sapp_emsc_target) { + console.warn("sokol_app.h: can't find html5_canvas_selector ", target_selector_str); + } + if (!Module.sapp_emsc_target.requestPointerLock) { + console.warn("sokol_app.h: target doesn't support requestPointerLock: ", target_selector_str); + } +}) + +_SOKOL_PRIVATE EM_BOOL _sapp_tc_emsc_pointerlockchange_cb(int emsc_type, const EmscriptenPointerlockChangeEvent* emsc_event, void* user_data) { + _SOKOL_UNUSED(emsc_type); + _SOKOL_UNUSED(user_data); + _sapp_tc.mouse.locked = emsc_event->isActive; + return EM_TRUE; +} + +_SOKOL_PRIVATE EM_BOOL _sapp_tc_emsc_pointerlockerror_cb(int emsc_type, const void* reserved, void* user_data) { + _SOKOL_UNUSED(emsc_type); + _SOKOL_UNUSED(reserved); + _SOKOL_UNUSED(user_data); + _sapp_tc.mouse.locked = false; + _sapp_tc.emsc.mouse_lock_requested = false; + return true; +} + +EM_JS(void, _sapp_tc_js_request_pointerlock, (void), { + if (Module.sapp_emsc_target) { + if (Module.sapp_emsc_target.requestPointerLock) { + Module.sapp_emsc_target.requestPointerLock(); + } + } +}) + +EM_JS(void, _sapp_tc_js_exit_pointerlock, (void), { + if (document.exitPointerLock) { + document.exitPointerLock(); + } +}) + +_SOKOL_PRIVATE void _sapp_tc_emsc_lock_mouse(bool lock) { + if (lock) { + /* request mouse-lock during event handler invocation (see _sapp_tc_emsc_update_mouse_lock_state) */ + _sapp_tc.emsc.mouse_lock_requested = true; + } else { + /* NOTE: the _sapp_tc.mouse_locked state will be set in the pointerlockchange callback */ + _sapp_tc.emsc.mouse_lock_requested = false; + _sapp_tc_js_exit_pointerlock(); + } +} + +/* called from inside event handlers to check if mouse lock had been requested, + and if yes, actually enter mouse lock. +*/ +_SOKOL_PRIVATE void _sapp_tc_emsc_update_mouse_lock_state(void) { + if (_sapp_tc.emsc.mouse_lock_requested) { + _sapp_tc.emsc.mouse_lock_requested = false; + _sapp_tc_js_request_pointerlock(); + } +} + +// set mouse cursor type +EM_JS(void, _sapp_tc_js_set_cursor, (int cursor_type, int shown, int use_custom_cursor_image), { + if (Module.sapp_emsc_target) { + let cursor; + if (shown === 0) { + cursor = "none"; + } else if (use_custom_cursor_image != 0) { + cursor = Module.__sapp_tc_custom_cursors[cursor_type].css_property; + } else switch (cursor_type) { + case 0: cursor = "auto"; break; // SAPP_MOUSECURSOR_DEFAULT + case 1: cursor = "default"; break; // SAPP_MOUSECURSOR_ARROW + case 2: cursor = "text"; break; // SAPP_MOUSECURSOR_IBEAM + case 3: cursor = "crosshair"; break; // SAPP_MOUSECURSOR_CROSSHAIR + case 4: cursor = "pointer"; break; // SAPP_MOUSECURSOR_POINTING_HAND + case 5: cursor = "ew-resize"; break; // SAPP_MOUSECURSOR_RESIZE_EW + case 6: cursor = "ns-resize"; break; // SAPP_MOUSECURSOR_RESIZE_NS + case 7: cursor = "nwse-resize"; break; // SAPP_MOUSECURSOR_RESIZE_NWSE + case 8: cursor = "nesw-resize"; break; // SAPP_MOUSECURSOR_RESIZE_NESW + case 9: cursor = "all-scroll"; break; // SAPP_MOUSECURSOR_RESIZE_ALL + case 10: cursor = "not-allowed"; break; // SAPP_MOUSECURSOR_NOT_ALLOWED + default: cursor = "auto"; break; + } + Module.sapp_emsc_target.style.cursor = cursor; + } +}) + +_SOKOL_PRIVATE void _sapp_tc_emsc_update_cursor(sapp_mouse_cursor cursor, bool shown) { + SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); + bool custom_cursor = _sapp_tc.custom_cursor_bound[cursor]; + _sapp_tc_js_set_cursor((int)cursor, shown ? 1 : 0, custom_cursor ? 1 : 0); +} + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdollar-in-identifier-extension" +EM_JS(void, _sapp_tc_js_make_custom_mouse_cursor, (int cursor_slot_idx, int width, int height, const void* pixels_ptr, int hotspot_x, int hotspot_y), { + // encode the cursor pixels into a BMP which then is encoded into an 'object url' + const bmp_hdr_size = 14; + const dib_hdr_size = 124; // common values are 56, I saw 124 for the rgba32-1.bmp file of the test suite included in firefox, and 108 from wikipedia example 2 (transparent) + const pixels_size = width * height * 4; + const bmp_size = bmp_hdr_size + dib_hdr_size + pixels_size; + const bmp = new Uint8Array(bmp_size); + let idx = 0; + const w8 = (val) => { + bmp[idx++] = val & 255; + }; + const w16 = (val) => { + bmp[idx++] = val & 255; + bmp[idx++] = (val >> 8) & 255; + }; + const w32 = (val) => { + bmp[idx++] = val & 255; + bmp[idx++] = (val >> 8) & 255; + bmp[idx++] = (val >> 16) & 255; + bmp[idx++] = (val >> 24) & 255; + }; + + // bmp file header + w8(66); // 'B' + w8(77); // 'M' + w32(bmp_size); + w32(0); // reserved + w32(bmp_hdr_size + dib_hdr_size); // offset to pixel data + assert(idx == bmp_hdr_size); + + // DIB header + w32(dib_hdr_size); // header size + w32(width); + w32(height); + w16(1); // planes + w16(32); // bits per pixel + w32(3); // compression method. 3 = BI_BITFIELDS + w32(pixels_size); // image size + w32(2835); // pixel per metre horizontal + w32(2835); // pixel per metre vertical + w32(0); // colors number + w32(0); // important colors + w32(0x000000ff); // red channel bit mask (big endian) + w32(0x0000ff00); // green channel bit mask (big endian) + w32(0x00ff0000); // blue channel bit mask (big endian) + w32(0xff000000); // alpha channel bit mask (big endian) + w8(66); w8(71); w8(82); w8(115); // color space type: 'sRGB' + idx += 64; // color space stuff, unused for 'Win ' or 'sRGB' + assert(idx == bmp_hdr_size + dib_hdr_size); + const row_pitch = width * 4; + for (let y = 0; y < height; y++) { + const src_idx = pixels_ptr + y * row_pitch; + const dst_idx = idx + (height - y - 1) * row_pitch; + const row_data = HEAPU8.slice(src_idx, src_idx + row_pitch); + bmp.set(row_data, dst_idx); + } + const blob = new Blob([bmp.buffer], { type: 'image/bmp' }); + const url = URL.createObjectURL(blob); + + const cursor_slot = { + css_property: `url('${url}') ${hotspot_x} ${hotspot_y}, auto`, + blob_url: url // so we can release it later + }; + + // Store a reference to the js cursor object in a global table, indexed by its sapp_mouse_cursor + if (!Module.__sapp_tc_custom_cursors) { + Module.__sapp_tc_custom_cursors = Array().fill(null); + } + Module.__sapp_tc_custom_cursors[cursor_slot_idx] = cursor_slot; +}) + +#pragma GCC diagnostic pop +EM_JS(void, _sapp_tc_js_destroy_custom_mouse_cursor, (int cursor_slot_idx), { + if (Module.__sapp_tc_custom_cursors) { + const cursor = Module.__sapp_tc_custom_cursors[cursor_slot_idx]; + URL.revokeObjectURL(cursor.blob_url); // release the url, which should allow the blob to be garbage collected. + Module.__sapp_tc_custom_cursors[cursor_slot_idx] = null; // clear this array entry + } +}) + +_SOKOL_PRIVATE bool _sapp_tc_emsc_make_custom_mouse_cursor(sapp_mouse_cursor cursor, const sapp_image_desc* desc) { + _sapp_tc_js_make_custom_mouse_cursor((int)cursor, desc->width, desc->height, desc->pixels.ptr, desc->cursor_hotspot_x, desc->cursor_hotspot_y); + return true; +} + +_SOKOL_PRIVATE void _sapp_tc_emsc_destroy_custom_mouse_cursor(sapp_mouse_cursor cursor) { + _sapp_tc_js_destroy_custom_mouse_cursor((int) cursor); +} + +// NOTE: this callback is needed to react to the user actively leaving fullscreen mode via Esc +_SOKOL_PRIVATE EM_BOOL _sapp_tc_emsc_fullscreenchange_cb(int emsc_type, const EmscriptenFullscreenChangeEvent* emsc_event, void* user_data) { + _SOKOL_UNUSED(emsc_type); + _SOKOL_UNUSED(user_data); + _sapp_tc.fullscreen = emsc_event->isFullscreen; + return true; +} + +EM_JS(void, _sapp_tc_js_toggle_fullscreen, (void), { + const canvas = Module.sapp_emsc_target; + if (canvas) { + // NOTE: Safari had the prefix until 2023, Firefox until 2018 + const fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement; + let p = undefined; + if (!fullscreenElement) { + if (canvas.requestFullscreen) { + p = canvas.requestFullscreen(); + } else if (canvas.webkitRequestFullscreen) { + p = canvas.webkitRequestFullscreen(); + } else if (canvas.mozRequestFullScreen) { + p = canvas.mozRequestFullScreen(); + } + if (p) { + p.catch((err) => { + console.warn('_sapp_tc_js_toggle_fullscreen(): failed to enter fullscreen mode with', err); + __sapp_tc_emsc_set_fullscreen_flag(0); + }); + } else { + console.warn('_sapp_tc_js_toogle_fullscreen(): browser has no [webkit|moz]requestFullscreen function'); + __sapp_tc_emsc_set_fullscreen_flag(0); + } + } else { + if (document.exitFullscreen) { + p = document.exitFullscreen(); + } else if (document.webkitExitFullscreen) { + p = document.webkitExitFullscreen(); + } else if (document.mozCancelFullScreen) { + p = document.mozCancelFullScreen(); + } + if (p) { + p.catch((err) => { + console.warn('_sapp_tc_js_toggle_fullscreen(): failed to exit fullscreen mode with', err); + __sapp_tc_emsc_set_fullscreen_flag(1); + }); + } else { + console.warn('_sapp_tc_js_toggle_fullscreen(): browser has no [wekbit|moz]exitFullscreen'); + // NOTE: don't need to explicitly set the fullscreen flag here + } + } + } +}) + +_SOKOL_PRIVATE void _sapp_tc_emsc_toggle_fullscreen(void) { + // toggle the fullscreen flag preliminary, this may be undone + // when requesting/exiting fullscreen mode actually fails + _sapp_tc.fullscreen = !_sapp_tc.fullscreen; + _sapp_tc_js_toggle_fullscreen(); +} + +/* JS helper functions to update browser tab favicon */ +EM_JS(void, _sapp_tc_js_clear_favicon, (void), { + const link = document.getElementById('sokol-app-favicon'); + if (link) { + document.head.removeChild(link); + } +}) + +EM_JS(void, _sapp_tc_js_set_favicon, (int w, int h, const uint8_t* pixels), { + const canvas = document.createElement('canvas'); + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext('2d'); + const img_data = ctx.createImageData(w, h); + img_data.data.set(HEAPU8.subarray(pixels, pixels + w*h*4)); + ctx.putImageData(img_data, 0, 0); + const new_link = document.createElement('link'); + new_link.id = 'sokol-app-favicon'; + new_link.rel = 'shortcut icon'; + new_link.href = canvas.toDataURL(); + document.head.appendChild(new_link); +}) + +_SOKOL_PRIVATE void _sapp_tc_emsc_set_icon(const sapp_icon_desc* icon_desc, int num_images) { + SOKOL_ASSERT((num_images > 0) && (num_images <= SAPP_MAX_ICONIMAGES)); + _sapp_tc_js_clear_favicon(); + // find the best matching image candidate for 16x16 pixels + int img_index = _sapp_tc_image_bestmatch(icon_desc->images, num_images, 16, 16); + const sapp_image_desc* img_desc = &icon_desc->images[img_index]; + _sapp_tc_js_set_favicon(img_desc->width, img_desc->height, (const uint8_t*) img_desc->pixels.ptr); +} + +_SOKOL_PRIVATE uint32_t _sapp_tc_emsc_mouse_button_mods(uint16_t buttons) { + uint32_t m = 0; + if (0 != (buttons & (1<<0))) { m |= SAPP_MODIFIER_LMB; } + if (0 != (buttons & (1<<1))) { m |= SAPP_MODIFIER_RMB; } // not a bug + if (0 != (buttons & (1<<2))) { m |= SAPP_MODIFIER_MMB; } // not a bug + return m; +} + +_SOKOL_PRIVATE uint32_t _sapp_tc_emsc_mouse_event_mods(const EmscriptenMouseEvent* ev) { + uint32_t m = 0; + if (ev->ctrlKey) { m |= SAPP_MODIFIER_CTRL; } + if (ev->shiftKey) { m |= SAPP_MODIFIER_SHIFT; } + if (ev->altKey) { m |= SAPP_MODIFIER_ALT; } + if (ev->metaKey) { m |= SAPP_MODIFIER_SUPER; } + m |= _sapp_tc_emsc_mouse_button_mods(_sapp_tc.emsc.mouse_buttons); + return m; +} + +_SOKOL_PRIVATE uint32_t _sapp_tc_emsc_key_event_mods(const EmscriptenKeyboardEvent* ev) { + uint32_t m = 0; + if (ev->ctrlKey) { m |= SAPP_MODIFIER_CTRL; } + if (ev->shiftKey) { m |= SAPP_MODIFIER_SHIFT; } + if (ev->altKey) { m |= SAPP_MODIFIER_ALT; } + if (ev->metaKey) { m |= SAPP_MODIFIER_SUPER; } + m |= _sapp_tc_emsc_mouse_button_mods(_sapp_tc.emsc.mouse_buttons); + return m; +} + +_SOKOL_PRIVATE uint32_t _sapp_tc_emsc_touch_event_mods(const EmscriptenTouchEvent* ev) { + uint32_t m = 0; + if (ev->ctrlKey) { m |= SAPP_MODIFIER_CTRL; } + if (ev->shiftKey) { m |= SAPP_MODIFIER_SHIFT; } + if (ev->altKey) { m |= SAPP_MODIFIER_ALT; } + if (ev->metaKey) { m |= SAPP_MODIFIER_SUPER; } + m |= _sapp_tc_emsc_mouse_button_mods(_sapp_tc.emsc.mouse_buttons); + return m; +} + +_SOKOL_PRIVATE EM_BOOL _sapp_tc_emsc_size_changed(int event_type, const EmscriptenUiEvent* ui_event, void* user_data) { + _SOKOL_UNUSED(event_type); + _SOKOL_UNUSED(user_data); + double w, h; + emscripten_get_element_css_size(_sapp_tc.html5_canvas_selector, &w, &h); + /* The above method might report zero when toggling HTML5 fullscreen, + in that case use the window's inner width reported by the + emscripten event. This works ok when toggling *into* fullscreen + but doesn't properly restore the previous canvas size when switching + back from fullscreen. + + In general, due to the HTML5's fullscreen API's flaky nature it is + recommended to use 'soft fullscreen' (stretching the WebGL canvas + over the browser windows client rect) with a CSS definition like this: + + position: absolute; + top: 0px; + left: 0px; + margin: 0px; + border: 0; + width: 100%; + height: 100%; + overflow: hidden; + display: block; + */ + if (w < 1.0) { + w = ui_event->windowInnerWidth; + } else { + _sapp_tc.window_width = _sapp_tc_roundf_gzero(w); + } + if (h < 1.0) { + h = ui_event->windowInnerHeight; + } else { + _sapp_tc.window_height = _sapp_tc_roundf_gzero(h); + } + if (_sapp_tc.desc.high_dpi) { + _sapp_tc.dpi_scale = emscripten_get_device_pixel_ratio(); + } + _sapp_tc.framebuffer_width = _sapp_tc_roundf_gzero(w * _sapp_tc.dpi_scale); + _sapp_tc.framebuffer_height = _sapp_tc_roundf_gzero(h * _sapp_tc.dpi_scale); + emscripten_set_canvas_element_size(_sapp_tc.html5_canvas_selector, _sapp_tc.framebuffer_width, _sapp_tc.framebuffer_height); + #if defined(SOKOL_WGPU) + // on WebGPU: recreate size-dependent rendering surfaces + _sapp_tc_wgpu_swapchain_size_changed(); + #endif + if (_sapp_tc_events_enabled()) { + _sapp_tc_init_event(SAPP_EVENTTYPE_RESIZED); + _sapp_tc_call_event(&_sapp_tc.event); + } + return true; +} + +_SOKOL_PRIVATE EM_BOOL _sapp_tc_emsc_mouse_cb(int emsc_type, const EmscriptenMouseEvent* emsc_event, void* user_data) { + _SOKOL_UNUSED(user_data); + bool consume_event = !_sapp_tc.desc.html5.bubble_mouse_events; + _sapp_tc.emsc.mouse_buttons = emsc_event->buttons; + if (_sapp_tc.mouse.locked) { + _sapp_tc.mouse.dx = (float) emsc_event->movementX; + _sapp_tc.mouse.dy = (float) emsc_event->movementY; + } else { + float new_x = emsc_event->targetX * _sapp_tc.dpi_scale; + float new_y = emsc_event->targetY * _sapp_tc.dpi_scale; + if (_sapp_tc.mouse.pos_valid) { + _sapp_tc.mouse.dx = new_x - _sapp_tc.mouse.x; + _sapp_tc.mouse.dy = new_y - _sapp_tc.mouse.y; + } + _sapp_tc.mouse.x = new_x; + _sapp_tc.mouse.y = new_y; + _sapp_tc.mouse.pos_valid = true; + } + if (_sapp_tc_events_enabled() && (emsc_event->button >= 0) && (emsc_event->button < SAPP_MAX_MOUSEBUTTONS)) { + sapp_event_type type; + bool is_button_event = false; + bool clear_dxdy = false; + switch (emsc_type) { + case EMSCRIPTEN_EVENT_MOUSEDOWN: + type = SAPP_EVENTTYPE_MOUSE_DOWN; + is_button_event = true; + break; + case EMSCRIPTEN_EVENT_MOUSEUP: + type = SAPP_EVENTTYPE_MOUSE_UP; + is_button_event = true; + break; + case EMSCRIPTEN_EVENT_MOUSEMOVE: + type = SAPP_EVENTTYPE_MOUSE_MOVE; + break; + case EMSCRIPTEN_EVENT_MOUSEENTER: + type = SAPP_EVENTTYPE_MOUSE_ENTER; + clear_dxdy = true; + break; + case EMSCRIPTEN_EVENT_MOUSELEAVE: + type = SAPP_EVENTTYPE_MOUSE_LEAVE; + clear_dxdy = true; + break; + default: + type = SAPP_EVENTTYPE_INVALID; + break; + } + if (clear_dxdy) { + _sapp_tc.mouse.dx = 0.0f; + _sapp_tc.mouse.dy = 0.0f; + } + if (type != SAPP_EVENTTYPE_INVALID) { + _sapp_tc_init_event(type); + _sapp_tc.event.modifiers = _sapp_tc_emsc_mouse_event_mods(emsc_event); + if (is_button_event) { + switch (emsc_event->button) { + case 0: _sapp_tc.event.mouse_button = SAPP_MOUSEBUTTON_LEFT; break; + case 1: _sapp_tc.event.mouse_button = SAPP_MOUSEBUTTON_MIDDLE; break; + case 2: _sapp_tc.event.mouse_button = SAPP_MOUSEBUTTON_RIGHT; break; + default: _sapp_tc.event.mouse_button = (sapp_mousebutton)emsc_event->button; break; + } + } else { + _sapp_tc.event.mouse_button = SAPP_MOUSEBUTTON_INVALID; + } + consume_event |= _sapp_tc_call_event(&_sapp_tc.event); + } + // mouse lock can only be activated in mouse button events (not in move, enter or leave) + if (is_button_event) { + _sapp_tc_emsc_update_mouse_lock_state(); + } + } + return consume_event; +} + +_SOKOL_PRIVATE EM_BOOL _sapp_tc_emsc_wheel_cb(int emsc_type, const EmscriptenWheelEvent* emsc_event, void* user_data) { + _SOKOL_UNUSED(emsc_type); + _SOKOL_UNUSED(user_data); + bool consume_event = !_sapp_tc.desc.html5.bubble_wheel_events; + _sapp_tc.emsc.mouse_buttons = emsc_event->mouse.buttons; + if (_sapp_tc_events_enabled()) { + _sapp_tc_init_event(SAPP_EVENTTYPE_MOUSE_SCROLL); + _sapp_tc.event.modifiers = _sapp_tc_emsc_mouse_event_mods(&emsc_event->mouse); + /* see https://github.com/floooh/sokol/issues/339 */ + float scale; + switch (emsc_event->deltaMode) { + case DOM_DELTA_PIXEL: scale = -0.01f; break; + case DOM_DELTA_LINE: scale = -1.33f; break; + case DOM_DELTA_PAGE: scale = -10.0f; break; // FIXME: this is a guess + default: scale = -0.1f; break; // shouldn't happen + } + _sapp_tc.event.scroll_x = scale * (float)emsc_event->deltaX; + _sapp_tc.event.scroll_y = scale * (float)emsc_event->deltaY; + consume_event |= _sapp_tc_call_event(&_sapp_tc.event); + } + _sapp_tc_emsc_update_mouse_lock_state(); + return consume_event; +} + +static struct { + const char* str; + sapp_keycode code; +} _sapp_tc_emsc_keymap[] = { + { "Backspace", SAPP_KEYCODE_BACKSPACE }, + { "Tab", SAPP_KEYCODE_TAB }, + { "Enter", SAPP_KEYCODE_ENTER }, + { "ShiftLeft", SAPP_KEYCODE_LEFT_SHIFT }, + { "ShiftRight", SAPP_KEYCODE_RIGHT_SHIFT }, + { "ControlLeft", SAPP_KEYCODE_LEFT_CONTROL }, + { "ControlRight", SAPP_KEYCODE_RIGHT_CONTROL }, + { "AltLeft", SAPP_KEYCODE_LEFT_ALT }, + { "AltRight", SAPP_KEYCODE_RIGHT_ALT }, + { "Pause", SAPP_KEYCODE_PAUSE }, + { "CapsLock", SAPP_KEYCODE_CAPS_LOCK }, + { "Escape", SAPP_KEYCODE_ESCAPE }, + { "Space", SAPP_KEYCODE_SPACE }, + { "PageUp", SAPP_KEYCODE_PAGE_UP }, + { "PageDown", SAPP_KEYCODE_PAGE_DOWN }, + { "End", SAPP_KEYCODE_END }, + { "Home", SAPP_KEYCODE_HOME }, + { "ArrowLeft", SAPP_KEYCODE_LEFT }, + { "ArrowUp", SAPP_KEYCODE_UP }, + { "ArrowRight", SAPP_KEYCODE_RIGHT }, + { "ArrowDown", SAPP_KEYCODE_DOWN }, + { "PrintScreen", SAPP_KEYCODE_PRINT_SCREEN }, + { "Insert", SAPP_KEYCODE_INSERT }, + { "Delete", SAPP_KEYCODE_DELETE }, + { "Digit0", SAPP_KEYCODE_0 }, + { "Digit1", SAPP_KEYCODE_1 }, + { "Digit2", SAPP_KEYCODE_2 }, + { "Digit3", SAPP_KEYCODE_3 }, + { "Digit4", SAPP_KEYCODE_4 }, + { "Digit5", SAPP_KEYCODE_5 }, + { "Digit6", SAPP_KEYCODE_6 }, + { "Digit7", SAPP_KEYCODE_7 }, + { "Digit8", SAPP_KEYCODE_8 }, + { "Digit9", SAPP_KEYCODE_9 }, + { "KeyA", SAPP_KEYCODE_A }, + { "KeyB", SAPP_KEYCODE_B }, + { "KeyC", SAPP_KEYCODE_C }, + { "KeyD", SAPP_KEYCODE_D }, + { "KeyE", SAPP_KEYCODE_E }, + { "KeyF", SAPP_KEYCODE_F }, + { "KeyG", SAPP_KEYCODE_G }, + { "KeyH", SAPP_KEYCODE_H }, + { "KeyI", SAPP_KEYCODE_I }, + { "KeyJ", SAPP_KEYCODE_J }, + { "KeyK", SAPP_KEYCODE_K }, + { "KeyL", SAPP_KEYCODE_L }, + { "KeyM", SAPP_KEYCODE_M }, + { "KeyN", SAPP_KEYCODE_N }, + { "KeyO", SAPP_KEYCODE_O }, + { "KeyP", SAPP_KEYCODE_P }, + { "KeyQ", SAPP_KEYCODE_Q }, + { "KeyR", SAPP_KEYCODE_R }, + { "KeyS", SAPP_KEYCODE_S }, + { "KeyT", SAPP_KEYCODE_T }, + { "KeyU", SAPP_KEYCODE_U }, + { "KeyV", SAPP_KEYCODE_V }, + { "KeyW", SAPP_KEYCODE_W }, + { "KeyX", SAPP_KEYCODE_X }, + { "KeyY", SAPP_KEYCODE_Y }, + { "KeyZ", SAPP_KEYCODE_Z }, + { "MetaLeft", SAPP_KEYCODE_LEFT_SUPER }, + { "MetaRight", SAPP_KEYCODE_RIGHT_SUPER }, + { "Numpad0", SAPP_KEYCODE_KP_0 }, + { "Numpad1", SAPP_KEYCODE_KP_1 }, + { "Numpad2", SAPP_KEYCODE_KP_2 }, + { "Numpad3", SAPP_KEYCODE_KP_3 }, + { "Numpad4", SAPP_KEYCODE_KP_4 }, + { "Numpad5", SAPP_KEYCODE_KP_5 }, + { "Numpad6", SAPP_KEYCODE_KP_6 }, + { "Numpad7", SAPP_KEYCODE_KP_7 }, + { "Numpad8", SAPP_KEYCODE_KP_8 }, + { "Numpad9", SAPP_KEYCODE_KP_9 }, + { "NumpadMultiply", SAPP_KEYCODE_KP_MULTIPLY }, + { "NumpadAdd", SAPP_KEYCODE_KP_ADD }, + { "NumpadSubtract", SAPP_KEYCODE_KP_SUBTRACT }, + { "NumpadDecimal", SAPP_KEYCODE_KP_DECIMAL }, + { "NumpadDivide", SAPP_KEYCODE_KP_DIVIDE }, + { "F1", SAPP_KEYCODE_F1 }, + { "F2", SAPP_KEYCODE_F2 }, + { "F3", SAPP_KEYCODE_F3 }, + { "F4", SAPP_KEYCODE_F4 }, + { "F5", SAPP_KEYCODE_F5 }, + { "F6", SAPP_KEYCODE_F6 }, + { "F7", SAPP_KEYCODE_F7 }, + { "F8", SAPP_KEYCODE_F8 }, + { "F9", SAPP_KEYCODE_F9 }, + { "F10", SAPP_KEYCODE_F10 }, + { "F11", SAPP_KEYCODE_F11 }, + { "F12", SAPP_KEYCODE_F12 }, + { "NumLock", SAPP_KEYCODE_NUM_LOCK }, + { "ScrollLock", SAPP_KEYCODE_SCROLL_LOCK }, + { "Semicolon", SAPP_KEYCODE_SEMICOLON }, + { "Equal", SAPP_KEYCODE_EQUAL }, + { "Comma", SAPP_KEYCODE_COMMA }, + { "Minus", SAPP_KEYCODE_MINUS }, + { "Period", SAPP_KEYCODE_PERIOD }, + { "Slash", SAPP_KEYCODE_SLASH }, + { "Backquote", SAPP_KEYCODE_GRAVE_ACCENT }, + { "BracketLeft", SAPP_KEYCODE_LEFT_BRACKET }, + { "Backslash", SAPP_KEYCODE_BACKSLASH }, + { "BracketRight", SAPP_KEYCODE_RIGHT_BRACKET }, + { "Quote", SAPP_KEYCODE_GRAVE_ACCENT }, // FIXME: ??? + { 0, SAPP_KEYCODE_INVALID }, +}; + +_SOKOL_PRIVATE sapp_keycode _sapp_tc_emsc_translate_key(const char* str) { + int i = 0; + const char* keystr; + while (( keystr = _sapp_tc_emsc_keymap[i].str )) { + if (0 == strcmp(str, keystr)) { + return _sapp_tc_emsc_keymap[i].code; + } + i += 1; + } + return SAPP_KEYCODE_INVALID; +} + +// returns true if the key code is a 'character key', this is used to decide +// if a key event needs to bubble up to create a char event +_SOKOL_PRIVATE bool _sapp_tc_emsc_is_char_key(sapp_keycode key_code) { + return key_code < SAPP_KEYCODE_WORLD_1; +} + +_SOKOL_PRIVATE EM_BOOL _sapp_tc_emsc_key_cb(int emsc_type, const EmscriptenKeyboardEvent* emsc_event, void* user_data) { + _SOKOL_UNUSED(user_data); + bool consume_event = false; + if (_sapp_tc_events_enabled()) { + sapp_event_type type; + switch (emsc_type) { + case EMSCRIPTEN_EVENT_KEYDOWN: + type = SAPP_EVENTTYPE_KEY_DOWN; + break; + case EMSCRIPTEN_EVENT_KEYUP: + type = SAPP_EVENTTYPE_KEY_UP; + break; + case EMSCRIPTEN_EVENT_KEYPRESS: + type = SAPP_EVENTTYPE_CHAR; + break; + default: + type = SAPP_EVENTTYPE_INVALID; + break; + } + if (type != SAPP_EVENTTYPE_INVALID) { + bool send_keyup_followup = false; + _sapp_tc_init_event(type); + _sapp_tc.event.key_repeat = emsc_event->repeat; + _sapp_tc.event.modifiers = _sapp_tc_emsc_key_event_mods(emsc_event); + if (type == SAPP_EVENTTYPE_CHAR) { + // NOTE: charCode doesn't appear to be supported on Android Chrome + _sapp_tc.event.char_code = emsc_event->charCode; + consume_event |= !_sapp_tc.desc.html5.bubble_char_events; + } else { + if (0 != emsc_event->code[0]) { + // This code path is for desktop browsers which send untranslated 'physical' key code strings + // (which is what we actually want for key events) + _sapp_tc.event.key_code = _sapp_tc_emsc_translate_key(emsc_event->code); + } else { + // This code path is for mobile browsers which only send localized key code + // strings. Note that the translation will only work for a small subset + // of localization-agnostic keys (like Enter, arrow keys, etc...), but + // regular alpha-numeric keys will all result in an SAPP_KEYCODE_INVALID) + _sapp_tc.event.key_code = _sapp_tc_emsc_translate_key(emsc_event->key); + } + + // Special hack for macOS: if the Super key is pressed, macOS doesn't + // send keyUp events. As a workaround, to prevent keys from + // "sticking", we'll send a keyup event following a keydown + // when the SUPER key is pressed + if ((type == SAPP_EVENTTYPE_KEY_DOWN) && + (_sapp_tc.event.key_code != SAPP_KEYCODE_LEFT_SUPER) && + (_sapp_tc.event.key_code != SAPP_KEYCODE_RIGHT_SUPER) && + (_sapp_tc.event.modifiers & SAPP_MODIFIER_SUPER)) + { + send_keyup_followup = true; + } + + // 'character key events' will always need to bubble up, otherwise the browser + // wouldn't be able to generate character events. + if (!_sapp_tc_emsc_is_char_key(_sapp_tc.event.key_code)) { + consume_event |= !_sapp_tc.desc.html5.bubble_key_events; + } + } + consume_event |= _sapp_tc_call_event(&_sapp_tc.event); + if (send_keyup_followup) { + _sapp_tc.event.type = SAPP_EVENTTYPE_KEY_UP; + consume_event |= _sapp_tc_call_event(&_sapp_tc.event); + } + } + } + _sapp_tc_emsc_update_mouse_lock_state(); + return consume_event; +} + +_SOKOL_PRIVATE EM_BOOL _sapp_tc_emsc_touch_cb(int emsc_type, const EmscriptenTouchEvent* emsc_event, void* user_data) { + _SOKOL_UNUSED(user_data); + bool consume_event = !_sapp_tc.desc.html5.bubble_touch_events; + if (_sapp_tc_events_enabled()) { + sapp_event_type type; + switch (emsc_type) { + case EMSCRIPTEN_EVENT_TOUCHSTART: + type = SAPP_EVENTTYPE_TOUCHES_BEGAN; + break; + case EMSCRIPTEN_EVENT_TOUCHMOVE: + type = SAPP_EVENTTYPE_TOUCHES_MOVED; + break; + case EMSCRIPTEN_EVENT_TOUCHEND: + type = SAPP_EVENTTYPE_TOUCHES_ENDED; + break; + case EMSCRIPTEN_EVENT_TOUCHCANCEL: + type = SAPP_EVENTTYPE_TOUCHES_CANCELLED; + break; + default: + type = SAPP_EVENTTYPE_INVALID; + break; + } + if (type != SAPP_EVENTTYPE_INVALID) { + _sapp_tc_init_event(type); + _sapp_tc.event.modifiers = _sapp_tc_emsc_touch_event_mods(emsc_event); + _sapp_tc.event.num_touches = emsc_event->numTouches; + if (_sapp_tc.event.num_touches > SAPP_MAX_TOUCHPOINTS) { + _sapp_tc.event.num_touches = SAPP_MAX_TOUCHPOINTS; + } + for (int i = 0; i < _sapp_tc.event.num_touches; i++) { + const EmscriptenTouchPoint* src = &emsc_event->touches[i]; + sapp_touchpoint* dst = &_sapp_tc.event.touches[i]; + dst->identifier = (uintptr_t)src->identifier; + dst->pos_x = src->targetX * _sapp_tc.dpi_scale; + dst->pos_y = src->targetY * _sapp_tc.dpi_scale; + dst->changed = src->isChanged; + } + consume_event |= _sapp_tc_call_event(&_sapp_tc.event); + } + } + return consume_event; +} + +_SOKOL_PRIVATE EM_BOOL _sapp_tc_emsc_focus_cb(int emsc_type, const EmscriptenFocusEvent* emsc_event, void* user_data) { + _SOKOL_UNUSED(emsc_type); + _SOKOL_UNUSED(emsc_event); + _SOKOL_UNUSED(user_data); + if (_sapp_tc_events_enabled()) { + _sapp_tc_init_event(SAPP_EVENTTYPE_FOCUSED); + _sapp_tc_call_event(&_sapp_tc.event); + } + return true; +} + +_SOKOL_PRIVATE EM_BOOL _sapp_tc_emsc_blur_cb(int emsc_type, const EmscriptenFocusEvent* emsc_event, void* user_data) { + _SOKOL_UNUSED(emsc_type); + _SOKOL_UNUSED(emsc_event); + _SOKOL_UNUSED(user_data); + if (_sapp_tc_events_enabled()) { + _sapp_tc_init_event(SAPP_EVENTTYPE_UNFOCUSED); + _sapp_tc_call_event(&_sapp_tc.event); + } + return true; +} + +#if defined(SOKOL_GLES3) +_SOKOL_PRIVATE EM_BOOL _sapp_tc_emsc_webgl_context_cb(int emsc_type, const void* reserved, void* user_data) { + _SOKOL_UNUSED(reserved); + _SOKOL_UNUSED(user_data); + sapp_event_type type; + switch (emsc_type) { + case EMSCRIPTEN_EVENT_WEBGLCONTEXTLOST: type = SAPP_EVENTTYPE_SUSPENDED; break; + case EMSCRIPTEN_EVENT_WEBGLCONTEXTRESTORED: type = SAPP_EVENTTYPE_RESUMED; break; + default: type = SAPP_EVENTTYPE_INVALID; break; + } + if (_sapp_tc_events_enabled() && (SAPP_EVENTTYPE_INVALID != type)) { + _sapp_tc_init_event(type); + _sapp_tc_call_event(&_sapp_tc.event); + } + return true; +} + +_SOKOL_PRIVATE void _sapp_tc_emsc_webgl_init(void) { + EmscriptenWebGLContextAttributes attrs; + emscripten_webgl_init_context_attributes(&attrs); + attrs.alpha = _sapp_tc.desc.alpha; + attrs.depth = true; + attrs.stencil = true; + attrs.antialias = _sapp_tc.sample_count > 1; + attrs.premultipliedAlpha = _sapp_tc.desc.html5.premultiplied_alpha; + attrs.preserveDrawingBuffer = _sapp_tc.desc.html5.preserve_drawing_buffer; + attrs.enableExtensionsByDefault = true; + attrs.majorVersion = 2; + EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_create_context(_sapp_tc.html5_canvas_selector, &attrs); + // FIXME: error message? + emscripten_webgl_make_context_current(ctx); + glGetIntegerv(GL_FRAMEBUFFER_BINDING, (GLint*)&_sapp_tc.gl.framebuffer); +} +#endif + +_SOKOL_PRIVATE void _sapp_tc_emsc_register_eventhandlers(void) { + // NOTE: HTML canvas doesn't receive input focus, this is why key event handlers are added + // to the window object (this could be worked around by adding a "tab index" to the + // canvas) + emscripten_set_mousedown_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_mouse_cb); + emscripten_set_mouseup_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_mouse_cb); + emscripten_set_mousemove_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_mouse_cb); + emscripten_set_mouseenter_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_mouse_cb); + emscripten_set_mouseleave_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_mouse_cb); + emscripten_set_wheel_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_wheel_cb); + // Modified by tettou771 for TrussC: register keyboard events on canvas instead of window + // This allows other page elements (like Monaco editor) to handle keyboard events + // Canvas must have tabindex attribute to receive focus + emscripten_set_keydown_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_key_cb); + emscripten_set_keyup_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_key_cb); + emscripten_set_keypress_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_key_cb); + emscripten_set_touchstart_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_touch_cb); + emscripten_set_touchmove_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_touch_cb); + emscripten_set_touchend_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_touch_cb); + emscripten_set_touchcancel_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_touch_cb); + emscripten_set_pointerlockchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, 0, true, _sapp_tc_emsc_pointerlockchange_cb); + emscripten_set_pointerlockerror_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, 0, true, _sapp_tc_emsc_pointerlockerror_cb); + emscripten_set_focus_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, true, _sapp_tc_emsc_focus_cb); + emscripten_set_blur_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, true, _sapp_tc_emsc_blur_cb); + emscripten_set_fullscreenchange_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_fullscreenchange_cb); + _sapp_tc_js_add_beforeunload_listener(); + if (_sapp_tc.clipboard.enabled) { + _sapp_tc_js_add_clipboard_listener(); + } + if (_sapp_tc.drop.enabled) { + _sapp_tc_js_add_dragndrop_listeners(); + } + #if defined(SOKOL_GLES3) + emscripten_set_webglcontextlost_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_webgl_context_cb); + emscripten_set_webglcontextrestored_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_webgl_context_cb); + #endif +} + +_SOKOL_PRIVATE void _sapp_tc_emsc_unregister_eventhandlers(void) { + emscripten_set_mousedown_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + emscripten_set_mouseup_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + emscripten_set_mousemove_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + emscripten_set_mouseenter_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + emscripten_set_mouseleave_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + emscripten_set_wheel_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + // Modified by tettou771 for TrussC: unregister from canvas (see register above) + emscripten_set_keydown_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + emscripten_set_keyup_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + emscripten_set_keypress_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + emscripten_set_touchstart_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + emscripten_set_touchmove_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + emscripten_set_touchend_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + emscripten_set_touchcancel_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + emscripten_set_pointerlockchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, 0, true, 0); + emscripten_set_pointerlockerror_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, 0, true, 0); + emscripten_set_focus_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, true, 0); + emscripten_set_blur_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, true, 0); + emscripten_set_fullscreenchange_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + if (!_sapp_tc.desc.html5.canvas_resize) { + emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, true, 0); + } + _sapp_tc_js_remove_beforeunload_listener(); + if (_sapp_tc.clipboard.enabled) { + _sapp_tc_js_remove_clipboard_listener(); + } + if (_sapp_tc.drop.enabled) { + _sapp_tc_js_remove_dragndrop_listeners(); + } + #if defined(SOKOL_GLES3) + emscripten_set_webglcontextlost_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + emscripten_set_webglcontextrestored_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + #endif +} + +_SOKOL_PRIVATE EM_BOOL _sapp_tc_emsc_frame_animation_loop(double time, void* userData) { + _SOKOL_UNUSED(userData); + _sapp_tc_timing_update(&_sapp_tc.timing, time / 1000.0); + + #if defined(SOKOL_WGPU) + _sapp_tc_wgpu_frame(); + #else + _sapp_tc_frame(); + #endif + + // quit-handling + if (_sapp_tc.quit_requested) { + _sapp_tc_init_event(SAPP_EVENTTYPE_QUIT_REQUESTED); + _sapp_tc_call_event(&_sapp_tc.event); + if (_sapp_tc.quit_requested) { + _sapp_tc.quit_ordered = true; + } + } + if (_sapp_tc.quit_ordered) { + _sapp_tc_emsc_unregister_eventhandlers(); + #if defined(SOKOL_WGPU) + _sapp_tc_wgpu_discard(); + #endif + _sapp_tc_call_cleanup(); + _sapp_tc_discard_state(); + return EM_FALSE; + } + return EM_TRUE; +} + +_SOKOL_PRIVATE void _sapp_tc_emsc_frame_main_loop(void) { + const double time = emscripten_performance_now(); + if (!_sapp_tc_emsc_frame_animation_loop(time, 0)) { + emscripten_cancel_main_loop(); + } +} + +_SOKOL_PRIVATE void _sapp_tc_emsc_run(const sapp_desc* desc) { + _sapp_tc_init_state(desc); + _sapp_tc.fullscreen = false; // override user provided fullscreen state: can't start in fullscreen on the web! + const char* document_title = desc->html5.update_document_title ? _sapp_tc.window_title : 0; + _sapp_tc_js_init(_sapp_tc.html5_canvas_selector, document_title); + double w, h; + if (_sapp_tc.desc.html5.canvas_resize) { + w = (double) _sapp_tc_def(_sapp_tc.desc.width, _SAPP_FALLBACK_DEFAULT_WINDOW_WIDTH); + h = (double) _sapp_tc_def(_sapp_tc.desc.height, _SAPP_FALLBACK_DEFAULT_WINDOW_HEIGHT); + } else { + emscripten_get_element_css_size(_sapp_tc.html5_canvas_selector, &w, &h); + emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, false, _sapp_tc_emsc_size_changed); + } + if (_sapp_tc.desc.high_dpi) { + _sapp_tc.dpi_scale = emscripten_get_device_pixel_ratio(); + } + _sapp_tc.window_width = _sapp_tc_roundf_gzero(w); + _sapp_tc.window_height = _sapp_tc_roundf_gzero(h); + _sapp_tc.framebuffer_width = _sapp_tc_roundf_gzero(w * _sapp_tc.dpi_scale); + _sapp_tc.framebuffer_height = _sapp_tc_roundf_gzero(h * _sapp_tc.dpi_scale); + emscripten_set_canvas_element_size(_sapp_tc.html5_canvas_selector, _sapp_tc.framebuffer_width, _sapp_tc.framebuffer_height); + #if defined(SOKOL_GLES3) + _sapp_tc_emsc_webgl_init(); + #elif defined(SOKOL_WGPU) + _sapp_tc_wgpu_init(); + #endif + _sapp_tc.valid = true; + _sapp_tc_emsc_register_eventhandlers(); + sapp_set_icon(&desc->icon); + + // start the frame loop + if (_sapp_tc.desc.html5.use_emsc_set_main_loop) { + emscripten_set_main_loop(_sapp_tc_emsc_frame_main_loop, 0, _sapp_tc.desc.html5.emsc_set_main_loop_simulate_infinite_loop); + } else { + emscripten_request_animation_frame_loop(_sapp_tc_emsc_frame_animation_loop, 0); + } + // NOT A BUG: do not call _sapp_tc_discard_state() here, instead this is + // called in _sapp_tc_emsc_frame() when the application is ordered to quit +} + +#if !defined(SOKOL_NO_ENTRY) +int main(int argc, char* argv[]) { + sapp_desc desc = sokol_main(argc, argv); + _sapp_tc_emsc_run(&desc); + return 0; +} +#endif /* SOKOL_NO_ENTRY */ +#endif /* _SAPP_EMSCRIPTEN */ + +/* ---- public API (lifted from sokol_app.h >>public, web-live branches) ---- */ +#if defined(SOKOL_NO_ENTRY) +SOKOL_API_IMPL void sapp_run(const sapp_desc* desc) { + SOKOL_ASSERT(desc); + #if defined(_SAPP_MACOS) + _sapp_tc_macos_run(desc); + #elif defined(_SAPP_IOS) + _sapp_tc_ios_run(desc); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_run(desc); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_run(desc); + #elif defined(_SAPP_LINUX) + _sapp_tc_linux_run(desc); + #else + #error "sapp_run() not supported on this platform" + #endif +} + +/* this is just a stub so the linker doesn't complain */ +sapp_desc sokol_main(int argc, char* argv[]) { + _SOKOL_UNUSED(argc); + _SOKOL_UNUSED(argv); + _SAPP_STRUCT(sapp_desc, desc); + return desc; +} +#else +/* likewise, in normal mode, sapp_run() is just an empty stub */ +SOKOL_API_IMPL void sapp_run(const sapp_desc* desc) { + _SOKOL_UNUSED(desc); +} +#endif + +SOKOL_API_IMPL bool sapp_isvalid(void) { + return _sapp_tc.valid; +} + +SOKOL_API_IMPL void* sapp_userdata(void) { + return _sapp_tc.desc.user_data; +} + +SOKOL_API_IMPL sapp_desc sapp_query_desc(void) { + return _sapp_tc.desc; +} + +SOKOL_API_IMPL uint64_t sapp_frame_count(void) { + return _sapp_tc.frame_count; +} + +SOKOL_API_IMPL double sapp_frame_duration(void) { + #if defined(_SAPP_MACOS) && defined(SOKOL_METAL) + return _sapp_tc_macos_mtl_timing_frame_duration(); + #elif defined(_SAPP_IOS) && defined(SOKOL_METAL) + return _sapp_tc_ios_mtl_timing_frame_duration(); + #else + return _sapp_tc_timing_get(&_sapp_tc.timing); + #endif +} + +SOKOL_API_IMPL double sapp_frame_duration_unfiltered(void) { + return _sapp_tc.timing.dt; +} + +SOKOL_API_IMPL int sapp_width(void) { + return (_sapp_tc.framebuffer_width > 0) ? _sapp_tc.framebuffer_width : 1; +} + +SOKOL_API_IMPL float sapp_widthf(void) { + return (float)sapp_width(); +} + +SOKOL_API_IMPL int sapp_height(void) { + return (_sapp_tc.framebuffer_height > 0) ? _sapp_tc.framebuffer_height : 1; +} + +SOKOL_API_IMPL float sapp_heightf(void) { + return (float)sapp_height(); +} + +SOKOL_API_IMPL sapp_pixel_format sapp_color_format(void) { + #if defined(SOKOL_WGPU) + switch (_sapp_tc.wgpu.render_format) { + case WGPUTextureFormat_RGBA8Unorm: + return SAPP_PIXELFORMAT_RGBA8; + case WGPUTextureFormat_BGRA8Unorm: + return SAPP_PIXELFORMAT_BGRA8; + default: + SOKOL_UNREACHABLE; + return SAPP_PIXELFORMAT_NONE; + } + #elif defined(SOKOL_VULKAN) + switch (_sapp_tc.vk.surface_format.format) { + case VK_FORMAT_R8G8B8A8_UNORM: + return SAPP_PIXELFORMAT_RGBA8; + case VK_FORMAT_B8G8R8A8_UNORM: + return SAPP_PIXELFORMAT_BGRA8; + default: + // FIXME! + SOKOL_UNREACHABLE; + return SAPP_PIXELFORMAT_NONE; + } + #elif defined(SOKOL_METAL) || defined(SOKOL_D3D11) + // Modified by tettou771 for TrussC: report 10-bit color format (RGB10A2) + return SAPP_PIXELFORMAT_RGB10A2; + #else + return SAPP_PIXELFORMAT_RGBA8; + #endif +} + +SOKOL_API_IMPL sapp_pixel_format sapp_depth_format(void) { + return SAPP_PIXELFORMAT_DEPTH_STENCIL; +} + +SOKOL_API_IMPL int sapp_sample_count(void) { + return _sapp_tc.sample_count; +} + +SOKOL_API_IMPL bool sapp_high_dpi(void) { + return _sapp_tc.desc.high_dpi && (_sapp_tc.dpi_scale >= 1.5f); +} + +SOKOL_API_IMPL float sapp_dpi_scale(void) { + return _sapp_tc.dpi_scale; +} + +SOKOL_API_IMPL const void* sapp_egl_get_display(void) { + SOKOL_ASSERT(_sapp_tc.valid); + #if defined(_SAPP_ANDROID) + return _sapp_tc.android.display; + #elif defined(_SAPP_LINUX) && defined(_SAPP_EGL) + return _sapp_tc.egl.display; + #else + return 0; + #endif +} + +SOKOL_API_IMPL const void* sapp_egl_get_context(void) { + SOKOL_ASSERT(_sapp_tc.valid); + #if defined(_SAPP_ANDROID) + return _sapp_tc.android.context; + #elif defined(_SAPP_LINUX) && defined(_SAPP_EGL) + return _sapp_tc.egl.context; + #else + return 0; + #endif +} + +SOKOL_API_IMPL void sapp_show_keyboard(bool show) { + #if defined(_SAPP_IOS) + _sapp_tc_ios_show_keyboard(show); + #elif defined(_SAPP_ANDROID) + _sapp_tc_android_show_keyboard(show); + #else + _SOKOL_UNUSED(show); + #endif +} + +SOKOL_API_IMPL bool sapp_keyboard_shown(void) { + return _sapp_tc.onscreen_keyboard_shown; +} + +SOKOL_API_IMPL bool sapp_is_fullscreen(void) { + return _sapp_tc.fullscreen; +} + +SOKOL_API_IMPL void sapp_toggle_fullscreen(void) { + #if defined(_SAPP_MACOS) + _sapp_tc_macos_toggle_fullscreen(); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_toggle_fullscreen(); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_toggle_fullscreen(); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_toggle_fullscreen(); + #endif +} + +_SOKOL_PRIVATE void _sapp_tc_update_cursor(sapp_mouse_cursor cursor, bool shown) { + #if defined(_SAPP_MACOS) + _sapp_tc_macos_update_cursor(cursor, shown); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_update_cursor(cursor, shown, false); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_update_cursor(cursor, shown); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_update_cursor(cursor, shown); + #endif + _sapp_tc.mouse.current_cursor = cursor; + _sapp_tc.mouse.shown = shown; +} + +/* NOTE that sapp_show_mouse() does not "stack" like the Win32 or macOS API functions! */ +SOKOL_API_IMPL void sapp_show_mouse(bool show) { + if (_sapp_tc.mouse.shown != show) { + _sapp_tc_update_cursor(_sapp_tc.mouse.current_cursor, show); + } +} + +SOKOL_API_IMPL bool sapp_mouse_shown(void) { + return _sapp_tc.mouse.shown; +} + +SOKOL_API_IMPL void sapp_lock_mouse(bool lock) { + #if defined(_SAPP_MACOS) + _sapp_tc_macos_lock_mouse(lock); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_lock_mouse(lock); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_lock_mouse(lock); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_lock_mouse(lock); + #else + _sapp_tc.mouse.locked = lock; + #endif +} + +SOKOL_API_IMPL bool sapp_mouse_locked(void) { + return _sapp_tc.mouse.locked; +} + +SOKOL_API_IMPL void sapp_set_mouse_cursor(sapp_mouse_cursor cursor) { + SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); + if (_sapp_tc.mouse.current_cursor != cursor) { + _sapp_tc_update_cursor(cursor, _sapp_tc.mouse.shown); + } +} + +SOKOL_API_IMPL sapp_mouse_cursor sapp_get_mouse_cursor(void) { + return _sapp_tc.mouse.current_cursor; +} + +SOKOL_API_IMPL sapp_mouse_cursor sapp_bind_mouse_cursor_image(sapp_mouse_cursor cursor, const sapp_image_desc* desc) { + SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); + // NOTE: It seems that for some reason, the hotspot doesn't work if it is one less + // than the dimension of the cursor image (or more), on windows. So for a cursor + // that is 32 by 32 px, a hotspot of x = 30 works, but not x = 31. + // The cursor simply dissapears in such cases. Asserting for all platforms to make + // the behaviour consistent. + SOKOL_ASSERT(desc->cursor_hotspot_x < desc->width - 1 && desc->cursor_hotspot_y < desc->height - 1); + SOKOL_ASSERT(desc->width * desc->height * 4 == (int) desc->pixels.size); + + sapp_unbind_mouse_cursor_image(cursor); + + bool res = false; + #if defined(_SAPP_MACOS) + res = _sapp_tc_macos_make_custom_mouse_cursor(cursor, desc); + #elif defined(_SAPP_EMSCRIPTEN) + res = _sapp_tc_emsc_make_custom_mouse_cursor(cursor, desc); + #elif defined(_SAPP_WIN32) + res = _sapp_tc_win32_make_custom_mouse_cursor(cursor, desc); + #elif defined(_SAPP_LINUX) + res = _sapp_tc_x11_make_custom_mouse_cursor(cursor, desc); + #else + _SOKOL_UNUSED(desc); + #endif + _sapp_tc.custom_cursor_bound[(int)cursor] = res; + + // Update the displayed cursor in case the current cursor is the one we just bound. + if (_sapp_tc.mouse.current_cursor == cursor) { + _sapp_tc_update_cursor(cursor, _sapp_tc.mouse.shown); + } + return cursor; // returning the passed-in cursor puerly for convenience, in case you want to asign the value to a variable. +} + +SOKOL_API_IMPL void sapp_unbind_mouse_cursor_image(sapp_mouse_cursor cursor) { + SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); + if (_sapp_tc.custom_cursor_bound[(int)cursor]) { + // if this is the active cursor, first restore it to its default image, + // this must be done before attempting to destroy any cursor image + // resources which at least on win32 would fail if the cursor is still in use + _sapp_tc.custom_cursor_bound[(int)cursor] = false; + if (_sapp_tc.mouse.current_cursor == cursor) { + _sapp_tc_update_cursor(cursor, _sapp_tc.mouse.shown); + } + #if defined(_SAPP_MACOS) + _sapp_tc_macos_destroy_custom_mouse_cursor(cursor); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_destroy_custom_mouse_cursor(cursor); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_destroy_custom_mouse_cursor(cursor); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_destroy_custom_mouse_cursor(cursor); + #endif + } +} + +SOKOL_API_IMPL void sapp_request_quit(void) { + _sapp_tc.quit_requested = true; +} + +SOKOL_API_IMPL void sapp_cancel_quit(void) { + _sapp_tc.quit_requested = false; +} + +SOKOL_API_IMPL void sapp_quit(void) { + _sapp_tc.quit_ordered = true; +} + +SOKOL_API_IMPL void sapp_consume_event(void) { + _sapp_tc.event_consumed = true; +} + +/* NOTE: on HTML5, sapp_set_clipboard_string() must be called from within event handler! */ +SOKOL_API_IMPL void sapp_set_clipboard_string(const char* str) { + if (!_sapp_tc.clipboard.enabled) { + return; + } + SOKOL_ASSERT(str); + #if defined(_SAPP_MACOS) + _sapp_tc_macos_set_clipboard_string(str); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_set_clipboard_string(str); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_set_clipboard_string(str); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_set_clipboard_string(str); + #else + /* not implemented */ + #endif + _sapp_tc_strcpy(str, _sapp_tc.clipboard.buffer, (size_t)_sapp_tc.clipboard.buf_size); +} + +SOKOL_API_IMPL const char* sapp_get_clipboard_string(void) { + if (!_sapp_tc.clipboard.enabled) { + return ""; + } + #if defined(_SAPP_MACOS) + return _sapp_tc_macos_get_clipboard_string(); + #elif defined(_SAPP_EMSCRIPTEN) + return _sapp_tc.clipboard.buffer; + #elif defined(_SAPP_WIN32) + return _sapp_tc_win32_get_clipboard_string(); + #elif defined(_SAPP_LINUX) + return _sapp_tc_x11_get_clipboard_string(); + #else + /* not implemented */ + return _sapp_tc.clipboard.buffer; + #endif +} + +SOKOL_API_IMPL void sapp_set_window_title(const char* title) { + SOKOL_ASSERT(title); + _sapp_tc_strcpy(title, _sapp_tc.window_title, sizeof(_sapp_tc.window_title)); + #if defined(_SAPP_MACOS) + _sapp_tc_macos_update_window_title(); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_update_window_title(); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_update_window_title(); + #endif +} + +SOKOL_API_IMPL void sapp_set_icon(const sapp_icon_desc* desc) { + SOKOL_ASSERT(desc); + if (desc->sokol_default) { + /* the sokol default-icon generator is not ported: the favicon is the + page's business unless the app provides real icon images */ + return; + } + const int num_images = _sapp_tc_icon_num_images(desc); + if (num_images == 0) { + return; + } + SOKOL_ASSERT((num_images > 0) && (num_images <= SAPP_MAX_ICONIMAGES)); + if (!_sapp_tc_validate_icon_desc(desc, num_images)) { + return; + } + #if defined(_SAPP_MACOS) + _sapp_tc_macos_set_icon(desc, num_images); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_set_icon(desc, num_images); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_set_icon(desc, num_images); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_set_icon(desc, num_images); + #endif +} + +SOKOL_API_IMPL int sapp_get_num_dropped_files(void) { + if (!_sapp_tc.drop.enabled) { + return 0; + } + return _sapp_tc.drop.num_files; +} + +SOKOL_API_IMPL const char* sapp_get_dropped_file_path(int index) { + SOKOL_ASSERT((index >= 0) && (index < _sapp_tc.drop.num_files)); + if (!_sapp_tc.drop.enabled) { + return ""; + } + SOKOL_ASSERT(_sapp_tc.drop.buffer); + if ((index < 0) || (index >= _sapp_tc.drop.max_files)) { + return ""; + } + return (const char*) _sapp_tc_dropped_file_path_ptr(index); +} + +SOKOL_API_IMPL uint32_t sapp_html5_get_dropped_file_size(int index) { + SOKOL_ASSERT((index >= 0) && (index < _sapp_tc.drop.num_files)); + #if defined(_SAPP_EMSCRIPTEN) + if (!_sapp_tc.drop.enabled) { + return 0; + } + return _sapp_tc_js_dropped_file_size(index); + #else + (void)index; + return 0; + #endif +} + +SOKOL_API_IMPL void sapp_html5_fetch_dropped_file(const sapp_html5_fetch_request* request) { + SOKOL_ASSERT(_sapp_tc.drop.enabled); + SOKOL_ASSERT(request); + SOKOL_ASSERT(request->callback); + SOKOL_ASSERT(request->buffer.ptr); + SOKOL_ASSERT(request->buffer.size > 0); + #if defined(_SAPP_EMSCRIPTEN) + const int index = request->dropped_file_index; + sapp_html5_fetch_error error_code = SAPP_HTML5_FETCH_ERROR_NO_ERROR; + if ((index < 0) || (index >= _sapp_tc.drop.num_files)) { + error_code = SAPP_HTML5_FETCH_ERROR_OTHER; + } + if (sapp_html5_get_dropped_file_size(index) > request->buffer.size) { + error_code = SAPP_HTML5_FETCH_ERROR_BUFFER_TOO_SMALL; + } + if (SAPP_HTML5_FETCH_ERROR_NO_ERROR != error_code) { + _sapp_tc_emsc_invoke_fetch_cb(index, + false, // success + (int)error_code, + request->callback, + 0, // fetched_size + (void*)request->buffer.ptr, + request->buffer.size, + request->user_data); + } else { + _sapp_tc_js_fetch_dropped_file(index, + request->callback, + (void*)request->buffer.ptr, + request->buffer.size, + request->user_data); + } + #else + (void)request; + #endif +} + +SOKOL_API_IMPL sapp_environment sapp_get_environment(void) { + SOKOL_ASSERT(_sapp_tc.valid); + _SAPP_STRUCT(sapp_environment, res); + res.defaults.color_format = sapp_color_format(); + res.defaults.depth_format = sapp_depth_format(); + res.defaults.sample_count = sapp_sample_count(); + #if defined(SOKOL_METAL) + #if defined(_SAPP_MACOS) + res.metal.device = (__bridge const void*) _sapp_tc.macos.mtl.device; + #else + res.metal.device = (__bridge const void*) _sapp_tc.ios.mtl.device; + #endif + #endif + #if defined(SOKOL_D3D11) + res.d3d11.device = (const void*) _sapp_tc.d3d11.device; + res.d3d11.device_context = (const void*) _sapp_tc.d3d11.device_context; + #endif + #if defined(SOKOL_WGPU) + res.wgpu.device = (const void*) _sapp_tc.wgpu.device; + #endif + #if defined(SOKOL_VULKAN) + res.vulkan.instance = (const void*) _sapp_tc.vk.instance; + res.vulkan.physical_device = (const void*) _sapp_tc.vk.physical_device; + res.vulkan.device = (const void*) _sapp_tc.vk.device; + res.vulkan.queue = (const void*) _sapp_tc.vk.queue; + res.vulkan.queue_family_index = _sapp_tc.vk.queue_family_index; + #endif + return res; +} + +SOKOL_API_IMPL sapp_swapchain sapp_get_swapchain(void) { + SOKOL_ASSERT(_sapp_tc.valid); + _SAPP_STRUCT(sapp_swapchain, res); + #if defined(SOKOL_METAL) + #if defined(_SAPP_MACOS) + res.metal.current_drawable = (__bridge const void*) _sapp_tc_macos_mtl_swapchain_next(); + res.metal.depth_stencil_texture = (__bridge const void*) _sapp_tc.macos.mtl.depth_tex; + res.metal.msaa_color_texture = (__bridge const void*) _sapp_tc.macos.mtl.msaa_tex; + #else + res.metal.current_drawable = (__bridge const void*) _sapp_tc_ios_mtl_swapchain_next(); + res.metal.depth_stencil_texture = (__bridge const void*) _sapp_tc.ios.mtl.depth_tex; + res.metal.msaa_color_texture = (__bridge const void*) _sapp_tc.ios.mtl.msaa_tex; + #endif + #endif + #if defined(SOKOL_D3D11) + SOKOL_ASSERT(_sapp_tc.d3d11.rtv); + if (_sapp_tc.sample_count > 1) { + SOKOL_ASSERT(_sapp_tc.d3d11.msaa_rtv); + res.d3d11.render_view = (const void*) _sapp_tc.d3d11.msaa_rtv; + res.d3d11.resolve_view = (const void*) _sapp_tc.d3d11.rtv; + } else { + res.d3d11.render_view = (const void*) _sapp_tc.d3d11.rtv; + } + res.d3d11.depth_stencil_view = (const void*) _sapp_tc.d3d11.dsv; + #endif + #if defined(SOKOL_WGPU) + SOKOL_ASSERT(0 == _sapp_tc.wgpu.swapchain_view); + _sapp_tc_wgpu_swapchain_next(); + // FIXME: swapchain_view being null must be allowed and should skip the frame + SOKOL_ASSERT(_sapp_tc.wgpu.swapchain_view); + if (_sapp_tc.sample_count > 1) { + SOKOL_ASSERT(_sapp_tc.wgpu.msaa_view); + res.wgpu.render_view = (const void*) _sapp_tc.wgpu.msaa_view; + res.wgpu.resolve_view = (const void*) _sapp_tc.wgpu.swapchain_view; + } else { + res.wgpu.render_view = (const void*) _sapp_tc.wgpu.swapchain_view; + } + res.wgpu.depth_stencil_view = (const void*) _sapp_tc.wgpu.depth_stencil_view; + #endif + #if defined(SOKOL_VULKAN) + _sapp_tc_vk_swapchain_next(); + // FIXME: swapchain_view being null must be allowed and should skip the frame + uint32_t img_idx = _sapp_tc.vk.cur_swapchain_image_index; + if (_sapp_tc.sample_count > 1) { + SOKOL_ASSERT(_sapp_tc.vk.msaa.img && _sapp_tc.vk.msaa.view); + res.vulkan.render_image = (const void*) _sapp_tc.vk.msaa.img; + res.vulkan.render_view = (const void*) _sapp_tc.vk.msaa.view; + res.vulkan.resolve_image = (const void*) _sapp_tc.vk.swapchain_images[img_idx]; + res.vulkan.resolve_view = (const void*) _sapp_tc.vk.swapchain_views[img_idx]; + } else { + res.vulkan.render_image = (const void*) _sapp_tc.vk.swapchain_images[img_idx]; + res.vulkan.render_view = (const void*) _sapp_tc.vk.swapchain_views[img_idx]; + } + res.vulkan.depth_stencil_image = (const void*) _sapp_tc.vk.depth.img; + res.vulkan.depth_stencil_view = (const void*) _sapp_tc.vk.depth.view; + // NOTE: using the current swapchain image index here is *NOT* a bug! The render_finished_semaphore *must* + // be associated with its swapchain image in case the swapchain implementation doesn't return swapchain images in order + res.vulkan.render_finished_semaphore = _sapp_tc.vk.sync[img_idx].render_finished_sem; + res.vulkan.present_complete_semaphore = _sapp_tc.vk.sync[_sapp_tc.vk.sync_slot].present_complete_sem; + #endif + #if defined(_SAPP_ANY_GL) + res.gl.framebuffer = _sapp_tc.gl.framebuffer; + #endif + res.width = sapp_width(); + res.height = sapp_height(); + res.color_format = sapp_color_format(); + res.depth_format = sapp_depth_format(); + res.sample_count = sapp_sample_count(); + return res; +} + +SOKOL_API_IMPL const void* sapp_macos_get_window(void) { + #if defined(_SAPP_MACOS) + const void* obj = (__bridge const void*) _sapp_tc.macos.window; + SOKOL_ASSERT(obj); + return obj; + #else + return 0; + #endif +} + +SOKOL_API_IMPL const void* sapp_ios_get_window(void) { + #if defined(_SAPP_IOS) + const void* obj = (__bridge const void*) _sapp_tc.ios.window; + SOKOL_ASSERT(obj); + return obj; + #else + return 0; + #endif +} + +// Modified by tettou771 for TrussC: runtime orientation control +SOKOL_API_IMPL void sapp_ios_set_supported_orientations(uint32_t mask) { + #if defined(_SAPP_IOS) + _sapp_tc.ios.supported_orientations = (NSUInteger)mask; + #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 160000 + if (@available(iOS 16.0, *)) { + [_sapp_tc.ios.view_ctrl setNeedsUpdateOfSupportedInterfaceOrientations]; + } + #endif + #else + (void)mask; + #endif +} + +SOKOL_API_IMPL const void* sapp_d3d11_get_swap_chain(void) { + SOKOL_ASSERT(_sapp_tc.valid); +#if defined(SOKOL_D3D11) + return _sapp_tc.d3d11.swap_chain; +#else + return 0; +#endif +} + +SOKOL_API_IMPL const void* sapp_win32_get_hwnd(void) { + SOKOL_ASSERT(_sapp_tc.valid); + #if defined(_SAPP_WIN32) + return _sapp_tc.win32.hwnd; + #else + return 0; + #endif +} + +SOKOL_API_IMPL int sapp_gl_get_major_version(void) { + SOKOL_ASSERT(_sapp_tc.valid); + #if defined(_SAPP_ANY_GL) + return _sapp_tc.desc.gl.major_version; + #else + return 0; + #endif +} + +SOKOL_API_IMPL int sapp_gl_get_minor_version(void) { + SOKOL_ASSERT(_sapp_tc.valid); + #if defined(_SAPP_ANY_GL) + return _sapp_tc.desc.gl.minor_version; + #else + return 0; + #endif +} + +SOKOL_API_IMPL bool sapp_gl_is_gles(void) { + #if defined(SOKOL_GLES3) + return true; + #else + return false; + #endif +} + +SOKOL_API_IMPL const void* sapp_x11_get_window(void) { + #if defined(_SAPP_LINUX) + return (void*)_sapp_tc.x11.window; + #else + return 0; + #endif +} + +SOKOL_API_IMPL const void* sapp_x11_get_display(void) { + #if defined(_SAPP_LINUX) + return (void*)_sapp_tc.x11.display; + #else + return 0; + #endif +} + +SOKOL_API_IMPL const void* sapp_android_get_native_activity(void) { + // NOTE: _sapp_tc.valid is not asserted here because sapp_android_get_native_activity() + // needs to be callable from within sokol_main() (see: https://github.com/floooh/sokol/issues/708) + #if defined(_SAPP_ANDROID) + return (void*)_sapp_tc.android.activity; + #else + return 0; + #endif +} + +SOKOL_API_IMPL void sapp_html5_ask_leave_site(bool ask) { + _sapp_tc.html5_ask_leave_site = ask; +} + +// Modified by tettou771 for TrussC: skip the next present call (fix D3D11 flickering) +SOKOL_API_IMPL void sapp_skip_present(void) { + _sapp_tc.skip_present = true; +} +// end of modification + + +/* ---- multi-window API: explicit platform gap on the web ------------------ + the whole backend binds to ONE element (Module.sapp_emsc_target); + a second window is not representable in a browser tab */ +sapp_window sapp_create_window(const sapp_window_desc* desc) { + _SOKOL_UNUSED(desc); + fprintf(stderr, "sokol_app_tc.h: sapp_create_window() is not supported on the web (single-canvas platform)\n"); + sapp_window w = {0}; + return w; +} +void sapp_destroy_window(sapp_window win) { _SOKOL_UNUSED(win); } +bool sapp_window_valid(sapp_window win) { _SOKOL_UNUSED(win); return false; } +int sapp_window_width(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +int sapp_window_height(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +int sapp_window_framebuffer_width(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +int sapp_window_framebuffer_height(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +float sapp_window_dpi_scale(sapp_window win) { _SOKOL_UNUSED(win); return 1.0f; } +int sapp_window_sample_count(sapp_window win) { _SOKOL_UNUSED(win); return 1; } +bool sapp_window_occluded(sapp_window win) { _SOKOL_UNUSED(win); return false; } +void sapp_window_set_title(sapp_window win, const char* title) { _SOKOL_UNUSED(win); _SOKOL_UNUSED(title); } +double sapp_window_frame_duration(sapp_window win) { _SOKOL_UNUSED(win); return 1.0 / 60.0; } +const void* sapp_window_metal_current_drawable(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_metal_depth_stencil_texture(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_metal_msaa_color_texture(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_d3d11_render_view(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_d3d11_resolve_view(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_d3d11_depth_stencil_view(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_macos_get_window(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_win32_get_hwnd(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_x11_get_window(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +uint32_t sapp_window_gl_framebuffer(sapp_window win) { _SOKOL_UNUSED(win); return 0; } + +/* ---- other-backend getters (zero stubs, same set as the native sections) - */ +const void* sapp_metal_get_device(void) { return 0; } +const void* sapp_metal_get_current_drawable(void) { return 0; } +const void* sapp_metal_get_depth_stencil_texture(void) { return 0; } +const void* sapp_metal_get_msaa_color_texture(void) { return 0; } +const void* sapp_d3d11_get_device(void) { return 0; } +const void* sapp_d3d11_get_device_context(void) { return 0; } + +#else /* other platforms */ +/*== stubs (explicit platform gap; the mobile ports replace these) ==========*/ +#if defined(__cplusplus) +extern "C" { +#endif +sapp_window sapp_create_window(const sapp_window_desc* desc) { (void)desc; sapp_window w = {0}; return w; } +void sapp_destroy_window(sapp_window win) { (void)win; } +bool sapp_window_valid(sapp_window win) { (void)win; return false; } +int sapp_window_width(sapp_window win) { (void)win; return 0; } +int sapp_window_height(sapp_window win) { (void)win; return 0; } +int sapp_window_framebuffer_width(sapp_window win) { (void)win; return 0; } +int sapp_window_framebuffer_height(sapp_window win) { (void)win; return 0; } +float sapp_window_dpi_scale(sapp_window win) { (void)win; return 1.0f; } +int sapp_window_sample_count(sapp_window win) { (void)win; return 1; } +bool sapp_window_occluded(sapp_window win) { (void)win; return false; } +void sapp_window_set_title(sapp_window win, const char* title) { (void)win; (void)title; } +double sapp_window_frame_duration(sapp_window win) { (void)win; return 1.0 / 60.0; } +const void* sapp_window_metal_current_drawable(sapp_window win) { (void)win; return 0; } +const void* sapp_window_metal_depth_stencil_texture(sapp_window win) { (void)win; return 0; } +const void* sapp_window_metal_msaa_color_texture(sapp_window win) { (void)win; return 0; } +const void* sapp_window_d3d11_render_view(sapp_window win) { (void)win; return 0; } +const void* sapp_window_d3d11_resolve_view(sapp_window win) { (void)win; return 0; } +const void* sapp_window_d3d11_depth_stencil_view(sapp_window win) { (void)win; return 0; } +const void* sapp_window_macos_get_window(sapp_window win) { (void)win; return 0; } +const void* sapp_window_win32_get_hwnd(sapp_window win) { (void)win; return 0; } +const void* sapp_window_x11_get_window(sapp_window win) { (void)win; return 0; } +uint32_t sapp_window_gl_framebuffer(sapp_window win) { (void)win; return 0; } +#if defined(__cplusplus) +} /* extern "C" */ +#endif +#endif /* platform select */ + +#endif /* SOKOL_APP_TC_IMPL_INCLUDED */ + +/* + zlib/libpng license + + Copyright (c) 2018 Andre Weissflog + Copyright (c) 2026 tettou771 (multi-window extension) + + This software is provided 'as-is', without any express or implied warranty. + In no event will the authors be held liable for any damages arising from the + use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software in a + product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source + distribution. +*/ diff --git a/core/include/sokol/sokol_gfx.h b/core/include/sokol/sokol_gfx.h index d91d09de..497e8c4f 100644 --- a/core/include/sokol/sokol_gfx.h +++ b/core/include/sokol/sokol_gfx.h @@ -125,7 +125,7 @@ the sokol_glue.h header: #include "sokol_gfx.h" - #include "sokol_app.h" + #include "sokol_app_tc.h" #include "sokol_glue.h" //... sg_setup(&(sg_desc){ diff --git a/core/include/tc/3d/tcEasyCam.h b/core/include/tc/3d/tcEasyCam.h index 86e24859..1d0d8344 100644 --- a/core/include/tc/3d/tcEasyCam.h +++ b/core/include/tc/3d/tcEasyCam.h @@ -43,9 +43,9 @@ class EasyCam { // the format-correct one; no per-site swapchain/FBO branch. internal::loadPipeline(internal::active3D()); - float dpiScale = sapp_dpi_scale(); - float w = (float)sapp_width() / dpiScale; - float h = (float)sapp_height() / dpiScale; + float dpiScale = getDpiScale(); + float w = (float)getFramebufferWidth() / dpiScale; + float h = (float)getFramebufferHeight() / dpiScale; float aspect = w / h; // Camera basis from orientation quaternion (no gimbal lock / no pole @@ -99,10 +99,10 @@ class EasyCam { Mat4 view = Mat4::lookAt(eye, target_, camUp); // Save for worldToScreen/screenToWorld - internal::currentProjectionMatrix = projection; - internal::currentViewMatrix = view; - internal::currentViewW = w; - internal::currentViewH = h; + internal::currentWindowContext().currentProjectionMatrix = projection; + internal::currentWindowContext().currentViewMatrix = view; + internal::currentWindowContext().currentViewW = w; + internal::currentWindowContext().currentViewH = h; // Register this camera scope: nodes drawn between begin()/end() stamp // it, so mouse picking unprojects through THIS camera for them. @@ -294,6 +294,13 @@ class EasyCam { // --------------------------------------------------------------------------- // Mouse input (auto-subscribe to events) + // + // Multi-window contract: the listeners bind to the CURRENT window's event + // streams at the moment enableMouseInput() is called (events() resolves + // per window). Call it from within the lifecycle of the App that drives + // the camera's window -- setup()/update()/draw() of a secondary window's + // App binds to THAT window; calling it from the main App and then using + // the camera in a secondary window would listen to the wrong stream. // --------------------------------------------------------------------------- void enableMouseInput() { diff --git a/core/include/tc/3d/tcEnvironment.h b/core/include/tc/3d/tcEnvironment.h index cf8f2582..4714d156 100644 --- a/core/include/tc/3d/tcEnvironment.h +++ b/core/include/tc/3d/tcEnvironment.h @@ -55,7 +55,7 @@ class Environment { // Load an equirectangular HDR image from disk and bake all IBL maps. // Returns false if the file cannot be loaded. - bool loadFromHDR(const std::string& path) { + bool loadFromHDR(const fs::path& path) { Pixels src; if (!src.loadHDR(path)) { logError("Environment") << "loadHDR failed: " << path; diff --git a/core/include/tc/3d/tcIesProfile.h b/core/include/tc/3d/tcIesProfile.h index b52fbc59..e91e76c8 100644 --- a/core/include/tc/3d/tcIesProfile.h +++ b/core/include/tc/3d/tcIesProfile.h @@ -59,8 +59,8 @@ class IesProfile { // Load from file path (.ies). Path is resolved via getDataPath() // (relative to the project data/ directory). - bool load(const std::string& path) { - std::string resolved = getDataPath(path); + bool load(const fs::path& path) { + fs::path resolved = getDataPath(path); std::ifstream ifs(resolved); if (!ifs.is_open()) { logWarning() << "[IesProfile] cannot open: " << resolved; diff --git a/core/include/tc/3d/tcMeshPbrPipeline.h b/core/include/tc/3d/tcMeshPbrPipeline.h index 964c4e63..912afab2 100644 --- a/core/include/tc/3d/tcMeshPbrPipeline.h +++ b/core/include/tc/3d/tcMeshPbrPipeline.h @@ -111,7 +111,7 @@ class PbrPipeline { // SG_PIXELFORMAT_NONE (1) は「カラーアタッチメントなし」なので使わない sg_pixel_format colorFmt; int sampleCount; - if (internal::inFboPass) { + if (internal::currentWindowContext().inFboPass) { colorFmt = internal::currentFboColorFormat; sampleCount = internal::currentFboSampleCount; } else { @@ -234,7 +234,7 @@ class PbrPipeline { // TrussC Mat4 is row-major; GLSL mat4 is column-major. Transpose the // storage before upload so shader can use the conventional `model * v`. Mat4 modelT = getDefaultContext().getMatrix().transposed(); - Mat4 viewProj = internal::currentProjectionMatrix * internal::currentViewMatrix; + Mat4 viewProj = internal::currentWindowContext().currentProjectionMatrix * internal::currentWindowContext().currentViewMatrix; Mat4 viewProjT = viewProj.transposed(); // For now, normal matrix is just the model matrix. This is correct for // rotations and uniform scale; non-uniform scale would need @@ -366,7 +366,7 @@ class PbrPipeline { // shaders use). flushDeferredShaderDraws() replays it per layer. PbrDrawCommand cmd{ pip, bind, vsp, fsp, mesh.getGpuIndexCount(), mesh.getGpuVertexCount() }; - if (internal::inFboPass) { + if (internal::currentWindowContext().inFboPass) { // Defer (like the swapchain path) into the per-FBO list; flushed // per-layer in Fbo::end(). Bump the FBO layer so 2D drawn after this // mesh composites on top of it, matching the swapchain ordering. @@ -487,10 +487,10 @@ class PbrPipeline { ensureShadowTexture(light.getShadowResolution()); // Suspend swapchain pass if active - if (internal::inSwapchainPass) { + if (internal::currentWindowContext().inSwapchainPass) { sgl_draw(); sg_end_pass(); - internal::inSwapchainPass = false; + internal::currentWindowContext().inSwapchainPass = false; } // Compute light VP matrix (reuse spot projector VP) diff --git a/core/include/tc/3d/tcMeshPointPipeline.h b/core/include/tc/3d/tcMeshPointPipeline.h index 78f8172d..0297adfc 100644 --- a/core/include/tc/3d/tcMeshPointPipeline.h +++ b/core/include/tc/3d/tcMeshPointPipeline.h @@ -152,7 +152,7 @@ class PointPipeline { sg_pixel_format colorFmt; int sampleCount; - if (internal::inFboPass) { + if (internal::currentWindowContext().inFboPass) { colorFmt = internal::currentFboColorFormat; sampleCount = internal::currentFboSampleCount; } else { @@ -181,7 +181,7 @@ class PointPipeline { // after this mesh composites on top. Inside an FBO pass we defer into the // per-FBO list (flushed in flushFboDeferredPbr); otherwise into the // swapchain list (flushed in flushDeferredShaderDraws). - if (internal::inFboPass) { + if (internal::currentWindowContext().inFboPass) { internal::fboPointDraws.push_back({ internal::fboLayerNext, cmd }); internal::fboLayerNext++; sgl_layer(internal::fboLayerNext); @@ -201,7 +201,7 @@ class PointPipeline { // TrussC Mat4 is row-major; GLSL mat4 is column-major. Transpose before // upload so the shader can use the conventional `viewProj * v`. - Mat4 viewProj = (internal::currentProjectionMatrix * internal::currentViewMatrix).transposed(); + Mat4 viewProj = (internal::currentWindowContext().currentProjectionMatrix * internal::currentWindowContext().currentViewMatrix).transposed(); std::memcpy(vsp.viewProj, viewProj.m, sizeof(vsp.viewProj)); const int w = getWindowWidth(); diff --git a/core/include/tc/app/tcGlobal.cpp b/core/include/tc/app/tcGlobal.cpp index a88999f8..39ee93bc 100644 --- a/core/include/tc/app/tcGlobal.cpp +++ b/core/include/tc/app/tcGlobal.cpp @@ -68,8 +68,8 @@ void setup() { // RenderTarget: the swapchain target uses the default sgl context (carries the // swapchain color/depth format + sample count). Pipelines build lazily on first use. - internal::swapchainTarget.context = sgl_default_context(); - internal::swapchainTarget.isFbo = false; + internal::currentWindowContext().swapchainTarget.context = sgl_default_context(); + internal::currentWindowContext().swapchainTarget.isFbo = false; // Initialize bitmap font sampler + pipeline. The atlas TEXTURE itself is // allocated lazily on first drawBitmapString call (see ensureFontAtlas in @@ -180,10 +180,10 @@ void resizeSgl(int newMaxVertices, int newMaxCommands) { // corrupts the frame (the same reason we pre-warm at setup). Warm against the // swapchain target explicitly in case a resize ever fires outside a swapchain // context. - swapchainTarget.context = sgl_default_context(); - swapchainTarget.cache.clear(); - RenderTarget* prevTarget = currentTarget; - currentTarget = &swapchainTarget; + internal::currentWindowContext().swapchainTarget.context = sgl_default_context(); + internal::currentWindowContext().swapchainTarget.cache.clear(); + RenderTarget* prevTarget = internal::currentWindowContext().currentTarget; + internal::currentWindowContext().currentTarget = &internal::currentWindowContext().swapchainTarget; active2D(BlendMode::Alpha); active2D(BlendMode::Add); active2D(BlendMode::Multiply); @@ -193,7 +193,7 @@ void resizeSgl(int newMaxVertices, int newMaxCommands) { activePremult(); activeClear(); active3D(); - currentTarget = prevTarget; + internal::currentWindowContext().currentTarget = prevTarget; // NOTE: each FBO's RenderTarget cache (and its sgl context) is also invalidated // by sgl_shutdown but is NOT rebuilt here — FBOs surviving an sgl buffer resize // is a pre-existing limitation, out of scope for this refactor. @@ -210,12 +210,12 @@ void clear(float r, float g, float b, float a /* = 1.0f */) { // Skip in headless mode (no graphics context) if (headless::isActive()) return; - if (internal::inFboPass && internal::fboClearColorFunc) { + if (internal::currentWindowContext().inFboPass && internal::fboClearColorFunc) { // During FBO pass, call FBO's clearColor() to restart pass internal::fboClearColorFunc(r, g, b, a); - } else if (internal::inSwapchainPass) { + } else if (internal::currentWindowContext().inSwapchainPass) { // During swapchain pass - BlendMode prevBlendMode = internal::currentBlendMode; + BlendMode prevBlendMode = internal::currentWindowContext().currentBlendMode; sgl_push_matrix(); sgl_matrix_mode_projection(); @@ -247,7 +247,7 @@ void clear(float r, float g, float b, float a /* = 1.0f */) { // Save clear color — the pass will start in present() with this value. // sgl commands accumulate without an active pass, so FBOs can render // in their own passes without interrupting the swapchain pass. - internal::swapchainClearValue = { r, g, b, a }; + internal::currentWindowContext().swapchainClearValue = { r, g, b, a }; } } @@ -255,18 +255,22 @@ void clear(float r, float g, float b, float a /* = 1.0f */) { // パス管理関数(non-inline: Hot Reload時にHost/Guest間で同じ状態を参照するため) // --------------------------------------------------------------------------- void ensureSwapchainPass() { - if (!internal::inSwapchainPass && !internal::inFboPass) { + if (!internal::currentWindowContext().inSwapchainPass && !internal::currentWindowContext().inFboPass) { sg_pass pass = {}; pass.action.colors[0].load_action = SG_LOADACTION_CLEAR; - pass.action.colors[0].clear_value = internal::swapchainClearValue; + pass.action.colors[0].clear_value = internal::currentWindowContext().swapchainClearValue; pass.action.depth.load_action = SG_LOADACTION_CLEAR; pass.action.depth.clear_value = 1.0f; - pass.swapchain = sglue_swapchain(); + auto& ctx = internal::currentWindowContext(); + // Secondary windows provide their own swapchain (same drawable for the + // whole window frame); the main window uses sokol_glue as before. + pass.swapchain = ctx.acquireSwapchain ? ctx.acquireSwapchain(ctx.acquireSwapchainUser) + : sglue_swapchain(); // Record the drawable we render into so end-of-frame capture reads back // THIS one (sapp_get_swapchain() advances the Metal drawable per call). - internal::lastSwapchainDrawable = pass.swapchain.metal.current_drawable; + ctx.lastSwapchainDrawable = pass.swapchain.metal.current_drawable; sg_begin_pass(&pass); - internal::inSwapchainPass = true; + ctx.inSwapchainPass = true; } } @@ -292,32 +296,34 @@ void present() { } sg_end_pass(); - internal::inSwapchainPass = false; + internal::currentWindowContext().inSwapchainPass = false; sg_commit(); } bool isInSwapchainPass() { - return internal::inSwapchainPass; + return internal::currentWindowContext().inSwapchainPass; } void suspendSwapchainPass() { - if (internal::inSwapchainPass) { + if (internal::currentWindowContext().inSwapchainPass) { sg_end_pass(); - internal::inSwapchainPass = false; + internal::currentWindowContext().inSwapchainPass = false; } } void resumeSwapchainPass() { - if (!internal::inSwapchainPass) { + if (!internal::currentWindowContext().inSwapchainPass) { sg_pass pass = {}; pass.action.colors[0].load_action = SG_LOADACTION_CLEAR; - pass.action.colors[0].clear_value = internal::swapchainClearValue; + pass.action.colors[0].clear_value = internal::currentWindowContext().swapchainClearValue; pass.action.depth.load_action = SG_LOADACTION_CLEAR; pass.action.depth.clear_value = 1.0f; - pass.swapchain = sglue_swapchain(); - internal::lastSwapchainDrawable = pass.swapchain.metal.current_drawable; + auto& ctx = internal::currentWindowContext(); + pass.swapchain = ctx.acquireSwapchain ? ctx.acquireSwapchain(ctx.acquireSwapchainUser) + : sglue_swapchain(); + ctx.lastSwapchainDrawable = pass.swapchain.metal.current_drawable; sg_begin_pass(&pass); - internal::inSwapchainPass = true; + ctx.inSwapchainPass = true; } } @@ -325,9 +331,46 @@ void resumeSwapchainPass() { // Non-inline singletons / accessors (Hot Reload: Host/Guest share state) // --------------------------------------------------------------------------- +namespace internal { +// The main window's state container. Non-inline so a hot-reload guest binds +// to the host's instance (same pattern as events()/getDefaultContext()). +WindowContext& mainWindowContext() { + static WindowContext ctx; + return ctx; +} + +// Open-window registry (see tcWindow.h). Non-inline for the same host/guest +// reason as mainWindowContext(). Main thread only. +static std::vector& windowRegistryStorage() { + static std::vector list; + return list; +} +void registerWindow(Window* w) { + windowRegistryStorage().push_back(w); +} +void unregisterWindow(Window* w) { + auto& list = windowRegistryStorage(); + list.erase(std::remove(list.begin(), list.end(), w), list.end()); +} +std::vector openWindows() { + std::vector out; + for (Window* w : windowRegistryStorage()) { + if (w && w->isOpen()) out.push_back(w); + } + return out; +} +} // namespace internal + CoreEvents& events() { - static CoreEvents instance; - return instance; + // The CoreEvents instance is owned per window context. The main window's + // is wired lazily here (preserving the pre-context construction timing); + // Phase 1 pre-wires the pointer when constructing secondary contexts. + auto& ctx = internal::currentWindowContext(); + if (!ctx.coreEvents) { + static CoreEvents instance; // main-window storage + ctx.coreEvents = &instance; + } + return *ctx.coreEvents; } double getElapsedTime() { @@ -354,24 +397,27 @@ uint64_t getFrameCount() { } double getDeltaTime() { - return internal::updateDeltaTime; + // Per-window: inside a secondary window's update/draw this is that + // window's own tick delta, not the main window's. + return internal::currentWindowContext().updateDeltaTime; } double getFrameRate() { - double dt = internal::updateDeltaTime; + auto& ctx = internal::currentWindowContext(); + double dt = ctx.updateDeltaTime; if (dt <= 0.0) return 0.0; - internal::frameTimeBuffer[internal::frameTimeIndex] = dt; - internal::frameTimeIndex = (internal::frameTimeIndex + 1) % 10; - if (internal::frameTimeIndex == 0) { - internal::frameTimeBufferFilled = true; + ctx.frameTimeBuffer[ctx.frameTimeIndex] = dt; + ctx.frameTimeIndex = (ctx.frameTimeIndex + 1) % 10; + if (ctx.frameTimeIndex == 0) { + ctx.frameTimeBufferFilled = true; } - int count = internal::frameTimeBufferFilled ? 10 : internal::frameTimeIndex; + int count = ctx.frameTimeBufferFilled ? 10 : ctx.frameTimeIndex; if (count == 0) return 0.0; double sum = 0.0; for (int i = 0; i < count; i++) { - sum += internal::frameTimeBuffer[i]; + sum += ctx.frameTimeBuffer[i]; } double avgDt = sum / count; return avgDt > 0.0 ? 1.0 / avgDt : 0.0; diff --git a/core/include/tc/app/tcHotReloadHost.h b/core/include/tc/app/tcHotReloadHost.h index dd9eb7d5..e6dab668 100644 --- a/core/include/tc/app/tcHotReloadHost.h +++ b/core/include/tc/app/tcHotReloadHost.h @@ -207,7 +207,7 @@ inline string findCMake(const string& buildDir = "") { // On Windows, PATH may resolve to an older VS cmake that doesn't // know the generator used by the current build (e.g. "Visual Studio 18 2026"). if (!buildDir.empty()) { - string cachePath = buildDir + "/CMakeCache.txt"; + fs::path cachePath = fs::path(buildDir) / "CMakeCache.txt"; if (fs::exists(cachePath)) { ifstream cache(cachePath); string line; @@ -554,7 +554,7 @@ inline int runHotReloadApp(const WindowSettings& settings) { events().update.notify(); App* app = g_host.getApp(); if (app) { - app->handleUpdate(internal::mouseX, internal::mouseY); + app->handleUpdate(internal::currentWindowContext().mouseX, internal::currentWindowContext().mouseY); } }; @@ -638,7 +638,7 @@ inline int runHotReloadApp(const WindowSettings& settings) { desc.max_dropped_file_path_length = 2048; desc.enable_clipboard = true; desc.clipboard_size = settings.clipboardSize; - internal::clipboardSize = settings.clipboardSize; + internal::currentWindowContext().clipboardSize = settings.clipboardSize; sapp_run(&desc); return 0; diff --git a/core/include/tc/app/tcMouseGlobal.h b/core/include/tc/app/tcMouseGlobal.h index f85cd113..0a82f3df 100644 --- a/core/include/tc/app/tcMouseGlobal.h +++ b/core/include/tc/app/tcMouseGlobal.h @@ -7,20 +7,16 @@ // TrussC.h so lower-level headers (e.g. tcNode.h, which converts global mouse // coords to node-local) can depend on them directly, in dependency order, // instead of pulling the whole umbrella. Apps include ; the framework -// event loop writes internal::mouseX/Y each frame. This is an internal piece. +// event loop writes internal::currentWindowContext().mouseX/Y each frame. This is an internal piece. // ============================================================================= #include "tcMath.h" // Vec2 namespace trussc { -namespace internal { - // Mouse state (window coordinates), written by the framework event loop. - inline float mouseX = 0.0f; - inline float mouseY = 0.0f; - inline float pmouseX = 0.0f; // Previous frame mouse position - inline float pmouseY = 0.0f; -} +// Mouse position state (mouseX/Y, pmouseX/Y) lives in WindowContext +// (tc/app/tcWindowContext.h, included from TrussC.h before this header); +// the framework event loop writes the current window's members each frame. // --------------------------------------------------------------------------- // Mouse state (global / window coordinates) @@ -28,28 +24,28 @@ namespace internal { // Current mouse X coordinate (window coordinates) inline float getGlobalMouseX() { - return internal::mouseX; + return internal::currentWindowContext().mouseX; } // Current mouse Y coordinate (window coordinates) inline float getGlobalMouseY() { - return internal::mouseY; + return internal::currentWindowContext().mouseY; } // Previous frame mouse X coordinate (window coordinates) inline float getGlobalPMouseX() { - return internal::pmouseX; + return internal::currentWindowContext().pmouseX; } // Previous frame mouse Y coordinate (window coordinates) inline float getGlobalPMouseY() { - return internal::pmouseY; + return internal::currentWindowContext().pmouseY; } // Alias for getGlobalMouseX/Y (for tcDebugInput) -inline float getMouseX() { return internal::mouseX; } -inline float getMouseY() { return internal::mouseY; } -inline Vec2 getMousePos() { return Vec2(internal::mouseX, internal::mouseY); } +inline float getMouseX() { return getGlobalMouseX(); } +inline float getMouseY() { return getGlobalMouseY(); } +inline Vec2 getMousePos() { return Vec2(getGlobalMouseX(), getGlobalMouseY()); } inline Vec2 getGlobalMousePos() { return Vec2(getGlobalMouseX(), getGlobalMouseY()); } } // namespace trussc diff --git a/core/include/tc/app/tcWindow.h b/core/include/tc/app/tcWindow.h new file mode 100644 index 00000000..dc1d15d9 --- /dev/null +++ b/core/include/tc/app/tcWindow.h @@ -0,0 +1,186 @@ +#pragma once + +// ============================================================================= +// tcWindow.h - Secondary application windows (multi-window, Phase 1) +// ============================================================================= +// +// One App, many windows: the main window keeps the classic implicit lifecycle +// (runApp / update / draw), a secondary Window owns its own Node tree, events +// and render state (WindowContext) and is driven by its display's own vsync +// (macOS: one CADisplayLink per window, delivered on the main run loop; +// Windows: one DXGI frame latency waitable object per swapchain, serviced by +// the message loop; Linux: timer-paced ticks at the measured refresh rate, +// serviced by the X11 event loop). Everything runs synchronously on the main +// thread; a fully occluded window simply skips rendering (fps 0), so it can +// never stall the others. +// +// GPU resources (Texture / Fbo / Mesh / Font) live in the single shared +// sokol_gfx context and can be used freely from any window. +// +// Platform support: macOS, Windows and Linux (X11). On other platforms +// createWindow() logs an error and returns nullptr. +// +// This file is included from TrussC.h (after Node / CoreEvents / WindowSettings). + +namespace trussc { + +class Window; + +namespace internal { +// Open-window registry (creation order, secondaries only — the main window is +// not a Window object). Non-inline storage in tcGlobal.cpp so the hot-reload +// host and guest see ONE list. Used by the MCP window-targeting tools. +void registerWindow(Window* w); +void unregisterWindow(Window* w); +std::vector openWindows(); // only windows whose native side is alive + +// RAII registrar: a Window member, so every ~Window() unregisters no matter +// which platform adapter defines the destructor. +struct WindowRegistryEntry { + Window* w; + explicit WindowRegistryEntry(Window* win) : w(win) { registerWindow(win); } + ~WindowRegistryEntry() { unregisterWindow(w); } + WindowRegistryEntry(const WindowRegistryEntry&) = delete; + WindowRegistryEntry& operator=(const WindowRegistryEntry&) = delete; +}; +} + +class TC_PLATFORMS("macos,windows,linux") Window { +public: + ~Window(); + + // Attach an App to this window — the ONLY way to give a window content. + // The App gets its familiar lifecycle wired to THIS window: setup() once + // on the first tick, update()/draw() on the window's own tick, + // keyPressed/mousePressed/... from this window's event stream, and + // windowResized() on resize (whose base impl keeps the App's RectNode + // size in sync, exactly like the main window). App IS a Node — attach + // children as usual for scene content. + // One App per window; attaching an App that is already driving another + // window (or the main one) is an error. + // Caveat (Phase 2): App::setSize / window-control helpers still target + // the MAIN window; use this Window handle to control this one. + // Note: the App's setup() runs once on the window's first tree update + // (standard Node lifecycle), i.e. on the window's first tick. + void setApp(std::shared_ptr app); + std::shared_ptr getApp() const { return app_; } + + // This window's event stream (mousePressed / keyPressed / draw / ...). + CoreEvents& events() { return events_; } + + // Close the native window. The main window and other windows keep running. + void close(); + bool isOpen() const { return native_ != nullptr; } + + void setTitle(const std::string& title); + // Last title set via WindowSettings/setTitle (for window listing/tooling). + const std::string& getTitle() const { return title_; } + int getWidth() const; // logical size (matches the window's coordinates) + int getHeight() const; + + void setClearColor(const Color& c) { clearColor_ = c; } + + internal::WindowContext& context() { return ctx_; } + + // --- tree driving (called by the platform glue; friend access to Node) --- + void dispatchMousePressToTree(const MouseEventArgs& e) { + if (ctx_.rootNode) ctx_.rootNode->dispatchMousePress(e); + } + void dispatchMouseReleaseToTree(const MouseEventArgs& e) { + if (ctx_.rootNode) ctx_.rootNode->dispatchMouseRelease(e); + } + void tickTree() { + if (!ctx_.rootNode) return; + ctx_.rootNode->updateTree(); + ctx_.rootNode->updateHoverState(ctx_.mouseX, ctx_.mouseY); + } + void drawTreeNow() { + if (ctx_.rootNode) ctx_.rootNode->drawTree(); + } + // Size-sync convention (mirrors the main App, which is a RectNode kept in + // sync with the window): if the root IS a RectNode it is resized to the + // window's logical size at attach time and on every resize/display change. + // A plain Node root is left untouched. + void syncRootSize(float wPts, float hPts) { + if (!app_) return; + // handleWindowResized() = RectNode::setSize (non-virtual; App::setSize + // is overridden to resize the OS window) + the windowResized() hook — + // the same path the main window uses on SAPP_EVENTTYPE_RESIZED. + if (app_->getWidth() != wPts || app_->getHeight() != hPts) { + app_->handleWindowResized((int)wPts, (int)hPts); + ResizeEventArgs args; args.width = (int)wPts; args.height = (int)hPts; + events_.windowResized.notify(args); + } + } + + // --- internal (used by the platform glue; not user API) --- + Window(); + std::shared_ptr app_; // set via setApp (may be null) + void* native_ = nullptr; // platform-side state + internal::WindowContext ctx_; + CoreEvents events_; + internal::RenderContext render_; + Color clearColor_ = Color(0.05f, 0.05f, 0.08f, 1.0f); + std::string title_; + internal::WindowRegistryEntry registryEntry_{this}; +}; + +// Create a secondary window. macOS, Windows and Linux (nullptr elsewhere). +// The returned shared_ptr keeps the window alive; closing the window (user or +// close()) destroys the native side while the handle stays valid (isOpen()). +TC_PLATFORMS("macos,windows,linux") std::shared_ptr createWindow(const WindowSettings& settings = {}); + +inline Window::Window() { + ctx_.isMain = false; + ctx_.render = &render_; + ctx_.coreEvents = &events_; +} + +namespace internal { +// Apps currently driving a window (double-attach guard). An inline variable: +// a hot-reload guest gets its own copy, so the guard is per-binary — fine for +// a misuse check. (runApp unification — "runApp = create main window + +// setApp" — is a future refactor; the main App is guarded via rootNode.) +inline std::unordered_set attachedApps; +} + +inline void Window::setApp(std::shared_ptr app) { + if (app) { + if (app.get() == internal::mainWindowContext().rootNode) { + logError("Window") << "setApp(): this App is the running main App"; + return; + } + if (internal::attachedApps.count(app.get())) { + logError("Window") << "setApp(): this App already drives another window"; + return; + } + } + if (app_) internal::attachedApps.erase(app_.get()); + if (app) internal::attachedApps.insert(app.get()); + app_ = std::move(app); + ctx_.rootNode = app_.get(); +} + +#if defined(__APPLE__) +#include +#endif +#if !(defined(__APPLE__) && TARGET_OS_OSX) && !defined(_WIN32) && !(defined(__linux__) && defined(SOKOL_GLCORE)) +// Stubs for platforms without window glue (web / iOS / Android / Raspberry +// Pi). The real implementations live in platform/mac/tcWindowMac.mm, +// platform/win/tcWindowWin.cpp and platform/linux/tcWindowLinux.cpp (desktop +// Linux is SOKOL_GLCORE; Raspberry Pi builds the same sources with GLES3/EGL +// and keeps these stubs). +inline Window::~Window() {} +inline void Window::close() {} +inline void Window::setTitle(const std::string&) {} +inline int Window::getWidth() const { return 0; } +inline int Window::getHeight() const { return 0; } +inline std::shared_ptr createWindow(const WindowSettings&) { + logError("Window") << "createWindow(): secondary windows are supported on " + "macOS, Windows and desktop Linux (OpenGL); this platform is " + "single-window"; + return nullptr; +} +#endif + +} // namespace trussc diff --git a/core/include/tc/app/tcWindowContext.h b/core/include/tc/app/tcWindowContext.h new file mode 100644 index 00000000..2fdcda6b --- /dev/null +++ b/core/include/tc/app/tcWindowContext.h @@ -0,0 +1,191 @@ +#pragma once + +// ============================================================================= +// tcWindowContext.h - per-window state container (internal) +// ============================================================================= +// +// Phase 0 of multi-window support: every piece of state that is conceptually +// "per window" — input, hover/scene, camera/projection, render-pass — lives in +// one WindowContext instead of scattered process globals. The app is still +// single-window: currentWindowContext() == mainWindowContext() always. Phase 1 +// creates additional contexts (one per native window) and switches +// internal::currentWindowCtx while dispatching each window's events and draw. +// +// This file is included from TrussC.h AFTER tcRenderTarget.h (RenderTarget by +// value) and tcCameraContext.h / tcCoreEvents.h; it is not self-contained. +// +// Hot-reload note: mainWindowContext() is NON-inline (defined in tcGlobal.cpp) +// so Host and Guest share one instance — same pattern as events() and +// getDefaultContext(). The RenderContext / CoreEvents pointers below are wired +// lazily by those accessors, preserving their existing construction timing. + +namespace trussc { + +class Node; +class CoreEvents; + +namespace internal { + +class RenderContext; // the real class lives in internal:: (tcRenderContext.h) + +// --------------------------------------------------------------------------- +// Scissor clipping stack (moved from TrussC.h) +// --------------------------------------------------------------------------- +struct ScissorRect { + float x, y, w, h; + bool active; // Whether a valid range exists in the stack +}; + +struct WindowContext { + WindowContext() = default; + // currentTarget points into this object — copying/moving would dangle it. + WindowContext(const WindowContext&) = delete; + WindowContext& operator=(const WindowContext&) = delete; + + // --- input state (written by the framework event loop) --- + float mouseX = 0.0f; + float mouseY = 0.0f; + float pmouseX = 0.0f; // Previous frame mouse position + float pmouseY = 0.0f; + int mouseButton = -1; // Currently pressed button (-1 = none) + bool mousePressed = false; + std::unordered_set keysPressed; + + // --- scene / hover state (per window's node tree) --- + Node* rootNode = nullptr; // The running App (set by the framework) + Node* hoveredNode = nullptr; // Currently hovered node + Node* prevHoveredNode = nullptr; // Previously hovered node + Node* grabbedNode = nullptr; // Node grabbed by mouse press + int grabbedButton = -1; // Mouse button that caused the grab + Node* selectedNode = nullptr; // Last node clicked (selection) + + // --- camera / projection state --- + std::shared_ptr currentCameraContext; + Mat4 currentViewMatrix = Mat4::identity(); + Mat4 currentProjectionMatrix = Mat4::identity(); + float currentScreenFov = 45.0f; + float currentViewW = 0.0f; + float currentViewH = 0.0f; + float currentCameraDist = 0.0f; // Distance from camera to Z=0 plane + + // --- render-pass state --- + RenderTarget swapchainTarget; // .context set after sgl_setup() + RenderTarget* currentTarget = &swapchainTarget; // Fbo::begin/end retargets this + sg_color swapchainClearValue = { 0.0f, 0.0f, 0.0f, 1.0f }; + bool inSwapchainPass = false; + bool inFboPass = false; + // Metal drawable actually rendered into this frame (see tcGlobal.cpp notes) + const void* lastSwapchainDrawable = nullptr; + std::vector scissorStack; + ScissorRect currentScissor = {0, 0, 0, 0, false}; + // Current 2D blend mode (the "role"); actual sgl pipeline in currentTarget + BlendMode currentBlendMode = BlendMode::Alpha; + + // --- window identity / swapchain source (Phase 1 seam) --- + // Main window: isMain=true, swapchain comes from sglue_swapchain() and + // dimensions from sapp. Secondary windows: the native layer sets isMain + // false, keeps fbWidth/fbHeight/dpiScale up to date, and provides the + // frame's swapchain through acquireSwapchain (called by + // ensureSwapchainPass/resumeSwapchainPass; must return the SAME drawable + // for the duration of one window frame). + bool isMain = true; + // Swapchain attachment formats of THIS window. Secondary windows: set by + // the platform adapter (mac/win: BGRA8, linux: RGBA8 -- the main window + // is the only RGB10A2 surface). Zero = environment defaults (main). + // Consumers that build pipelines rendering into this window's swapchain + // (e.g. tcxImGui) must use these instead of the environment defaults. + sg_pixel_format swapchainColorFormat = _SG_PIXELFORMAT_DEFAULT; + int swapchainSampleCount = 0; + int fbWidth = 0; + int fbHeight = 0; + float dpiScale = 1.0f; + sg_swapchain (*acquireSwapchain)(void* user) = nullptr; + void* acquireSwapchainUser = nullptr; + + // --- frame timing (per window) --- + // getDeltaTime()/getFrameRate() resolve through the current context, so a + // window ticking at 60 Hz next to a 120 Hz main window sees its own real + // per-tick delta (measured wall-clock between THIS window's update calls). + double updateDeltaTime = 0.0; + std::chrono::high_resolution_clock::time_point lastUpdateCallTime; + bool lastUpdateCallTimeInitialized = false; + // Frame rate measurement (10-frame moving average) + double frameTimeBuffer[10] = {}; + int frameTimeIndex = 0; + bool frameTimeBufferFilled = false; + + // --- misc per-window --- + int clipboardSize = 65536; // Clipboard buffer size (for overflow check) + + // Wired lazily by getDefaultContext() / events() for the main window + // (preserves their pre-context construction timing); Phase 1 pre-wires + // these when constructing secondary-window contexts. + RenderContext* render = nullptr; + CoreEvents* coreEvents = nullptr; +}; + +// Main window context. Non-inline (tcGlobal.cpp) — Host/Guest share one. +WindowContext& mainWindowContext(); + +// Active context while dispatching a window's events / draw. Null = main. +// (An inline variable: a hot-reload guest may hold its own copy, which stays +// null there and falls through to the shared mainWindowContext() — identical +// behavior while single-window.) +inline WindowContext* currentWindowCtx = nullptr; + +inline WindowContext& currentWindowContext() { + return currentWindowCtx ? *currentWindowCtx : mainWindowContext(); +} + +// --------------------------------------------------------------------------- +// Active render-target pipeline helpers (moved from tcRenderTarget.h — they +// resolve through the current window's target now) +// --------------------------------------------------------------------------- + +// Role keys: high nibble = role, low byte = blend mode (2D only). +inline sgl_pipeline active2D(BlendMode m) { return currentWindowContext().currentTarget->pipeline(0x000u | (uint32_t)m, pipeDesc2D(m)); } +inline sgl_pipeline activeFill2D() { return active2D(BlendMode::Alpha); } +inline sgl_pipeline activePremult() { return currentWindowContext().currentTarget->pipeline(0x100u, pipeDescPremult()); } +inline sgl_pipeline activeClear() { return currentWindowContext().currentTarget->pipeline(0x200u, pipeDesc2D(BlendMode::Disabled)); } +inline sgl_pipeline active3D() { return currentWindowContext().currentTarget->pipeline(0x300u, pipeDesc3D()); } + +// Single chokepoint for loading an sgl pipeline by role. No-op if the target isn't +// ready (id 0). In debug builds it asserts the pipeline was built for the active +// target's context — the runtime safety net against loading a pipeline meant for a +// different target (the bug class this refactor removes). +inline void loadPipeline(sgl_pipeline p) { + if (p.id == 0) return; +#ifndef NDEBUG + auto& owner = pipelineOwnerCtx(); + auto it = owner.find(p.id); + // Only check pipelines WE built (others, e.g. sgl's built-in default, are unknown). + assert((it == owner.end() || it->second == currentWindowContext().currentTarget->context.id) + && "loadPipeline: sgl pipeline built for a different render target than the active one"); +#endif + sgl_load_pipeline(p); +} + +// Restore the current blend pipeline after temporary pipeline changes. +// FBO uses its accumulating Fill2D; swapchain honors the current blend mode. +// (moved from TrussC.h) +inline void restoreCurrentPipeline() { + auto& ctx = currentWindowContext(); + loadPipeline(ctx.inFboPass ? activeFill2D() : active2D(ctx.currentBlendMode)); +} + +// Register a new camera scope (declared in tcCameraContext.h, defined here +// where WindowContext is complete). Always allocates a fresh context — see +// the immutability note in tcCameraContext.h. +inline void registerCameraContext(const Mat4& view, const Mat4& projection, + float viewW, float viewH, bool pickable) { + auto ctx = std::make_shared(); + ctx->view = view; + ctx->projection = projection; + ctx->viewW = viewW; + ctx->viewH = viewH; + ctx->pickable = pickable; + currentWindowContext().currentCameraContext = std::move(ctx); +} + +} // namespace internal +} // namespace trussc diff --git a/core/include/tc/events/tcCoreEvents.h b/core/include/tc/events/tcCoreEvents.h index 4836d5c2..9b331aa9 100644 --- a/core/include/tc/events/tcCoreEvents.h +++ b/core/include/tc/events/tcCoreEvents.h @@ -6,7 +6,7 @@ #include "tcEvent.h" #include "tcEventArgs.h" -#include "sokol/sokol_app.h" +#include "sokol/sokol_app_tc.h" namespace trussc { diff --git a/core/include/tc/gpu/tcFbo.h b/core/include/tc/gpu/tcFbo.h index fea81fa8..98592be6 100644 --- a/core/include/tc/gpu/tcFbo.h +++ b/core/include/tc/gpu/tcFbo.h @@ -253,23 +253,23 @@ class Fbo : public HasTexture { // Switch back to default context sgl_set_context(sgl_default_context()); active_ = false; - internal::inFboPass = false; + internal::currentWindowContext().inFboPass = false; internal::currentFbo = nullptr; internal::currentFboColorFormat = SG_PIXELFORMAT_RGBA8; internal::currentFboSampleCount = 1; - internal::currentTarget = &internal::swapchainTarget; // RenderTarget: back to swapchain + internal::currentWindowContext().currentTarget = &internal::currentWindowContext().swapchainTarget; // RenderTarget: back to swapchain internal::fboClearColorFunc = nullptr; // Restore the screen camera state saved in beginInternal() so // worldToScreen / camera-context stamping after end() see the screen // camera again instead of this FBO's projection. - internal::currentScreenFov = savedScreenFov_; - internal::currentViewW = savedViewW_; - internal::currentViewH = savedViewH_; - internal::currentCameraDist = savedCameraDist_; - internal::currentProjectionMatrix = savedProjectionMatrix_; - internal::currentViewMatrix = savedViewMatrix_; - internal::currentCameraContext = savedCameraContext_; + internal::currentWindowContext().currentScreenFov = savedScreenFov_; + internal::currentWindowContext().currentViewW = savedViewW_; + internal::currentWindowContext().currentViewH = savedViewH_; + internal::currentWindowContext().currentCameraDist = savedCameraDist_; + internal::currentWindowContext().currentProjectionMatrix = savedProjectionMatrix_; + internal::currentWindowContext().currentViewMatrix = savedViewMatrix_; + internal::currentWindowContext().currentCameraContext = savedCameraContext_; savedCameraContext_.reset(); // Resume swapchain pass (if we were in one before) @@ -595,7 +595,7 @@ class Fbo : public HasTexture { if (!allocated_) return; // Guard: nested FBO begin is not supported - if (internal::inFboPass) { + if (internal::currentWindowContext().inFboPass) { logWarning("Fbo") << "Nested fbo.begin() is not supported. " << "Call end() on the current FBO first."; return; @@ -651,13 +651,13 @@ class Fbo : public HasTexture { // projection setup below overwrites these globals, and anything drawn // after end() (worldToScreen, node camera-context stamping) must see // the screen camera again, not the FBO's. - savedScreenFov_ = internal::currentScreenFov; - savedViewW_ = internal::currentViewW; - savedViewH_ = internal::currentViewH; - savedCameraDist_ = internal::currentCameraDist; - savedProjectionMatrix_ = internal::currentProjectionMatrix; - savedViewMatrix_ = internal::currentViewMatrix; - savedCameraContext_ = internal::currentCameraContext; + savedScreenFov_ = internal::currentWindowContext().currentScreenFov; + savedViewW_ = internal::currentWindowContext().currentViewW; + savedViewH_ = internal::currentWindowContext().currentViewH; + savedCameraDist_ = internal::currentWindowContext().currentCameraDist; + savedProjectionMatrix_ = internal::currentWindowContext().currentProjectionMatrix; + savedViewMatrix_ = internal::currentWindowContext().currentViewMatrix; + savedCameraContext_ = internal::currentWindowContext().currentCameraContext; // Setup screen projection using defaultScreenFov (like main screen). // pickable=false: geometry drawn into an offscreen target must not be @@ -665,11 +665,11 @@ class Fbo : public HasTexture { internal::setupScreenFovWithSize(internal::defaultScreenFov, (float)width_, (float)height_, 0.0f, 0.0f, false); active_ = true; - internal::inFboPass = true; + internal::currentWindowContext().inFboPass = true; // Retarget to this FBO BEFORE loading its fill pipeline so activeFill2D() // resolves in the FBO's context/format (setupScreenFov above still ran on // the previous target, as before). - internal::currentTarget = &shared.target; + internal::currentWindowContext().currentTarget = &shared.target; // Use the FBO's alpha-blend Fill2D pipeline (Porter-Duff over); the result // is stored as premultiplied alpha in the FBO. diff --git a/core/include/tc/gpu/tcShader.h b/core/include/tc/gpu/tcShader.h index 5a3cbf1c..0b7bacce 100644 --- a/core/include/tc/gpu/tcShader.h +++ b/core/include/tc/gpu/tcShader.h @@ -438,7 +438,7 @@ class Shader { // sample count and depth match the FBO, so one is built lazily (from the same // createPipelineDesc()) and cached per distinct (format, sampleCount) target. sg_pipeline pipelineForCurrentTarget() { - if (!internal::inFboPass) return pipeline; + if (!internal::currentWindowContext().inFboPass) return pipeline; uint64_t key = ((uint64_t)internal::currentFboColorFormat << 8) | (uint64_t)(internal::currentFboSampleCount & 0xff); auto it = targetPipelines_.find(key); @@ -611,13 +611,31 @@ class FullscreenShader : public Shader { sg_draw(0, 6, 1); - // Restore sokol_gl state + // Restore sokol_gl state to what the rest of the frame expects. + // + // Inside an Fbo pass that is the corner-origin ortho Fbo::begin set up. + // On the SWAPCHAIN the screen convention is NOT a corner ortho — it is + // the screen camera from setupScreenFovWithSize (a CENTERED projection + // plus a lookat modelview; perspective when defaultScreenFov > 0). A + // plain ortho here leaves a mixed state: as soon as the engine + // re-applies the camera modelview, later 2D draws land shifted by + // (-W/2, -H/2) at the wrong scale. (Historic bug: this used + // sapp_width(), physical px, which additionally halved everything on + // retina.) Re-run the real setup with the CURRENT view params instead. sg_reset_state_cache(); - sgl_defaults(); - sgl_matrix_mode_projection(); - sgl_ortho(0.0f, (float)sapp_width(), (float)sapp_height(), 0.0f, -10000.0f, 10000.0f); - sgl_matrix_mode_modelview(); - sgl_load_identity(); + auto& wctx = internal::currentWindowContext(); + if (wctx.inFboPass) { + sgl_defaults(); + internal::loadPipeline(internal::activeFill2D()); + sgl_matrix_mode_projection(); + sgl_ortho(0.0f, wctx.currentViewW, wctx.currentViewH, 0.0f, -10000.0f, 10000.0f); + sgl_matrix_mode_modelview(); + sgl_load_identity(); + } else { + internal::setupScreenFovWithSize(wctx.currentScreenFov, + wctx.currentViewW, wctx.currentViewH, + 0.0f, 0.0f); + } } protected: diff --git a/core/include/tc/gpu/tcTexture.h b/core/include/tc/gpu/tcTexture.h index e3663187..b1d3747c 100644 --- a/core/include/tc/gpu/tcTexture.h +++ b/core/include/tc/gpu/tcTexture.h @@ -904,7 +904,7 @@ class Texture { float u0, float v0, float u1, float v1) const { // Blend pipeline for the active target (behavior preserved: FBO uses Fill2D // even for premultiplied sources, as before). - if (internal::inFboPass) { + if (internal::currentWindowContext().inFboPass) { internal::loadPipeline(internal::activeFill2D()); } else if (premultipliedAlpha_) { internal::loadPipeline(internal::activePremult()); diff --git a/core/include/tc/graphics/tcCameraContext.h b/core/include/tc/graphics/tcCameraContext.h index 9de5771a..b7e3fd52 100644 --- a/core/include/tc/graphics/tcCameraContext.h +++ b/core/include/tc/graphics/tcCameraContext.h @@ -72,24 +72,17 @@ class CameraContext { namespace internal { -// The context the current drawing happens under (most recent registration). -// Nodes stamp this during drawTree(); null until the first camera setup of -// the process (e.g. headless mode), in which case picking falls back to the -// plain vertical screen ray. -inline std::shared_ptr currentCameraContext; +// The context the current drawing happens under (most recent registration) +// lives in WindowContext (tc/app/tcWindowContext.h): reach it via +// internal::currentWindowContext().currentCameraContext. Null until the first +// camera setup of the process (e.g. headless mode), in which case picking +// falls back to the plain vertical screen ray. // Register a new camera scope. Always allocates a fresh context — see the -// immutability note in the header comment. +// immutability note in the header comment. Defined in tcWindowContext.h +// (needs the complete WindowContext). inline void registerCameraContext(const Mat4& view, const Mat4& projection, - float viewW, float viewH, bool pickable = true) { - auto ctx = std::make_shared(); - ctx->view = view; - ctx->projection = projection; - ctx->viewW = viewW; - ctx->viewH = viewH; - ctx->pickable = pickable; - currentCameraContext = std::move(ctx); -} + float viewW, float viewH, bool pickable = true); // Per-pick ray cache used by one hit-test traversal: the cursor position plus // one lazily-built ray per camera context encountered (a frame typically has diff --git a/core/include/tc/graphics/tcFont.h b/core/include/tc/graphics/tcFont.h index 45af5312..a2d1efbd 100644 --- a/core/include/tc/graphics/tcFont.h +++ b/core/include/tc/graphics/tcFont.h @@ -30,7 +30,7 @@ #endif // sokol headers -#include "sokol/sokol_app.h" +#include "sokol/sokol_app_tc.h" #include "sokol/sokol_gfx.h" #include "sokol/util/sokol_gl_tc.h" @@ -38,6 +38,7 @@ #include "stb/stb_truetype.h" #include "../utils/tcLog.h" +#include "tc/utils/tcLoadResult.h" #include "../utils/tcSystemFont.h" #include "../types/tcDirection.h" #include "../types/tcRectangle.h" @@ -191,8 +192,9 @@ class FontAtlasManager { bool setup(const std::string& fontPath, int fontSize) { cleanup(); - // Load font file - std::ifstream file(fontPath, std::ios::binary | std::ios::ate); + // Load font file (fontPath is UTF-8 — convert so non-ASCII paths + // survive on Windows) + std::ifstream file(internal::utf8ToPath(fontPath), std::ios::binary | std::ios::ate); if (!file) { logError() << "FontAtlasManager: failed to open " << fontPath; return false; @@ -884,7 +886,7 @@ class Font { // - A system font name (PostScript or family name) — resolved via // tc::systemFontPath when the path doesn't exist on disk. Lets users // write `font.load("HiraginoSans-W3", 24)` cross-platform. - bool load(const std::string& nameOrPath, int size) { + LoadResult load(const fs::path& nameOrPath, int size) { // Render glyphs at physical pixel size for sharp text on HiDPI displays. // All metrics/drawing are scaled back to logical coordinates. dpiScale_ = sapp_dpi_scale(); @@ -896,17 +898,21 @@ class Font { initResources(); } - // Resolve input to a concrete path (file / URL). - std::string actualPath = nameOrPath; - if (!isUrl(nameOrPath)) { + // Resolve input to a concrete path (file / URL). A font NAME + // ("HiraginoSans-W3") is a valid relative fs::path, so both spellings + // arrive here; the UTF-8 string form is what cache keys and the + // system-font lookup use. + std::string nameStr = internal::pathToUtf8(nameOrPath); + std::string actualPath = nameStr; + if (!isUrl(nameStr)) { std::ifstream test(nameOrPath, std::ios::binary); if (!test.good()) { // Not a usable file path — try as a system font name. - std::string resolved = systemFontPath(nameOrPath); + fs::path resolved = systemFontPath(nameStr); if (!resolved.empty()) { - actualPath = resolved; - logNotice("Font") << "Resolved \"" << nameOrPath - << "\" → " << resolved; + actualPath = internal::pathToUtf8(resolved); + logNotice("Font") << "Resolved \"" << nameStr + << "\" → " << actualPath; } // If resolution failed, fall through with the original input so // the eventual load error mentions what the user actually asked for. @@ -920,16 +926,30 @@ class Font { #ifdef __EMSCRIPTEN__ // Async load - returns immediately, font available after fetch completes loadFromUrlAsync(actualPath, physicalSize); - return true; // Will be loaded asynchronously + return LoadResult::success(); // Will be loaded asynchronously #else logError() << "Font: URL loading only supported in WebAssembly"; - return false; + return LoadResult::fail(LoadError::UnsupportedFormat, + "URL loading only supported in WebAssembly: " + actualPath); #endif } else { atlasManager_ = internal::SharedFontCache::getInstance().getOrCreate(cacheKey_); } - return atlasManager_ != nullptr; + if (!atlasManager_) { + // Classify: the resolved path doesn't exist on disk -> the font + // was never found (bad path or unresolvable system-font name); + // otherwise the file opened but stb_truetype rejected it. + std::error_code ec; + if (!fs::exists(internal::utf8ToPath(actualPath), ec)) { + std::string msg = "font not found: \"" + nameStr + "\""; + if (actualPath != nameStr) msg += " (resolved to \"" + actualPath + "\")"; + return LoadResult::fail(LoadError::FileNotFound, msg); + } + return LoadResult::fail(LoadError::DecodeFailed, + "failed to init font: " + actualPath); + } + return LoadResult::success(); } private: diff --git a/core/include/tc/graphics/tcImage.h b/core/include/tc/graphics/tcImage.h index 3f182522..3e2843b2 100644 --- a/core/include/tc/graphics/tcImage.h +++ b/core/include/tc/graphics/tcImage.h @@ -67,32 +67,34 @@ class Image : public HasTexture { // onto a 3D surface that moves toward/away from the camera) — without // mipmaps, small projected sizes shimmer/moiré. Costs about +33% GPU // memory and a one-time CPU box-average to build the chain. - bool load(const fs::path& path, bool mipmaps = false) { + LoadResult load(const fs::path& path, bool mipmaps = false) { clear(); - fs::path resolved = path.is_absolute() ? path : fs::path(getDataPath(path.string())); - if (!pixels_.load(resolved)) { - return false; + fs::path resolved = getDataPath(path); // absolute paths pass through + LoadResult r = pixels_.load(resolved); + if (!r) { + return r; } mipmaps_ = mipmaps; usage_ = TextureUsage::Immutable; texture_.allocate(pixels_, TextureUsage::Immutable, mipmaps); - return true; + return LoadResult::success(); } // Load image from memory - bool loadFromMemory(const unsigned char* buffer, int len, bool mipmaps = false) { + LoadResult loadFromMemory(const unsigned char* buffer, int len, bool mipmaps = false) { clear(); - if (!pixels_.loadFromMemory(buffer, len)) { - return false; + LoadResult r = pixels_.loadFromMemory(buffer, len); + if (!r) { + return r; } mipmaps_ = mipmaps; usage_ = TextureUsage::Immutable; texture_.allocate(pixels_, TextureUsage::Immutable, mipmaps); - return true; + return LoadResult::success(); } // Save image (override of HasTexture::save()) diff --git a/core/include/tc/graphics/tcPixels.cpp b/core/include/tc/graphics/tcPixels.cpp index 04c5a196..62b41d8d 100644 --- a/core/include/tc/graphics/tcPixels.cpp +++ b/core/include/tc/graphics/tcPixels.cpp @@ -12,11 +12,12 @@ bool Pixels::save(const fs::path& path) const { // Convert relative paths to data path fs::path savePath = path; if (path.is_relative()) { - savePath = getDataPath(path.string()); + savePath = getDataPath(path); } auto ext = savePath.extension().string(); - auto pathStr = savePath.string(); + // UTF-8 for stb (STBIW_WINDOWS_UTF8 makes stb wide-open it on Windows) + auto pathStr = internal::pathToUtf8(savePath); int result = 0; if (ext == ".png" || ext == ".PNG") { diff --git a/core/include/tc/graphics/tcPixels.h b/core/include/tc/graphics/tcPixels.h index 3fe3f71d..79f16d05 100644 --- a/core/include/tc/graphics/tcPixels.h +++ b/core/include/tc/graphics/tcPixels.h @@ -1,4 +1,5 @@ #pragma once +#include "tc/utils/tcLoadResult.h" // ============================================================================= // tcPixels.h - CPU pixel buffer management @@ -9,6 +10,7 @@ #include #include "stb/stb_image.h" #include "stb/stb_image_write.h" +#include "tc/utils/tcFileIO.h" // internal::pathToUtf8 namespace trussc { @@ -211,14 +213,22 @@ class Pixels { // === File I/O === // Load from file (stb_image first, then platform-specific fallback for HEIC etc.) - bool load(const fs::path& path) { + LoadResult load(const fs::path& path) { clear(); + std::error_code ec; + if (!std::filesystem::exists(path, ec)) { + return LoadResult::fail(LoadError::FileNotFound, + "file not found: " + internal::pathToUtf8(path)); + } int w, h, channels; - unsigned char* loaded = stbi_load(path.string().c_str(), &w, &h, &channels, 4); + unsigned char* loaded = stbi_load(internal::pathToUtf8(path).c_str(), &w, &h, &channels, 4); if (!loaded) { + const char* stbReason = stbi_failure_reason(); // Fallback to platform-specific loader (ImageIO on macOS) - return loadPlatform(path); + if (loadPlatform(path)) return LoadResult::success(); + return LoadResult::fail(LoadError::DecodeFailed, + stbReason ? stbReason : "decoder rejected the file"); } width_ = w; @@ -232,19 +242,26 @@ class Pixels { stbi_image_free(loaded); allocated_ = true; - return true; + return LoadResult::success(); } // Load an HDR (Radiance .hdr / .pic) image as float pixels. stb_image // decodes radiance RGBE into linear float32 RGB. The alpha channel is // synthesized as 1.0 to keep downstream code RGBA-friendly. - bool loadHDR(const fs::path& path) { + LoadResult loadHDR(const fs::path& path) { clear(); + std::error_code ec; + if (!std::filesystem::exists(path, ec)) { + return LoadResult::fail(LoadError::FileNotFound, + "file not found: " + internal::pathToUtf8(path)); + } int w, h, channels; - float* loaded = stbi_loadf(path.string().c_str(), &w, &h, &channels, 3); + float* loaded = stbi_loadf(internal::pathToUtf8(path).c_str(), &w, &h, &channels, 3); if (!loaded) { - return false; + const char* stbReason = stbi_failure_reason(); + return LoadResult::fail(LoadError::DecodeFailed, + stbReason ? stbReason : "decoder rejected the file"); } width_ = w; @@ -264,20 +281,22 @@ class Pixels { stbi_image_free(loaded); allocated_ = true; - return true; + return LoadResult::success(); } // Platform-specific image loader (implemented per platform) bool loadPlatform(const fs::path& path); // Load from memory - bool loadFromMemory(const unsigned char* buffer, int len) { + LoadResult loadFromMemory(const unsigned char* buffer, int len) { clear(); int w, h, channels; unsigned char* loaded = stbi_load_from_memory(buffer, len, &w, &h, &channels, 4); if (!loaded) { - return false; + const char* stbReason = stbi_failure_reason(); + return LoadResult::fail(LoadError::DecodeFailed, + stbReason ? stbReason : "decoder rejected the data"); } width_ = w; @@ -291,7 +310,7 @@ class Pixels { stbi_image_free(loaded); allocated_ = true; - return true; + return LoadResult::success(); } // Save to file (implemented in tcPixels.cpp for dataPath support) diff --git a/core/include/tc/graphics/tcRenderContext.cpp b/core/include/tc/graphics/tcRenderContext.cpp index 0ee0583a..1f55f902 100644 --- a/core/include/tc/graphics/tcRenderContext.cpp +++ b/core/include/tc/graphics/tcRenderContext.cpp @@ -187,7 +187,7 @@ bool internal::RenderContext::drawBitmapStringBillboard(const std::string& text, // Absence-of-context handling only (not a semantic gate): with no camera // context yet (headless / before the first camera setup) or a degenerate // viewport there is nothing to project through. - std::shared_ptr ctx = internal::currentCameraContext; + std::shared_ptr ctx = internal::currentWindowContext().currentCameraContext; if (!ctx || ctx->viewW <= 0.0f || ctx->viewH <= 0.0f) return false; if (text.empty()) return true; // handled: nothing to draw @@ -226,7 +226,7 @@ bool internal::RenderContext::drawBitmapStringBillboard(const std::string& text, sgl_matrix_mode_projection(); sgl_push_matrix(); sgl_load_identity(); - sgl_ortho(0.0f, internal::currentViewW, internal::currentViewH, 0.0f, -10000.0f, 10000.0f); + sgl_ortho(0.0f, internal::currentWindowContext().currentViewW, internal::currentWindowContext().currentViewH, 0.0f, -10000.0f, 10000.0f); sgl_matrix_mode_modelview(); sgl_push_matrix(); @@ -311,7 +311,7 @@ void internal::RenderContext::drawBitmapString(const std::string& text, float x, sgl_matrix_mode_projection(); sgl_push_matrix(); sgl_load_identity(); - sgl_ortho(0.0f, internal::currentViewW, internal::currentViewH, 0.0f, -10000.0f, 10000.0f); + sgl_ortho(0.0f, internal::currentWindowContext().currentViewW, internal::currentWindowContext().currentViewH, 0.0f, -10000.0f, 10000.0f); sgl_matrix_mode_modelview(); sgl_push_matrix(); @@ -484,7 +484,7 @@ void internal::RenderContext::drawBitmapString(const std::string& text, float x, sgl_matrix_mode_projection(); sgl_push_matrix(); sgl_load_identity(); - sgl_ortho(0.0f, internal::currentViewW, internal::currentViewH, 0.0f, -10000.0f, 10000.0f); + sgl_ortho(0.0f, internal::currentWindowContext().currentViewW, internal::currentWindowContext().currentViewH, 0.0f, -10000.0f, 10000.0f); sgl_matrix_mode_modelview(); sgl_push_matrix(); @@ -580,12 +580,19 @@ void internal::RenderContext::drawBitmapString(const std::string& text, float x, } // --------------------------------------------------------------------------- -// Default context singleton (non-inline — see tcRenderContext.h comment) +// Default context accessor (non-inline — see tcRenderContext.h comment) // --------------------------------------------------------------------------- namespace internal { RenderContext& getDefaultContext() { - static RenderContext instance; - return instance; + // The RenderContext is owned per window context. The main window's is + // wired lazily here (preserving the pre-context construction timing); + // Phase 1 pre-wires the pointer when constructing secondary contexts. + auto& ctx = currentWindowContext(); + if (!ctx.render) { + static RenderContext instance; // main-window storage + ctx.render = &instance; + } + return *ctx.render; } } // namespace internal diff --git a/core/include/tc/graphics/tcRenderContext.h b/core/include/tc/graphics/tcRenderContext.h index dae59dd0..3b02187b 100644 --- a/core/include/tc/graphics/tcRenderContext.h +++ b/core/include/tc/graphics/tcRenderContext.h @@ -59,15 +59,8 @@ namespace internal { extern sg_sampler fontSampler; extern bool fontInitialized; extern bool pixelPerfectMode; - // Current screen setup state (for 2D drawing in perspective mode) - extern float currentScreenFov; - extern float currentViewW; - extern float currentViewH; - extern float currentCameraDist; - - // View/Projection matrix tracking (for worldToScreen/screenToWorld) - extern Mat4 currentViewMatrix; - extern Mat4 currentProjectionMatrix; + // Screen setup / view-projection tracking state lives in WindowContext + // (tc/app/tcWindowContext.h, included before this header from TrussC.h). } // --------------------------------------------------------------------------- @@ -275,9 +268,9 @@ class RenderContext { // ----------------------------------------------------------------------- void pushStyle() { - // Capture the live blend mode (it lives in internal::currentBlendMode, not in + // Capture the live blend mode (it lives in internal::currentWindowContext().currentBlendMode, not in // style_) so it is saved/restored alongside color/fill/stroke. - style_.blend = internal::currentBlendMode; + style_.blend = internal::currentWindowContext().currentBlendMode; styleStack_.push_back(style_); } @@ -300,9 +293,9 @@ class RenderContext { // load is otherwise a no-op that sokol_gl batches away, but we skip it anyway). // No-op inside an FBO pass, which uses its own accumulating pipeline. void applyBlend(BlendMode mode) { - if (mode == internal::currentBlendMode) return; - internal::currentBlendMode = mode; - if (!internal::inFboPass) { + if (mode == internal::currentWindowContext().currentBlendMode) return; + internal::currentWindowContext().currentBlendMode = mode; + if (!internal::currentWindowContext().inFboPass) { internal::loadPipeline(internal::active2D(mode)); } } @@ -380,7 +373,7 @@ class RenderContext { // Restore default view matrix (camera lookat) set by setupScreenFovWithSize // This works correctly both in main screen and FBO contexts // Note: Mat4 is row-major, sokol_gl expects column-major, so transpose - Mat4 t = internal::currentViewMatrix.transposed(); + Mat4 t = internal::currentWindowContext().currentViewMatrix.transposed(); sgl_load_matrix(t.m); } @@ -964,7 +957,7 @@ class RenderContext { Direction textAlignH = Direction::Left; Direction textAlignV = Direction::Top; float bitmapLineHeight = bitmapfont::CHAR_TEX_HEIGHT; - // Mirror of internal::currentBlendMode. blend state itself lives outside the + // Mirror of internal::currentWindowContext().currentBlendMode. blend state itself lives outside the // RenderContext (it maps to an sgl pipeline), so push/popStyle capture and // restore it explicitly rather than via this field's value alone. BlendMode blend = BlendMode::Alpha; diff --git a/core/include/tc/graphics/tcRenderTarget.h b/core/include/tc/graphics/tcRenderTarget.h index c61b9fdc..d9fdf38a 100644 --- a/core/include/tc/graphics/tcRenderTarget.h +++ b/core/include/tc/graphics/tcRenderTarget.h @@ -138,30 +138,8 @@ struct RenderTarget { } }; -inline RenderTarget swapchainTarget; // .context set after sgl_setup() -inline RenderTarget* currentTarget = &swapchainTarget; // Fbo::begin/end retargets this - -// Role keys: high nibble = role, low byte = blend mode (2D only). -inline sgl_pipeline active2D(BlendMode m) { return currentTarget->pipeline(0x000u | (uint32_t)m, pipeDesc2D(m)); } -inline sgl_pipeline activeFill2D() { return active2D(BlendMode::Alpha); } -inline sgl_pipeline activePremult() { return currentTarget->pipeline(0x100u, pipeDescPremult()); } -inline sgl_pipeline activeClear() { return currentTarget->pipeline(0x200u, pipeDesc2D(BlendMode::Disabled)); } -inline sgl_pipeline active3D() { return currentTarget->pipeline(0x300u, pipeDesc3D()); } - -// Single chokepoint for loading an sgl pipeline by role. No-op if the target isn't -// ready (id 0). In debug builds it asserts the pipeline was built for the active -// target's context — the runtime safety net against loading a pipeline meant for a -// different target (the bug class this refactor removes). -inline void loadPipeline(sgl_pipeline p) { - if (p.id == 0) return; -#ifndef NDEBUG - auto& owner = pipelineOwnerCtx(); - auto it = owner.find(p.id); - // Only check pipelines WE built (others, e.g. sgl's built-in default, are unknown). - assert((it == owner.end() || it->second == currentTarget->context.id) - && "loadPipeline: sgl pipeline built for a different render target than the active one"); -#endif - sgl_load_pipeline(p); -} +// The swapchain target, the active-target pointer, and the active*() pipeline +// helpers moved to the per-window WindowContext — see tc/app/tcWindowContext.h +// (included right after this header from TrussC.h). }} // namespace trussc::internal diff --git a/core/include/tc/network/tcTcpServer.cpp b/core/include/tc/network/tcTcpServer.cpp index 8cc4ee9b..190c2d49 100644 --- a/core/include/tc/network/tcTcpServer.cpp +++ b/core/include/tc/network/tcTcpServer.cpp @@ -127,6 +127,11 @@ bool TcpServer::start(int port, int maxClients) { } void TcpServer::stop() { + // A server that never ran (or was already stopped) cleans up silently — + // logging "stopped" from e.g. a static instance's destructor in a CLI + // that never called start() would be announcing an event that didn't + // happen. Cleanup below is idempotent either way. + bool wasRunning = running_; running_ = false; // Close server socket (unblocks accept) @@ -152,7 +157,7 @@ void TcpServer::stop() { // Disconnect all clients disconnectAllClients(); - logNotice() << "TCP server stopped"; + if (wasRunning) logNotice() << "TCP server stopped"; } bool TcpServer::isRunning() const { diff --git a/core/include/tc/sound/tcAudio_impl.cpp b/core/include/tc/sound/tcAudio_impl.cpp index e09a6121..8737bc97 100644 --- a/core/include/tc/sound/tcAudio_impl.cpp +++ b/core/include/tc/sound/tcAudio_impl.cpp @@ -39,6 +39,20 @@ namespace trussc { +namespace { +// Open a ma_decoder from a path. Wide entry point on Windows so non-ASCII +// paths survive (same helper as tcSound_impl.cpp). +ma_result maDecoderInitPathA(const fs::path& path, + const ma_decoder_config* cfg, ma_decoder* dec) { +#ifdef _WIN32 + return ma_decoder_init_file_w(path.c_str(), cfg, dec); +#else + return ma_decoder_init_file(path.c_str(), cfg, dec); +#endif +} +} // namespace + + // ============================================================================= // Streaming audio playback // @@ -260,19 +274,16 @@ class StreamWorker { // query duration/channels/sampleRate. Per-voice decoders are opened // lazily by AudioEngine::play(). // --------------------------------------------------------------------------- -bool SoundStream::loadStream(const std::string& path, int maxPolyphony) { +LoadResult SoundStream::loadStream(const fs::path& path, int maxPolyphony) { if (maxPolyphony < 1) maxPolyphony = 1; // Detect format by extension. We don't go through stb_vorbis here — // OGG support for streaming would need a separate code path. WAV / // MP3 / FLAC are routed through ma_decoder, which handles all three // with the same API. - std::string ext; - auto dot = path.find_last_of('.'); - if (dot != std::string::npos) { - ext = path.substr(dot + 1); - for (auto& c : ext) c = (char)std::tolower((unsigned char)c); - } + std::string ext = path.extension().string(); + if (!ext.empty() && ext[0] == '.') ext.erase(0, 1); + for (auto& c : ext) c = (char)std::tolower((unsigned char)c); ma_encoding_format fmt = ma_encoding_format_unknown; if (ext == "wav") fmt = ma_encoding_format_wav; @@ -289,7 +300,17 @@ bool SoundStream::loadStream(const std::string& path, int maxPolyphony) { // and streaming would need per-platform plumbing — lower priority. printf("SoundStream: unsupported extension for streaming '.%s' (use load() for full decode)\n", ext.c_str()); - return false; + return LoadResult::fail(LoadError::UnsupportedFormat, + "unsupported extension for streaming '." + ext + + "' (use load() for full decode)"); + } + + // Classify missing files before handing the path to miniaudio (whose + // error codes don't distinguish the two cases cheaply). + std::error_code ec; + if (!fs::exists(path, ec)) { + return LoadResult::fail(LoadError::FileNotFound, + "file not found: " + internal::pathToUtf8(path)); } // Probe decode: open, query, close. Per-voice decoders re-open later. @@ -300,10 +321,13 @@ bool SoundStream::loadStream(const std::string& path, int maxPolyphony) { StreamInstance::CHANNELS, AudioEngine::getInstance().getSampleRate()); cfg.encodingFormat = fmt; - ma_result r = ma_decoder_init_file(path.c_str(), &cfg, &probe); + ma_result r = maDecoderInitPathA(path, &cfg, &probe); if (r != MA_SUCCESS) { - printf("SoundStream: failed to open %s (result=%d)\n", path.c_str(), (int)r); - return false; + printf("SoundStream: failed to open %s (result=%d)\n", + internal::pathToUtf8(path).c_str(), (int)r); + return LoadResult::fail(LoadError::DecodeFailed, + "failed to open " + internal::pathToUtf8(path) + + " (result=" + std::to_string((int)r) + ")"); } ma_uint64 totalFrames = 0; @@ -320,8 +344,8 @@ bool SoundStream::loadStream(const std::string& path, int maxPolyphony) { encodingFormatHint_ = (int)fmt; printf("SoundStream: ready %s (%d ch, %d Hz, %.2f s, maxPolyphony=%d)\n", - path.c_str(), channels, sampleRate, duration_, maxPolyphony); - return true; + internal::pathToUtf8(path).c_str(), channels, sampleRate, duration_, maxPolyphony); + return LoadResult::success(); } // --------------------------------------------------------------------------- @@ -360,7 +384,7 @@ std::shared_ptr AudioEngine::play(std::shared_ptr sou ma_decoder_config cfg = ma_decoder_config_init( ma_format_f32, StreamInstance::CHANNELS, sampleRate_); cfg.encodingFormat = (ma_encoding_format)s->encodingFormatHint_; - ma_result r = ma_decoder_init_file(s->getPath().c_str(), &cfg, &stream->decoder); + ma_result r = maDecoderInitPathA(s->path_, &cfg, &stream->decoder); if (r != MA_SUCCESS) { printf("SoundStream: per-voice decoder init failed for %s (result=%d)\n", s->getPath().c_str(), (int)r); @@ -799,7 +823,7 @@ void AudioEngine::migrateVoicesToNewRate(int oldRate, int newRate) { ma_decoder_config cfg = ma_decoder_config_init( ma_format_f32, StreamInstance::CHANNELS, newRate); cfg.encodingFormat = (ma_encoding_format)src->encodingFormatHint_; - ma_result r = ma_decoder_init_file(src->getPath().c_str(), &cfg, &newStream->decoder); + ma_result r = maDecoderInitPathA(src->path_, &cfg, &newStream->decoder); if (r != MA_SUCCESS) { printf("AudioEngine: stream voice migration failed for %s (result=%d) — stopping voice\n", src->getPath().c_str(), (int)r); diff --git a/core/include/tc/sound/tcSound.h b/core/include/tc/sound/tcSound.h index 905715a1..c7487e11 100644 --- a/core/include/tc/sound/tcSound.h +++ b/core/include/tc/sound/tcSound.h @@ -1,5 +1,7 @@ #pragma once #include "tc/utils/tcAnnotations.h" +#include "tc/utils/tcFileIO.h" // fs alias + path boundary helpers +#include "tc/utils/tcLoadResult.h" // ============================================================================= // TrussC Sound @@ -84,29 +86,29 @@ class SoundBuffer : public SoundSource { // File-based decoders (implemented in tcSound_impl.cpp). // WAV / MP3 / FLAC go through ma_decoder (miniaudio); OGG goes through // stb_vorbis directly because miniaudio does not bundle a Vorbis decoder. - bool loadOgg(const std::string& path); - bool loadWav(const std::string& path); - bool loadMp3(const std::string& path); - bool loadFlac(const std::string& path); + LoadResult loadOgg(const fs::path& path); + LoadResult loadWav(const fs::path& path); + LoadResult loadMp3(const fs::path& path); + LoadResult loadFlac(const fs::path& path); // Auto-detect entry point. Dispatches to the format-specific loader // based on the file's extension (.wav / .mp3 / .ogg / .flac / .aac / // .m4a — case-insensitive). - bool load(const std::string& path); + LoadResult load(const fs::path& path); // Memory-based decoders. Format must be known (no extension to sniff). - bool loadWavFromMemory(const void* data, size_t dataSize); - bool loadMp3FromMemory(const void* data, size_t dataSize); - bool loadFlacFromMemory(const void* data, size_t dataSize); - bool loadOggFromMemory(const void* data, size_t dataSize); + LoadResult loadWavFromMemory(const void* data, size_t dataSize); + LoadResult loadMp3FromMemory(const void* data, size_t dataSize); + LoadResult loadFlacFromMemory(const void* data, size_t dataSize); + LoadResult loadOggFromMemory(const void* data, size_t dataSize); // Load AAC/M4A file (platform-specific implementation) - // Returns false on unsupported platforms - TC_PLATFORMS("macos,windows,linux,ios,web") bool loadAac(const std::string& path); + // Fails on unsupported platforms + TC_PLATFORMS("macos,windows,linux,ios,web") LoadResult loadAac(const fs::path& path); // Load AAC data from memory (platform-specific implementation) - // Returns false on unsupported platforms - TC_PLATFORMS("macos,windows,linux,ios,web") bool loadAacFromMemory(const void* data, size_t dataSize); + // Fails on unsupported platforms + TC_PLATFORMS("macos,windows,linux,ios,web") LoadResult loadAacFromMemory(const void* data, size_t dataSize); // ------------------------------------------------------------------------- // ADTS header utilities (for raw AAC from MOV containers) @@ -150,12 +152,13 @@ class SoundBuffer : public SoundSource { #endif // Load raw PCM data (16-bit signed, little-endian) - bool loadPcmFromMemory(const void* data, size_t dataSize, - int numChannels, int rate, int bitsPerSample = 16, - bool bigEndian = false) { + LoadResult loadPcmFromMemory(const void* data, size_t dataSize, + int numChannels, int rate, int bitsPerSample = 16, + bool bigEndian = false) { if (bitsPerSample != 16 && bitsPerSample != 32) { printf("SoundBuffer: unsupported bits per sample: %d\n", bitsPerSample); - return false; + return LoadResult::fail(LoadError::UnsupportedFormat, + "unsupported bits per sample: " + std::to_string(bitsPerSample)); } channels = numChannels; @@ -189,7 +192,7 @@ class SoundBuffer : public SoundSource { printf("SoundBuffer: loaded PCM from memory (%d ch, %d Hz, %zu samples)\n", channels, sampleRate, numSamples); - return true; + return LoadResult::success(); } float getDuration() const override { @@ -405,18 +408,18 @@ class SoundStream : public SoundSource { // Open the file, validate format, populate channels / sampleRate / // duration. Decoders for individual voices are opened later by the - // engine when play() is called. Returns false if the file can't be + // engine when play() is called. Fails if the file can't be // opened or the format is unsupported. Format is detected from // extension (.wav .mp3 .flac .ogg — same as SoundBuffer::load). - bool loadStream(const std::string& path, int maxPolyphony = 1); + LoadResult loadStream(const fs::path& path, int maxPolyphony = 1); float getDuration() const override { return duration_; } - const std::string& getPath() const { return path_; } + fs::path getPath() const { return path_; } int getMaxPolyphony() const { return maxPolyphony_; } private: - std::string path_; + fs::path path_; int maxPolyphony_ = 1; int encodingFormatHint_ = 0; // ma_encoding_format value, stored as int // to avoid pulling miniaudio.h into the header. @@ -1051,7 +1054,7 @@ class Sound { // can overlap. Default 1 = single-instance // (typical BGM); raise for cross-fade or // layered ambient tracks. - bool load(const std::string& path) { + LoadResult load(const fs::path& path) { // Initialize AudioEngine (only once) AudioEngine::getInstance().init(); @@ -1059,29 +1062,31 @@ class Sound { auto buf = std::make_shared(); // Determine format by extension - std::string ext = path.substr(path.find_last_of('.') + 1); - bool success = false; + std::string ext = path.extension().string(); + if (!ext.empty() && ext[0] == '.') ext.erase(0, 1); + LoadResult result = LoadResult::fail(LoadError::UnsupportedFormat, + "unsupported extension '." + ext + "'"); if (ext == "ogg" || ext == "OGG") { - success = buf->loadOgg(path); + result = buf->loadOgg(path); } else if (ext == "wav" || ext == "WAV") { - success = buf->loadWav(path); + result = buf->loadWav(path); } else if (ext == "mp3" || ext == "MP3") { - success = buf->loadMp3(path); + result = buf->loadMp3(path); } else if (ext == "flac" || ext == "FLAC") { - success = buf->loadFlac(path); + result = buf->loadFlac(path); } else if (ext == "aac" || ext == "AAC" || ext == "m4a" || ext == "M4A") { - success = buf->loadAac(path); + result = buf->loadAac(path); } else { printf("Sound: unsupported format: %s\n", ext.c_str()); } - if (!success) { + if (!result) { buffer_.reset(); - return false; + return result; } buffer_ = std::move(buf); - return true; + return LoadResult::success(); } // Stream the file from disk instead of loading it all into RAM. @@ -1097,7 +1102,7 @@ class Sound { // user apps portable we fall back to eager load() and log a warning so // the developer can branch explicitly with isStreaming() / #ifdef // __EMSCRIPTEN__ if they need to know. - TC_PLATFORMS("macos,windows,linux,android,ios") bool loadStream(const std::string& path, int maxPolyphony = 1) { + TC_PLATFORMS("macos,windows,linux,android,ios") LoadResult loadStream(const fs::path& path, int maxPolyphony = 1) { AudioEngine::getInstance().init(); #ifdef __EMSCRIPTEN__ (void)maxPolyphony; @@ -1108,12 +1113,13 @@ class Sound { return load(path); #else auto stream = std::make_shared(); - if (!stream->loadStream(path, maxPolyphony)) { + LoadResult r = stream->loadStream(path, maxPolyphony); + if (!r) { buffer_.reset(); - return false; + return r; } buffer_ = std::move(stream); - return true; + return LoadResult::success(); #endif } diff --git a/core/include/tc/sound/tcSound_impl.cpp b/core/include/tc/sound/tcSound_impl.cpp index 8373a8fd..2b165ac2 100644 --- a/core/include/tc/sound/tcSound_impl.cpp +++ b/core/include/tc/sound/tcSound_impl.cpp @@ -90,21 +90,33 @@ ma_decoder_config makeFloat32Config(ma_encoding_format hint) { return cfg; } -bool decodeFileWithMiniaudio(const std::string& path, +// Open a ma_decoder from a path. Uses the wide-path entry point on Windows so +// non-ASCII paths survive (ma_fopen would take the narrow ACP route). +ma_result maDecoderInitPath(const fs::path& path, + const ma_decoder_config* cfg, ma_decoder* dec) { +#ifdef _WIN32 + return ma_decoder_init_file_w(path.c_str(), cfg, dec); +#else + return ma_decoder_init_file(path.c_str(), cfg, dec); +#endif +} + +bool decodeFileWithMiniaudio(const fs::path& path, ma_encoding_format hint, const char* label, SoundBuffer& out) { ma_decoder decoder; ma_decoder_config cfg = makeFloat32Config(hint); - ma_result result = ma_decoder_init_file(path.c_str(), &cfg, &decoder); + std::string pathStr = internal::pathToUtf8(path); + ma_result result = maDecoderInitPath(path, &cfg, &decoder); if (result != MA_SUCCESS) { printf("SoundBuffer: failed to open %s %s (result=%d)\n", - label, path.c_str(), (int)result); + label, pathStr.c_str(), (int)result); return false; } - if (!drainDecoder(decoder, out, path.c_str())) return false; + if (!drainDecoder(decoder, out, pathStr.c_str())) return false; printf("SoundBuffer: loaded %s %s (%d ch, %d Hz, %zu samples)\n", - label, path.c_str(), out.channels, out.sampleRate, out.numSamples); + label, pathStr.c_str(), out.channels, out.sampleRate, out.numSamples); return true; } @@ -132,12 +144,24 @@ bool decodeMemoryWithMiniaudio(const void* data, size_t dataSize, // OGG Vorbis: stb_vorbis (miniaudio does not bundle a Vorbis decoder) // ----------------------------------------------------------------------------- -bool SoundBuffer::loadOgg(const std::string& path) { +LoadResult SoundBuffer::loadOgg(const fs::path& path) { + std::string pathStr = internal::pathToUtf8(path); + // stb_vorbis has no UTF-8 filename mode on Windows — open the FILE* + // ourselves (wide API) and hand it over (close_handle_on_close=TRUE). + FILE* f = internal::openFile(path, "rb"); + if (!f) { + printf("SoundBuffer: failed to open %s\n", pathStr.c_str()); + return LoadResult::fail(LoadError::FileNotFound, + "failed to open: " + pathStr); + } int error = 0; - stb_vorbis* vorbis = stb_vorbis_open_filename(path.c_str(), &error, nullptr); + stb_vorbis* vorbis = stb_vorbis_open_file(f, 1, &error, nullptr); if (!vorbis) { - printf("SoundBuffer: failed to open %s (error=%d)\n", path.c_str(), error); - return false; + fclose(f); + printf("SoundBuffer: failed to open %s (error=%d)\n", pathStr.c_str(), error); + return LoadResult::fail(LoadError::DecodeFailed, + "stb_vorbis failed to open " + pathStr + + " (error=" + std::to_string(error) + ")"); } stb_vorbis_info info = stb_vorbis_get_info(vorbis); @@ -153,43 +177,72 @@ bool SoundBuffer::loadOgg(const std::string& path) { stb_vorbis_close(vorbis); printf("SoundBuffer: loaded %s (%d ch, %d Hz, %zu samples)\n", - path.c_str(), channels, sampleRate, numSamples); + pathStr.c_str(), channels, sampleRate, numSamples); - return decoded > 0; + return decoded > 0 ? LoadResult::success() + : LoadResult::fail(LoadError::DecodeFailed, "no samples decoded"); } // ----------------------------------------------------------------------------- // WAV / MP3 / FLAC: routed through ma_decoder // ----------------------------------------------------------------------------- -bool SoundBuffer::loadWav(const std::string& path) { - return decodeFileWithMiniaudio(path, ma_encoding_format_wav, "WAV", *this); +namespace { + +// Shared file-based decode wrapper: classify missing files before handing +// the path to miniaudio (whose error codes don't distinguish the two cases +// cheaply). +LoadResult loadFileViaMiniaudio(const fs::path& path, ma_encoding_format hint, + const char* label, SoundBuffer& out) { + std::error_code ec; + if (!fs::exists(path, ec)) { + return LoadResult::fail(LoadError::FileNotFound, + "file not found: " + internal::pathToUtf8(path)); + } + if (!decodeFileWithMiniaudio(path, hint, label, out)) { + return LoadResult::fail(LoadError::DecodeFailed, + std::string(label) + " decode failed: " + + internal::pathToUtf8(path)); + } + return LoadResult::success(); } -bool SoundBuffer::loadMp3(const std::string& path) { - return decodeFileWithMiniaudio(path, ma_encoding_format_mp3, "MP3", *this); +} // namespace + +LoadResult SoundBuffer::loadWav(const fs::path& path) { + return loadFileViaMiniaudio(path, ma_encoding_format_wav, "WAV", *this); } -bool SoundBuffer::loadFlac(const std::string& path) { - return decodeFileWithMiniaudio(path, ma_encoding_format_flac, "FLAC", *this); +LoadResult SoundBuffer::loadMp3(const fs::path& path) { + return loadFileViaMiniaudio(path, ma_encoding_format_mp3, "MP3", *this); } -bool SoundBuffer::loadWavFromMemory(const void* data, size_t dataSize) { - return decodeMemoryWithMiniaudio(data, dataSize, ma_encoding_format_wav, "WAV", *this); +LoadResult SoundBuffer::loadFlac(const fs::path& path) { + return loadFileViaMiniaudio(path, ma_encoding_format_flac, "FLAC", *this); } -bool SoundBuffer::loadMp3FromMemory(const void* data, size_t dataSize) { - return decodeMemoryWithMiniaudio(data, dataSize, ma_encoding_format_mp3, "MP3", *this); +LoadResult SoundBuffer::loadWavFromMemory(const void* data, size_t dataSize) { + return decodeMemoryWithMiniaudio(data, dataSize, ma_encoding_format_wav, "WAV", *this) + ? LoadResult::success() + : LoadResult::fail(LoadError::DecodeFailed, "WAV decode from memory failed"); } -bool SoundBuffer::loadFlacFromMemory(const void* data, size_t dataSize) { - return decodeMemoryWithMiniaudio(data, dataSize, ma_encoding_format_flac, "FLAC", *this); +LoadResult SoundBuffer::loadMp3FromMemory(const void* data, size_t dataSize) { + return decodeMemoryWithMiniaudio(data, dataSize, ma_encoding_format_mp3, "MP3", *this) + ? LoadResult::success() + : LoadResult::fail(LoadError::DecodeFailed, "MP3 decode from memory failed"); } -bool SoundBuffer::loadOggFromMemory(const void* data, size_t dataSize) { +LoadResult SoundBuffer::loadFlacFromMemory(const void* data, size_t dataSize) { + return decodeMemoryWithMiniaudio(data, dataSize, ma_encoding_format_flac, "FLAC", *this) + ? LoadResult::success() + : LoadResult::fail(LoadError::DecodeFailed, "FLAC decode from memory failed"); +} + +LoadResult SoundBuffer::loadOggFromMemory(const void* data, size_t dataSize) { if (data == nullptr || dataSize == 0) { printf("SoundBuffer: empty memory range for OGG decode\n"); - return false; + return LoadResult::fail(LoadError::DecodeFailed, "empty memory range"); } int error = 0; stb_vorbis* vorbis = stb_vorbis_open_memory( @@ -197,7 +250,9 @@ bool SoundBuffer::loadOggFromMemory(const void* data, size_t dataSize) { &error, nullptr); if (!vorbis) { printf("SoundBuffer: failed to decode OGG from memory (error=%d)\n", error); - return false; + return LoadResult::fail(LoadError::DecodeFailed, + "stb_vorbis failed to decode OGG from memory (error=" + + std::to_string(error) + ")"); } stb_vorbis_info info = stb_vorbis_get_info(vorbis); @@ -212,7 +267,8 @@ bool SoundBuffer::loadOggFromMemory(const void* data, size_t dataSize) { stb_vorbis_close(vorbis); printf("SoundBuffer: decoded OGG from memory (%d ch, %d Hz, %zu samples)\n", channels, sampleRate, numSamples); - return decoded > 0; + return decoded > 0 ? LoadResult::success() + : LoadResult::fail(LoadError::DecodeFailed, "no samples decoded"); } // ----------------------------------------------------------------------------- @@ -221,10 +277,10 @@ bool SoundBuffer::loadOggFromMemory(const void* data, size_t dataSize) { // can use it directly. // ----------------------------------------------------------------------------- -bool SoundBuffer::load(const std::string& path) { +LoadResult SoundBuffer::load(const fs::path& path) { // Lowercase the extension once - const auto dot = path.find_last_of('.'); - std::string ext = (dot == std::string::npos) ? "" : path.substr(dot + 1); + std::string ext = path.extension().string(); + if (!ext.empty() && ext[0] == '.') ext.erase(0, 1); for (auto& c : ext) c = (char)std::tolower((unsigned char)c); if (ext == "wav") return loadWav(path); @@ -234,8 +290,10 @@ bool SoundBuffer::load(const std::string& path) { if (ext == "aac" || ext == "m4a") return loadAac(path); printf("SoundBuffer: unsupported extension '.%s' for %s\n", - ext.c_str(), path.c_str()); - return false; + ext.c_str(), internal::pathToUtf8(path).c_str()); + return LoadResult::fail(LoadError::UnsupportedFormat, + "unsupported extension '." + ext + "' for " + + internal::pathToUtf8(path)); } } // namespace trussc diff --git a/core/include/tc/types/tcRectNode.h b/core/include/tc/types/tcRectNode.h index abe89945..1d2ea028 100644 --- a/core/include/tc/types/tcRectNode.h +++ b/core/include/tc/types/tcRectNode.h @@ -190,7 +190,7 @@ class RectNode : public Node { float gx1 = g1.x, gy1 = g1.y, gx2 = g2.x, gy2 = g2.y; // Calculate rectangle in screen coordinates (considering DPI scale) - float dpi = sapp_dpi_scale(); + float dpi = getDpiScale(); float sx = std::min(gx1, gx2) * dpi; float sy = std::min(gy1, gy2) * dpi; float sw = std::abs(gx2 - gx1) * dpi; diff --git a/core/include/tc/utils/tcFile.h b/core/include/tc/utils/tcFile.h index 01548e9c..9c911888 100644 --- a/core/include/tc/utils/tcFile.h +++ b/core/include/tc/utils/tcFile.h @@ -18,18 +18,18 @@ namespace trussc { // ============================================================================= // Get filename from path: "dir/test.txt" -> "test.txt" -inline std::string getFileName(const std::string& path) { - return std::filesystem::path(path).filename().string(); +inline std::string getFileName(const fs::path& path) { + return path.filename().string(); } // Get filename without extension: "dir/test.txt" -> "test" -inline std::string getBaseName(const std::string& path) { - return std::filesystem::path(path).stem().string(); +inline std::string getBaseName(const fs::path& path) { + return path.stem().string(); } // Get file extension without dot: "dir/test.txt" -> "txt" -inline std::string getFileExtension(const std::string& path) { - std::string ext = std::filesystem::path(path).extension().string(); +inline std::string getFileExtension(const fs::path& path) { + std::string ext = path.extension().string(); if (!ext.empty() && ext[0] == '.') { ext = ext.substr(1); } @@ -37,17 +37,17 @@ inline std::string getFileExtension(const std::string& path) { } // Get parent directory: "dir/test.txt" -> "dir" -inline std::string getParentDirectory(const std::string& path) { - return std::filesystem::path(path).parent_path().string(); +inline std::string getParentDirectory(const fs::path& path) { + return path.parent_path().string(); } // Join paths: ("dir", "file.txt") -> "dir/file.txt" -inline std::string joinPath(const std::string& dir, const std::string& file) { - return (std::filesystem::path(dir) / file).string(); +inline std::string joinPath(const fs::path& dir, const fs::path& file) { + return (dir / file).string(); } // Get absolute path -inline std::string getAbsolutePath(const std::string& path) { +inline std::string getAbsolutePath(const fs::path& path) { return std::filesystem::absolute(path).string(); } @@ -56,21 +56,21 @@ inline std::string getAbsolutePath(const std::string& path) { // ============================================================================= // Check if file exists -inline bool fileExists(const std::string& path) { - std::string fullPath = getDataPath(path); +inline bool fileExists(const fs::path& path) { + fs::path fullPath = getDataPath(path); return std::filesystem::exists(fullPath) && std::filesystem::is_regular_file(fullPath); } // Check if directory exists -inline bool directoryExists(const std::string& path) { - std::string fullPath = getDataPath(path); +inline bool directoryExists(const fs::path& path) { + fs::path fullPath = getDataPath(path); return std::filesystem::exists(fullPath) && std::filesystem::is_directory(fullPath); } // Create directory (and parent directories if needed) // Returns true if directory was created or already exists -inline bool createDirectory(const std::string& path) { - std::string fullPath = getDataPath(path); +inline bool createDirectory(const fs::path& path) { + fs::path fullPath = getDataPath(path); try { if (std::filesystem::exists(fullPath)) { return std::filesystem::is_directory(fullPath); @@ -84,9 +84,9 @@ inline bool createDirectory(const std::string& path) { // List files and directories in a directory // Returns vector of filenames (not full paths) -inline std::vector listDirectory(const std::string& path) { +inline std::vector listDirectory(const fs::path& path) { std::vector result; - std::string fullPath = getDataPath(path); + fs::path fullPath = getDataPath(path); if (!std::filesystem::exists(fullPath) || !std::filesystem::is_directory(fullPath)) { return result; @@ -104,8 +104,8 @@ inline std::vector listDirectory(const std::string& path) { } // Remove file -inline bool removeFile(const std::string& path) { - std::string fullPath = getDataPath(path); +inline bool removeFile(const fs::path& path) { + fs::path fullPath = getDataPath(path); try { return std::filesystem::remove(fullPath); } catch (const std::exception& e) { @@ -115,8 +115,8 @@ inline bool removeFile(const std::string& path) { } // Get file size in bytes (-1 on error) -inline int64_t getFileSize(const std::string& path) { - std::string fullPath = getDataPath(path); +inline int64_t getFileSize(const fs::path& path) { + fs::path fullPath = getDataPath(path); try { if (!std::filesystem::exists(fullPath)) return -1; return static_cast(std::filesystem::file_size(fullPath)); @@ -130,9 +130,11 @@ inline int64_t getFileSize(const std::string& path) { // ============================================================================= // Load entire text file into string -inline std::string loadTextFile(const std::string& path) { - std::string fullPath = getDataPath(path); - std::ifstream file(fullPath); +// (binary mode: the returned string is the file's exact bytes on every +// platform; Windows text mode would silently fold \r\n into \n) +inline std::string loadTextFile(const fs::path& path) { + fs::path fullPath = getDataPath(path); + std::ifstream file(fullPath, std::ios::binary); if (!file.is_open()) { logError() << "Cannot open file: " << path; return ""; @@ -144,9 +146,11 @@ inline std::string loadTextFile(const std::string& path) { } // Save string to text file -inline bool saveTextFile(const std::string& path, const std::string& content) { - std::string fullPath = getDataPath(path); - std::ofstream file(fullPath); +// (binary mode: what you pass is what lands on disk on every platform; +// Windows text mode would expand \n to \r\n, changing the file size) +inline bool saveTextFile(const fs::path& path, const std::string& content) { + fs::path fullPath = getDataPath(path); + std::ofstream file(fullPath, std::ios::binary); if (!file.is_open()) { logError() << "Cannot create file: " << path; return false; @@ -157,9 +161,9 @@ inline bool saveTextFile(const std::string& path, const std::string& content) { } // Append string to text file -inline bool appendToFile(const std::string& path, const std::string& content) { - std::string fullPath = getDataPath(path); - std::ofstream file(fullPath, std::ios::app); +inline bool appendToFile(const fs::path& path, const std::string& content) { + fs::path fullPath = getDataPath(path); + std::ofstream file(fullPath, std::ios::app | std::ios::binary); if (!file.is_open()) { logError() << "Cannot open file for append: " << path; return false; @@ -196,9 +200,9 @@ class FileWriter { } // Open file (append = true to append to existing file) - bool open(const std::string& path, bool append = false) { + bool open(const fs::path& path, bool append = false) { close(); - std::string fullPath = getDataPath(path); + fs::path fullPath = getDataPath(path); auto mode = std::ios::out | std::ios::binary; if (append) mode |= std::ios::app; @@ -304,9 +308,9 @@ class FileReader { } // Open file for reading - bool open(const std::string& path) { + bool open(const fs::path& path) { close(); - std::string fullPath = getDataPath(path); + fs::path fullPath = getDataPath(path); file_.open(fullPath, std::ios::in | std::ios::binary); if (!file_.is_open()) { logError() << "FileReader: Cannot open file: " << path; diff --git a/core/include/tc/utils/tcFileDialog.h b/core/include/tc/utils/tcFileDialog.h index 0db8d574..4067662d 100644 --- a/core/include/tc/utils/tcFileDialog.h +++ b/core/include/tc/utils/tcFileDialog.h @@ -13,6 +13,7 @@ // ============================================================================= #include +#include "tc/utils/tcFileIO.h" // fs alias #include #include @@ -32,8 +33,8 @@ namespace trussc { // Dialog result for load/save dialogs struct FileDialogResult { - std::string filePath; // Full path - std::string fileName; // Filename only + fs::path filePath; // Full path + fs::path fileName; // Filename only bool success = false; // true if not cancelled }; @@ -64,12 +65,12 @@ void confirmDialogAsync(const std::string& title, // ----------------------------------------------------------------------------- TC_PLATFORMS("macos,windows,linux,android") TC_SYNC_DIALOG_UNAVAILABLE FileDialogResult loadDialog(const std::string& title = "", const std::string& message = "", - const std::string& defaultPath = "", + const fs::path& defaultPath = {}, bool folderSelection = false); void loadDialogAsync(const std::string& title, const std::string& message, - const std::string& defaultPath, + const fs::path& defaultPath, bool folderSelection, std::function callback); @@ -79,13 +80,13 @@ void loadDialogAsync(const std::string& title, // ----------------------------------------------------------------------------- TC_PLATFORMS("macos,windows,linux,android") TC_SYNC_DIALOG_UNAVAILABLE FileDialogResult saveDialog(const std::string& title = "", const std::string& message = "", - const std::string& defaultPath = "", - const std::string& defaultName = ""); + const fs::path& defaultPath = {}, + const fs::path& defaultName = {}); void saveDialogAsync(const std::string& title, const std::string& message, - const std::string& defaultPath, - const std::string& defaultName, + const fs::path& defaultPath, + const fs::path& defaultName, std::function callback); } // namespace trussc diff --git a/core/include/tc/utils/tcFileIO.h b/core/include/tc/utils/tcFileIO.h new file mode 100644 index 00000000..4726cf9b --- /dev/null +++ b/core/include/tc/utils/tcFileIO.h @@ -0,0 +1,67 @@ +#pragma once + +// ============================================================================= +// tcFileIO.h - fs::path <-> C-library boundary helpers (internal) +// ============================================================================= +// +// TrussC carries file paths as fs::path end to end. C libraries (stb, +// miniaudio, fopen) only take narrow char* strings, which on Windows are +// interpreted in the active code page — non-ASCII paths get mangled. These +// helpers do the conversion at the last moment, right at the library call: +// +// - pathToUtf8() : path -> UTF-8 bytes (for UTF-8-aware sinks such as +// stb with STBI(W)_WINDOWS_UTF8, NSString, FFmpeg) +// - utf8ToPath() : UTF-8 bytes -> path (for UTF-8 sources such as JSON) +// - openFile() : fopen that takes fs::path (wide API on Windows) +// +// Everything here is internal plumbing, not public API. + +#include +#include +#include +#include + +namespace trussc { + +namespace fs = std::filesystem; + +namespace internal { + +// Convert a path to UTF-8 bytes. On POSIX the native encoding already is +// UTF-8; on Windows the native encoding is UTF-16, so go through u8string(). +inline std::string pathToUtf8(const fs::path& p) { +#ifdef _WIN32 + auto u8 = p.u8string(); // std::u8string (char8_t) in C++20 + return std::string(u8.begin(), u8.end()); +#else + return p.string(); +#endif +} + +// Convert UTF-8 bytes to a path. fs::path(std::string) would interpret the +// bytes in the active code page on Windows — this never does. +inline fs::path utf8ToPath(std::string_view utf8) { +#ifdef _WIN32 + return fs::path(std::u8string(utf8.begin(), utf8.end())); +#else + return fs::path(std::string(utf8)); +#endif +} + +// fopen that accepts fs::path. Uses _wfopen on Windows so non-ASCII paths +// survive; mode strings are short ASCII ("rb", "wb", ...). +inline std::FILE* openFile(const fs::path& p, const char* mode) { +#ifdef _WIN32 + wchar_t wmode[8]; + size_t i = 0; + for (; mode[i] != '\0' && i < 7; ++i) wmode[i] = static_cast(mode[i]); + wmode[i] = L'\0'; + return _wfopen(p.c_str(), wmode); +#else + return std::fopen(p.c_str(), mode); +#endif +} + +} // namespace internal + +} // namespace trussc diff --git a/core/include/tc/utils/tcJson.h b/core/include/tc/utils/tcJson.h index 1d70c3c4..4b8459ee 100644 --- a/core/include/tc/utils/tcJson.h +++ b/core/include/tc/utils/tcJson.h @@ -20,8 +20,8 @@ using Json = nlohmann::json; // JSON file loading // Relative paths are resolved via getDataPath (like oF) // --------------------------------------------------------------------------- -inline Json loadJson(const std::string& path) { - std::string fullPath = getDataPath(path); +inline Json loadJson(const fs::path& path) { + fs::path fullPath = getDataPath(path); std::ifstream file(fullPath); if (!file.is_open()) { logError() << "Cannot open JSON file: " << path; @@ -42,8 +42,8 @@ inline Json loadJson(const std::string& path) { // JSON file writing // Relative paths are resolved via getDataPath (like oF) // --------------------------------------------------------------------------- -inline bool saveJson(const Json& j, const std::string& path, int indent = 2) { - std::string fullPath = getDataPath(path); +inline bool saveJson(const Json& j, const fs::path& path, int indent = 2) { + fs::path fullPath = getDataPath(path); std::ofstream file(fullPath); if (!file.is_open()) { logError() << "Cannot create JSON file: " << path; diff --git a/core/include/tc/utils/tcLoadResult.h b/core/include/tc/utils/tcLoadResult.h new file mode 100644 index 00000000..92f1aaa7 --- /dev/null +++ b/core/include/tc/utils/tcLoadResult.h @@ -0,0 +1,61 @@ +#pragma once +#include "tc/utils/tcAnnotations.h" + +// ============================================================================= +// LoadResult — unified result type for resource loading (Image, Pixels, +// SoundBuffer, SoundPlayer, VideoPlayer, Font, ...). +// +// Replaces the old `bool` returns. `explicit operator bool()` keeps the +// common pattern working unchanged: +// +// if (img.load("photo.png")) { ... } // still fine +// if (!snd.load("beep.wav")) { +// logError() << snd.load("beep.wav").message; // (illustrative) +// } +// +// Error taxonomy is deliberately coarse for now (v0.7): the enum can gain +// values and messages can get richer without breaking anything. What CAN'T +// change compatibly is the return type — which is why the shape lands in +// the v0.7 breaking window. +// ============================================================================= + +#include + +namespace trussc { + +// What went wrong during a load. None == success. +enum class LoadError { + None = 0, // success + FileNotFound, // the (resolved) path does not exist / could not be opened + UnsupportedFormat, // extension / container not supported on this platform + DecodeFailed, // the decoder rejected the data (corrupt / truncated / wrong codec) + Unknown, // anything the loader could not classify +}; + +// Result of a load operation. Truthy on success. +struct LoadResult { + LoadError error = LoadError::None; + std::string message; // human-readable detail; may be empty + + bool ok() const { return error == LoadError::None; } + explicit operator bool() const { return ok(); } + + static LoadResult success() { return {}; } + static LoadResult fail(LoadError e, std::string msg = {}) { + return LoadResult{e, std::move(msg)}; + } +}; + +// Short label for a LoadError value ("FileNotFound", ...). For log messages. +inline const char* loadErrorName(LoadError e) { + switch (e) { + case LoadError::None: return "None"; + case LoadError::FileNotFound: return "FileNotFound"; + case LoadError::UnsupportedFormat: return "UnsupportedFormat"; + case LoadError::DecodeFailed: return "DecodeFailed"; + case LoadError::Unknown: return "Unknown"; + } + return "Unknown"; +} + +} // namespace trussc diff --git a/core/include/tc/utils/tcLog.h b/core/include/tc/utils/tcLog.h index 24172403..c450d114 100644 --- a/core/include/tc/utils/tcLog.h +++ b/core/include/tc/utils/tcLog.h @@ -16,6 +16,7 @@ // Uses Event system #include "../events/tcEvent.h" #include "../events/tcEventListener.h" +#include "tcFileIO.h" // fs alias + pathToUtf8 namespace trussc { @@ -133,16 +134,16 @@ class Logger { // === File settings === - bool setLogFile(const std::string& path) { + bool setLogFile(const fs::path& path) { closeFile(); fileStream_.open(path, std::ios::app); if (!fileStream_.is_open()) { - log(LogLevel::Error, "Failed to open log file: " + path); + log(LogLevel::Error, "Failed to open log file: " + internal::pathToUtf8(path)); return false; } - filePath_ = path; + filePath_ = internal::pathToUtf8(path); // Register file listener fileListener_ = onLog.listen([this](LogEventArgs& e) { @@ -214,7 +215,7 @@ inline void setFileLogLevel(LogLevel level) { getLogger().setFileLogLevel(level); } -inline bool setLogFile(const std::string& path) { +inline bool setLogFile(const fs::path& path) { return getLogger().setLogFile(path); } @@ -236,7 +237,7 @@ inline void tcSetConsoleLogLevel(LogLevel level) { setConsoleLogLevel(level); } inline void tcSetFileLogLevel(LogLevel level) { setFileLogLevel(level); } [[deprecated("Use setLogFile() instead. Will be removed in v1.0.0")]] -inline bool tcSetLogFile(const std::string& path) { return setLogFile(path); } +inline bool tcSetLogFile(const fs::path& path) { return setLogFile(path); } [[deprecated("Use closeLogFile() instead. Will be removed in v1.0.0")]] inline void tcCloseLogFile() { closeLogFile(); } diff --git a/core/include/tc/utils/tcMCP.h b/core/include/tc/utils/tcMCP.h index b0e1f1a5..23adfa85 100644 --- a/core/include/tc/utils/tcMCP.h +++ b/core/include/tc/utils/tcMCP.h @@ -37,7 +37,7 @@ namespace mcp { // --------------------------------------------------------------------------- // Deferred tool responses // --------------------------------------------------------------------------- -// Some tools (e.g. get_screenshot) need state that only exists AFTER present() +// Some tools (e.g. tc_get_screenshot) need state that only exists AFTER present() // — during the frame, drawing is still deferred and a readback would be blank. // Such a handler calls deferToolResultUntilAfterFrame(): processHttpQueue() // stashes the request instead of answering it, and drainDeferredResponses() @@ -47,16 +47,26 @@ namespace mcp { namespace detail { +// The promise carries a THUNK, not the reply string: the blocked HTTP worker +// executes it (future.get()()) to obtain the reply. For ordinary tools the +// thunk just returns a string built on the main thread; two-stage tools (see +// deferToolResultTwoStage) put their heavy encode work inside the thunk so it +// runs on the HTTP worker — off the frame loop — while main only did the +// readback. +using ReplyThunk = std::function; + struct DeferredResponse { - std::shared_ptr> response; // unblocks the HTTP worker - std::function makeEnvelope; // builds the JSON-RPC reply + std::shared_ptr> response; // unblocks the HTTP worker + std::function makeEnvelope; // main stage → worker thunk }; struct DeferralState { bool requested = false; // set by deferToolResultUntilAfterFrame() - std::function produce; // tool content producer + std::function produce; // tool content producer (runs fully on main) + bool twoStageRequested = false; // set by deferToolResultTwoStage() + std::function()> produceTwoStage; // main stage → worker stage bool hasEnvelope = false; // set by handleToolsCall(), read by processHttpQueue() - std::function envelope; // full JSON-RPC reply builder + std::function envelope; // JSON-RPC reply builder (main part) }; inline DeferralState& deferralState() { static DeferralState s; return s; } @@ -89,19 +99,32 @@ inline void deferToolResultUntilAfterFrame(std::function produce) { detail::deferralState().produce = std::move(produce); } -// Run all deferred tool producers and unblock their HTTP workers. Call from an -// events().afterFrame listener (i.e. after present()). +// Two-stage variant for tools whose result is expensive to build (e.g. +// tc_get_thumbnail): `mainStage` runs at the afterFrame safe point on the MAIN +// thread — do the GPU readback there and nothing else — and the closure it +// returns runs on the HTTP worker thread that is already sitting blocked on +// this reply. Put the heavy work (downscale, encode) in that closure and the +// frame loop never pays for it. +inline void deferToolResultTwoStage(std::function()> mainStage) { + detail::deferralState().twoStageRequested = true; + detail::deferralState().produceTwoStage = std::move(mainStage); +} + +// Run all deferred main stages and unblock their HTTP workers. Call from an +// events().afterFrame listener (i.e. after present()). Only the main part of +// each envelope runs here; the returned thunk executes on the HTTP worker. inline void drainDeferredResponses() { auto& list = detail::deferredResponses(); if (list.empty()) return; for (auto& d : list) { - std::string envelope; + detail::ReplyThunk thunk; try { - envelope = d.makeEnvelope(); + thunk = d.makeEnvelope(); } catch (const std::exception& e) { - envelope = std::string("{\"error\":\"deferred response failed: ") + e.what() + "\"}"; + std::string err = std::string("{\"error\":\"deferred response failed: ") + e.what() + "\"}"; + thunk = [err]() { return err; }; } - d.response->set_value(envelope); + d.response->set_value(std::move(thunk)); } list.clear(); } @@ -169,6 +192,11 @@ class Server { // --- Registration API --- void registerTool(const Tool& tool) { + // Last registration wins. Warn, because silently replacing a standard + // tool (they register before setup()) is a hard bug to spot. + if (tools_.count(tool.name)) { + logWarning("MCP") << "tool '" << tool.name << "' re-registered; previous handler replaced"; + } tools_[tool.name] = tool; } @@ -283,8 +311,10 @@ class Server { try { auto& ds = detail::deferralState(); ds.requested = false; + ds.twoStageRequested = false; - // Execute tool handler (may call deferToolResultUntilAfterFrame()) + // Execute tool handler (may call deferToolResultUntilAfterFrame() + // or deferToolResultTwoStage()) json content = tools_[name].handler(args); // Handler asked to produce its result after the next present(). @@ -292,8 +322,31 @@ class Server { auto produce = std::move(ds.produce); ds.requested = false; ds.hasEnvelope = true; - ds.envelope = [formatResult, produce]() -> std::string { - return formatResult(produce()); + // Everything runs on main at drain time; the worker thunk just + // hands back the prebuilt string. + ds.envelope = [formatResult, produce]() -> detail::ReplyThunk { + std::string reply = formatResult(produce()); + return [reply]() { return reply; }; + }; + return std::string(); // processHttpQueue() stashes the reply + } + + // Two-stage: main stage at drain time (readback), returned closure + // — wrapped so the JSON-RPC formatting ALSO happens on the worker — + // executes on the blocked HTTP worker thread. + if (ds.twoStageRequested) { + auto mainStage = std::move(ds.produceTwoStage); + ds.twoStageRequested = false; + ds.hasEnvelope = true; + ds.envelope = [formatResult, mainStage]() -> detail::ReplyThunk { + std::function workerStage = mainStage(); + return [formatResult, workerStage]() -> std::string { + try { + return formatResult(workerStage()); + } catch (const std::exception& e) { + return std::string("{\"error\":\"deferred worker stage failed: ") + e.what() + "\"}"; + } + }; }; return std::string(); // processHttpQueue() stashes the reply } @@ -371,7 +424,10 @@ class Server { struct McpRequest { std::string body; - std::shared_ptr> response; + // Carries a thunk the HTTP worker executes to obtain the reply string — + // see detail::ReplyThunk. Normal replies are prebuilt (thunk just returns + // them); two-stage tool replies do their heavy encode inside the thunk. + std::shared_ptr> response; }; namespace detail { @@ -443,7 +499,7 @@ inline void startHttpServer(int port = 0, const std::string& host = "localhost", } } - auto p = std::make_shared>(); + auto p = std::make_shared>(); auto f = p->get_future(); McpRequest mcpReq; @@ -452,8 +508,16 @@ inline void startHttpServer(int port = 0, const std::string& host = "localhost", detail::getHttpChannel().send(std::move(mcpReq)); - // Block until main thread processes the request - std::string result = f.get(); + // Block until main thread processes the request, then execute the + // reply thunk HERE — heavy two-stage work (thumbnail encode etc.) runs + // on this worker thread, not the frame loop. + detail::ReplyThunk thunk = f.get(); + std::string result; + try { + result = thunk(); + } catch (const std::exception& e) { + result = std::string("{\"error\":\"reply construction failed: ") + e.what() + "\"}"; + } res.set_content(result, "application/json"); }); @@ -505,7 +569,9 @@ inline void stopHttpServer() { { auto& list = detail::deferredResponses(); for (auto& d : list) { - d.response->set_value("{\"error\":\"server shutting down\"}"); + d.response->set_value([]() -> std::string { + return "{\"error\":\"server shutting down\"}"; + }); } list.clear(); } @@ -544,7 +610,7 @@ inline void processHttpQueue() { // Return empty JSON-RPC response for notifications result = "{}"; } - req.response->set_value(result); + req.response->set_value([result]() { return result; }); } } diff --git a/core/include/tc/utils/tcStandardTools.h b/core/include/tc/utils/tcStandardTools.h index 1eb38c59..40f7f803 100644 --- a/core/include/tc/utils/tcStandardTools.h +++ b/core/include/tc/utils/tcStandardTools.h @@ -6,11 +6,34 @@ #include "tcMCP.h" #include "tcUtils.h" +#include "tcTime.h" #include "tcJsonReflect.h" #include "../events/tcCoreEvents.h" #include "stb/stb_image_write.h" #include "../graphics/tcPixels.h" +#include +#include #include +#include +#include + +// Platform bits for detail::currentPid() / detail::processRssBytes() +#if defined(__APPLE__) +#include +#include +#elif defined(_WIN32) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#elif !defined(__EMSCRIPTEN__) +#include +#include +#endif // Forward declaration for stbi_write_png_to_mem (missing in older stb_image_write.h headers) extern "C" unsigned char *stbi_write_png_to_mem(const unsigned char *pixels, int stride_bytes, int x, int y, int n, int *out_len); @@ -26,7 +49,7 @@ namespace trussc { // members (same encoding as JsonWriteReflector), mods ({type, members} each), // and the children in draw order. maxDepth limits recursion (-1 = unlimited, // 0 = this node only); where children are cut off, "childCount" says how many -// were omitted so a caller can drill in with another get_node_tree(id) call. +// were omitted so a caller can drill in with another tc_get_node_tree(id) call. inline Json nodeToJson(Node& node, int maxDepth = -1) { Json j = Json::object(); j["type"] = node.getTypeName(); @@ -64,62 +87,438 @@ inline Json nodeToJson(Node& node, int maxDepth = -1) { namespace mcp { +namespace detail { + +// Area-average (box filter) downscale for interleaved 8-bit images. Good +// enough for monitoring thumbnails at any ratio; never called to upscale. +inline void downscaleImage(const unsigned char* src, int srcW, int srcH, int channels, + std::vector& dst, int dstW, int dstH) { + dst.resize(size_t(dstW) * dstH * channels); + for (int y = 0; y < dstH; ++y) { + int sy0 = int(size_t(y) * srcH / dstH); + int sy1 = std::max(int(size_t(y + 1) * srcH / dstH), sy0 + 1); + for (int x = 0; x < dstW; ++x) { + int sx0 = int(size_t(x) * srcW / dstW); + int sx1 = std::max(int(size_t(x + 1) * srcW / dstW), sx0 + 1); + uint64_t acc[4] = {0, 0, 0, 0}; + for (int sy = sy0; sy < sy1; ++sy) { + const unsigned char* p = src + (size_t(sy) * srcW + sx0) * channels; + for (int sx = sx0; sx < sx1; ++sx, p += channels) + for (int c = 0; c < channels; ++c) acc[c] += p[c]; + } + uint64_t n = uint64_t(sy1 - sy0) * (sx1 - sx0); + unsigned char* o = &dst[(size_t(y) * dstW + x) * channels]; + for (int c = 0; c < channels; ++c) o[c] = (unsigned char)(acc[c] / n); + } + } +} + +// Optional downscale + PNG/JPEG encode + Base64. Runs on the HTTP worker +// inside two-stage deferral thunks (tc_get_screenshot / tc_get_status_image), +// never on the main loop. reqWidth <= 0 means "no resize"; never upscales. +inline json pixelsToImageJson(const trussc::Pixels& px, const std::string& format, + int reqWidth, int quality) { + int srcW = px.getWidth(), srcH = px.getHeight(); + int ch = px.getChannels(); + int dstW = (reqWidth > 0) ? std::min(reqWidth, srcW) : srcW; + int dstH = std::max(1, (int)std::lround((double)srcH * dstW / srcW)); + + const unsigned char* data = px.getData(); + std::vector scaled; + if (dstW != srcW) { + downscaleImage(px.getData(), srcW, srcH, ch, scaled, dstW, dstH); + data = scaled.data(); + } + + std::vector out; + if (format == "png") { + int len = 0; + unsigned char* png = stbi_write_png_to_mem(data, 0, dstW, dstH, ch, &len); + if (png) { + out.assign(png, png + len); + std::free(png); + } + } else { + stbi_write_jpg_to_func( + [](void* ctx, void* d, int size) { + auto* v = static_cast*>(ctx); + auto* b = static_cast(d); + v->insert(v->end(), b, b + size); + }, + &out, dstW, dstH, ch, data, quality); + } + if (out.empty()) { + return json{{"status", "error"}, {"message", format + " encode failed"}}; + } + return json{{"mimeType", format == "png" ? "image/png" : "image/jpeg"}, + {"data", trussc::toBase64(out)}, + {"width", dstW}, + {"height", dstH}}; +} + +// Wrap an encoded image as MCP-standard content blocks: an image block +// (clients like Claude Code render it inline) followed by a text block with +// the metadata (width/height as JSON, for scripted consumers). Error objects +// pass through untouched and get the normal text wrapping. +inline json imageContentResult(json img) { + if (!img.contains("data")) return img; // error object + json meta = json::object(); + for (auto& key : {"width", "height"}) { + if (img.contains(key)) meta[key] = img[key]; + } + return json::array({ + json{{"type", "image"}, {"data", img["data"]}, {"mimeType", img["mimeType"]}}, + json{{"type", "text"}, {"text", meta.dump()}}, + }); +} + +// App-published ops status registries (see mcp::status / statusGraph / +// statusImage below). Main-thread only: registration happens in +// setup()/update() and the getters run inside MCP tool handlers, which +// execute on the main loop. +struct StatusEntry { + std::string name; + std::function getter; // returns a number or string json value + bool graph = false; // display hint: plot as time series +}; + +inline std::vector& statusRegistry() { + static std::vector reg; + return reg; +} + +struct StatusImageEntry { + std::string name; + std::function getter; +}; + +inline std::vector& statusImageRegistry() { + static std::vector reg; + return reg; +} + +inline void addStatusEntry(StatusEntry entry) { + auto& reg = statusRegistry(); + for (auto& e : reg) { + if (e.name == entry.name) { e = std::move(entry); return; } + } + reg.push_back(std::move(entry)); +} + +// Operator-alert queue (see mcp::alert below). Unlike the status getters, +// alerts can fire from any thread — a sensor callback, an async timer — so +// the queue carries its own lock. Bounded: past 100 pending, oldest drop. +inline std::mutex& alertMutex() { + static std::mutex m; + return m; +} + +inline std::deque& alertQueue() { + static std::deque q; + return q; +} + +// Process identity + real memory footprint for tc_get_health. RSS is the +// resident set of the whole process — the number that matters for leak +// hunting and for telling "the app grew" from "the OS ran out". +inline int64_t currentPid() { +#if defined(_WIN32) + return (int64_t)GetCurrentProcessId(); +#elif defined(__EMSCRIPTEN__) + return 0; +#else + return (int64_t)getpid(); +#endif +} + +inline int64_t processRssBytes() { +#if defined(__APPLE__) + mach_task_basic_info info; + mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT; + if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, + (task_info_t)&info, &count) == KERN_SUCCESS) { + return (int64_t)info.resident_size; + } + return 0; +#elif defined(_WIN32) + PROCESS_MEMORY_COUNTERS pmc; + if (K32GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) { + return (int64_t)pmc.WorkingSetSize; + } + return 0; +#elif defined(__EMSCRIPTEN__) + return 0; +#else + // Linux: /proc/self/statm field 2 = resident pages + std::ifstream in("/proc/self/statm"); + long total = 0, resident = 0; + if (in >> total >> resident) { + return (int64_t)resident * sysconf(_SC_PAGESIZE); + } + return 0; +#endif +} + +} // namespace detail + // --------------------------------------------------------------------------- -// Inspection Tools (read-only, always available when MCP is enabled) +// App-published ops status +// +// Apps expose custom monitoring data with one line per value; a supervisor +// (e.g. anchorbolt start) discovers the tc_get_status tool via tools/list +// and forwards the payload to its server. No supervisor-side configuration. +// +// mcp::status("scene", [&]{ return sceneName; }); // shown as-is +// mcp::statusGraph("visitors", [&]{ return visitorCount; }); // plotted over time +// mcp::statusImage("entranceCam", [&]{ return camPixels; }); +// +// Registering the same name again replaces the previous entry. // --------------------------------------------------------------------------- -inline void registerInspectionTools() { +inline void status(const std::string& name, std::function getter) { + detail::addStatusEntry({name, [getter]() { return json(getter()); }, false}); +} - tool("get_screenshot", "Get screenshot as Base64 PNG") - .bind(std::function([]() -> json { - // Defer the actual capture+encode to just after present(): during a - // frame nothing has been rendered yet (drawing is deferred), so a - // readback here would be blank (black on Linux). The producer below - // runs at the afterFrame safe point and its result is what the MCP - // client receives. - mcp::deferToolResultUntilAfterFrame([]() -> json { - // Capture screen to pixels - Pixels pixels; - if (!grabScreen(pixels)) { - return json{{"status", "error"}, {"message", "Failed to grab screen"}}; - } +inline void status(const std::string& name, std::function getter) { + detail::addStatusEntry({name, [getter]() { return json(getter()); }, false}); +} - // Encode to PNG in memory - int pngSize = 0; - unsigned char* pngData = stbi_write_png_to_mem( - pixels.getData(), 0, - pixels.getWidth(), pixels.getHeight(), pixels.getChannels(), - &pngSize); +inline void statusGraph(const std::string& name, std::function getter) { + detail::addStatusEntry({name, [getter]() { return json(getter()); }, true}); +} - if (!pngData) { - return json{{"status", "error"}, {"message", "Failed to encode PNG"}}; - } +inline void statusImage(const std::string& name, std::function getter) { + auto& reg = detail::statusImageRegistry(); + for (auto& e : reg) { + if (e.name == name) { e.getter = getter; return; } + } + reg.push_back({name, getter}); +} - // Convert to Base64 - std::string b64 = toBase64(pngData, pngSize); +// --------------------------------------------------------------------------- +// Operator alerts +// +// mcp::alert("IR camera disconnected!"); +// +// Queues a message for the supervisor: anchorbolt drains tc_get_alerts on +// its health cadence and forwards each entry to its notification sinks +// (Slack / Discord / ntfy...), so this can literally end up on someone's +// phone. Deliberately named ALERT, not notify — it is for "a human should +// hear about this", not a general message bus. The message is also written +// to the log (so it survives locally even with no supervisor attached). +// Thread-safe; callable from sensor callbacks / async timers. +// --------------------------------------------------------------------------- + +inline void alert(const std::string& msg) { + trussc::logWarning("alert") << msg; + std::lock_guard lock(detail::alertMutex()); + auto& q = detail::alertQueue(); + q.push_back(json{{"at", trussc::getTimestampString("%Y-%m-%dT%H:%M:%S")}, + {"text", msg}}); + if (q.size() > 100) q.pop_front(); +} + +// --------------------------------------------------------------------------- +// Inspection Tools (read-only, always available when MCP is enabled) +// --------------------------------------------------------------------------- + +inline void registerInspectionTools() { + + // Resolve the optional MCP "window" arg (0 = main window; 1..N = open + // secondary windows in the order tc_list_windows reports). Returns the + // WindowContext to capture from, or null on a bad index (error filled in). + // Must run at the afterFrame safe point (same frame the index was read). + auto resolveWindowCtx = [](int windowIdx, json& err) -> trussc::internal::WindowContext* { + if (windowIdx == 0) return &trussc::internal::mainWindowContext(); + auto wins = trussc::internal::openWindows(); + if (windowIdx < 0 || (size_t)windowIdx > wins.size()) { + err = json{{"status", "error"}, + {"message", "window index out of range (0=main, 1.." + + std::to_string(wins.size()) + " secondary)"}}; + return nullptr; + } + return &wins[(size_t)windowIdx - 1]->context(); + }; - // Free PNG data - std::free(pngData); + tool("tc_list_windows", "List open windows (index 0 = main; use the index as the 'window' arg of tc_get_screenshot / tc_save_screenshot)") + .bind(std::function([]() -> json { + json arr = json::array(); + arr.push_back(json{{"index", 0}, {"main", true}, + {"width", getWindowWidth()}, {"height", getWindowHeight()}}); + int i = 1; + for (auto* w : trussc::internal::openWindows()) { + arr.push_back(json{{"index", i++}, {"main", false}, + {"title", w->getTitle()}, + {"width", w->getWidth()}, {"height", w->getHeight()}}); + } + return json{{"windows", arr}}; + })); - return json{ - {"mimeType", "image/png"}, - {"data", b64} + tool("tc_get_screenshot", "Screenshot as Base64 PNG/JPEG. Defaults to full-resolution PNG; pass width for a downscaled monitoring thumbnail (aspect preserved, never upscales) and format 'jpg' for small payloads. Cheap to poll at any settings: only the framebuffer readback touches the frame loop — downscale + encode run on the HTTP worker thread, so polling does not stutter the app.") + .arg("format", "'png' (default, lossless) or 'jpg'", false) + .arg("width", "Target width in pixels, aspect preserved (clamped 16-4096; never upscales; omit = full resolution)", false) + .arg("quality", "JPEG quality 1-100 (default 75; ignored for png)", false) + .arg("window", "Window index from tc_list_windows (default 0 = main)", false) + .bind([resolveWindowCtx](const json& args) -> json { + std::string format = "png"; + if (args.contains("format") && args.at("format").is_string()) { + format = args.at("format").get(); + if (format != "png" && format != "jpg" && format != "jpeg") { + return json{{"status", "error"}, + {"message", "format must be 'png' or 'jpg'"}}; + } + if (format == "jpeg") format = "jpg"; + } + int reqWidth = 0; // 0 = full resolution + int quality = 75; + if (args.contains("width") && args.at("width").is_number()) + reqWidth = std::clamp(args.at("width").get(), 16, 4096); + if (args.contains("quality") && args.at("quality").is_number()) + quality = std::clamp(args.at("quality").get(), 1, 100); + const int windowIdx = args.value("window", 0); + + // Two-stage deferral: the main stage runs at the afterFrame safe + // point (mid-frame the framebuffer is blank — drawing is deferred) + // and only grabs the pixels; the returned closure — downscale + + // encode + Base64 — runs on the HTTP worker blocked on this reply. + // Window targeting: switch the current window context around the + // readback so captureWindow reads the target's presented drawable. + mcp::deferToolResultTwoStage([resolveWindowCtx, windowIdx, format, reqWidth, quality]() -> std::function { + json err; + auto* ctx = resolveWindowCtx(windowIdx, err); + if (!ctx) return [err]() -> json { return err; }; + auto* prev = trussc::internal::currentWindowCtx; + trussc::internal::currentWindowCtx = ctx; + auto px = std::make_shared(); + bool grabbed = trussc::grabScreen(*px); + trussc::internal::currentWindowCtx = prev; + return [px, grabbed, format, reqWidth, quality]() -> json { + if (!grabbed) { + return json{{"status", "error"}, {"message", "Failed to grab screen"}}; + } + return detail::imageContentResult(detail::pixelsToImageJson(*px, format, reqWidth, quality)); }; }); return json(nullptr); // ignored — deferred result is sent instead - })); + }); - tool("save_screenshot", "Save screenshot to file") + tool("tc_save_screenshot", "Save screenshot to file") .arg("path", "File path") - .bind([](std::string path) { - if (trussc::saveScreenshot(path)) { - return json{{"status", "ok"}, {"path", path}}; - } else { + .arg("window", "Window index from list_windows (default 0 = main)", false) + .bind([resolveWindowCtx](const json& args) -> json { + // JSON strings are UTF-8 — convert explicitly (fs::path(string) + // would interpret them in the ACP on Windows) + const std::string path = args.at("path").get(); + const int windowIdx = args.value("window", 0); + if (windowIdx == 0) { + // Main window: the classic queued path (drained after present) + if (trussc::saveScreenshot(trussc::internal::utf8ToPath(path))) { + return json{{"status", "ok"}, {"path", path}}; + } return json{{"status", "error"}, {"message", "Failed to save screenshot"}}; } + // Secondary window: defer, then capture that window's drawable + mcp::deferToolResultUntilAfterFrame([resolveWindowCtx, windowIdx, path]() -> json { + json err; + auto* ctx = resolveWindowCtx(windowIdx, err); + if (!ctx) return err; + auto* prev = trussc::internal::currentWindowCtx; + trussc::internal::currentWindowCtx = ctx; + bool ok = trussc::internal::captureWindowToFile(trussc::internal::utf8ToPath(path)); + trussc::internal::currentWindowCtx = prev; + if (ok) return json{{"status", "ok"}, {"path", path}, {"window", windowIdx}}; + return json{{"status", "error"}, {"message", "Failed to capture window " + std::to_string(windowIdx)}}; + }); + return json(nullptr); // deferred result is sent instead + }); + + tool("tc_get_status", "App-published ops status: values registered via mcp::status()/mcp::statusGraph() plus the names of mcp::statusImage() images. mode 'graph' means the value wants to be plotted over time. Empty when the app publishes nothing. Supervisors discover this tool via tools/list and forward the payload to their monitoring server.") + .bind(std::function([]() -> json { + json values = json::array(); + for (auto& e : detail::statusRegistry()) { + json v; + try { + v = e.getter(); + } catch (...) { + continue; // a throwing getter drops its entry, not the tool + } + values.push_back({{"name", e.name}, + {"value", v}, + {"mode", e.graph ? "graph" : "status"}}); + } + json images = json::array(); + for (auto& e : detail::statusImageRegistry()) images.push_back(e.name); + return json{{"values", values}, {"images", images}}; + })); + + tool("tc_get_alerts", "Drain operator alerts raised by the app via mcp::alert(). Returns and CLEARS the pending list, so exactly one consumer receives each alert. Empty 'alerts' array when nothing is pending. Supervisors (anchorbolt start) poll this on the health cadence and forward entries to their notification sinks.") + .bind(std::function([]() -> json { + std::deque drained; + { + std::lock_guard lock(detail::alertMutex()); + drained.swap(detail::alertQueue()); + } + json arr = json::array(); + for (auto& a : drained) arr.push_back(std::move(a)); + return json{{"alerts", arr}}; + })); + + tool("tc_get_status_image", "Fetch an app-published image registered via mcp::statusImage(), downscaled + JPEG-encoded like tc_get_screenshot (pixel grab on the main loop, encode on the HTTP worker — no frame stutter).") + .arg("name", "Image name as listed by tc_get_status") + .arg("width", "Target width in pixels, aspect preserved (default 512, clamped 16-4096; never upscales)", false) + .arg("quality", "JPEG quality 1-100 (default 75)", false) + .bind([](const json& args) -> json { + std::string name = args.value("name", ""); + int reqWidth = 512; + int quality = 75; + if (args.contains("width") && args.at("width").is_number()) + reqWidth = std::clamp(args.at("width").get(), 16, 4096); + if (args.contains("quality") && args.at("quality").is_number()) + quality = std::clamp(args.at("quality").get(), 1, 100); + + std::function getter; + for (auto& e : detail::statusImageRegistry()) { + if (e.name == name) { getter = e.getter; break; } + } + if (!getter) { + return json{{"status", "error"}, + {"message", "unknown image '" + name + "' (see tc_get_status)"}}; + } + mcp::deferToolResultTwoStage([getter, reqWidth, quality]() -> std::function { + auto px = std::make_shared(); + bool ok = true; + try { + *px = getter(); // app getter runs on the main loop + } catch (...) { + ok = false; + } + if (ok && (px->getWidth() <= 0 || px->getHeight() <= 0 || !px->getData())) ok = false; + return [px, ok, reqWidth, quality]() -> json { + if (!ok) { + return json{{"status", "error"}, + {"message", "image getter returned no pixels"}}; + } + return detail::imageContentResult(detail::pixelsToImageJson(*px, "jpg", reqWidth, quality)); + }; + }); + return json(nullptr); // ignored — deferred result is sent instead }); - tool("quit", "Quit the application gracefully") + tool("tc_get_health", "Lightweight liveness snapshot: fps (measured average), frame count, uptime seconds, window size, TrussC version, pid, process RSS bytes, sokol-tracked bytes. Cheap enough to poll — reads counters only, touches no GPU state. pid lets a supervisor confirm it is talking to ITS child (port collisions); rssBytes is the number to graph for leak hunting.") + .bind(std::function([]() -> json { + return json{{"status", "ok"}, + {"fps", trussc::getFps()}, + {"frameCount", trussc::getFrameCount()}, + {"uptimeSec", trussc::getElapsedTime()}, + {"width", trussc::getWindowWidth()}, + {"height", trussc::getWindowHeight()}, + {"version", trussc::getVersion()}, + {"pid", detail::currentPid()}, + {"rssBytes", detail::processRssBytes()}, + {"memoryBytes", trussc::getSokolMemoryBytes()}}; + })); + + tool("tc_quit", "Quit the application gracefully") .bind(std::function([]() -> json { sapp_request_quit(); return json{{"status", "ok"}}; @@ -127,9 +526,9 @@ inline void registerInspectionTools() { // --- Recording tools (native encoder, no ffmpeg) --- - tool("start_recording", "Start recording the window to a video file (the screenshot's video counterpart). Omit path for a timestamped file in the data dir; give duration for a fixed-length clip that auto-stops and finalizes itself.") + tool("tc_start_recording", "Start recording the window to a video file (the screenshot's video counterpart). Omit path for a timestamped file in the data dir; give duration for a fixed-length clip that auto-stops and finalizes itself.") .arg("path", "Output file path (relative paths resolve to the data dir). Omit for recording-.mp4/.mov in the data dir.", false) - .arg("duration", "Fixed length in seconds; the file auto-stops & finalizes at that length. Omit or 0 = unlimited (stop_recording to end).", false) + .arg("duration", "Fixed length in seconds; the file auto-stops & finalizes at that length. Omit or 0 = unlimited (tc_stop_recording to end).", false) .arg("fps", "Target frame rate (default 60; ProMotion frames are decimated to it)", false) .arg("codec", "h264 (default) | hevc | prores422 | prores4444 (.mov, macOS)", false) .bind([](const json& args) -> json { @@ -157,16 +556,16 @@ inline void registerInspectionTools() { path = "recording-" + trussc::getTimestampString("%Y-%m-%d-%H-%M-%S") + (isProRes ? ".mov" : ".mp4"); } - bool ok = trussc::startRecording(path, settings); + bool ok = trussc::startRecording(trussc::internal::utf8ToPath(path), settings); json r{{"status", ok ? "ok" : "error"}, - {"path", trussc::recordingPath()}, + {"path", trussc::internal::pathToUtf8(trussc::recordingPath())}, {"fps", settings.fps}, {"codec", trussc::videoCodecName(settings.codec)}}; if (settings.duration > 0.0f) r["duration"] = settings.duration; return r; }); - tool("stop_recording", "Stop the current recording and finalize the file. Wins over a pending fixed duration (finalizes immediately at the current length); a no-op if nothing is recording.") + tool("tc_stop_recording", "Stop the current recording and finalize the file. Wins over a pending fixed duration (finalizes immediately at the current length); a no-op if nothing is recording.") .bind(std::function([]() -> json { if (!trussc::isRecording()) { // Harmless no-op (e.g. a fixed-duration recording already @@ -174,7 +573,7 @@ inline void registerInspectionTools() { return json{{"status", "ok"}, {"recording", false}, {"message", "not recording"}}; } - std::string path = trussc::recordingPath(); + std::string path = trussc::internal::pathToUtf8(trussc::recordingPath()); int frames = trussc::recordingFrameCount(); float fps = trussc::internal::globalScreenRecorder().writer().getFps(); trussc::stopRecording(); @@ -186,7 +585,7 @@ inline void registerInspectionTools() { // --- Node tree tools --- - tool("get_node_tree", "Dump the node tree as JSON: per node {type, name, id, members (reflected, rotation in degrees, colors 0-1), mods, children}. Where depth cuts children off, childCount marks how many were omitted — drill in with another call passing that node's id") + tool("tc_get_node_tree", "Dump the node tree as JSON: per node {type, name, id, members (reflected, rotation in degrees, colors 0-1), mods, children}. Where depth cuts children off, childCount marks how many were omitted — drill in with another call passing that node's id") .arg("id", "Subtree root instance id (omit for the whole tree)", false) .arg("depth", "Max child depth (omit for unlimited, 0 = the node only)", false) .bind([](const json& args) -> json { @@ -208,7 +607,7 @@ inline void registerInspectionTools() { return json{{"status", "ok"}, {"tree", nodeToJson(*root, depth)}}; }); - tool("get_selected_node", "Get the currently selected node (type, name, id, reflected members), or null if nothing is selected") + tool("tc_get_selected_node", "Get the currently selected node (type, name, id, reflected members), or null if nothing is selected") .bind(std::function([]() -> json { Node* n = getSelectedNode(); if (!n) { @@ -230,7 +629,7 @@ inline void registerDebuggerTools() { // --- Mouse Tools --- - tool("mouse_move", "Move mouse cursor (with a button held, emits a drag)") + tool("tc_mouse_move", "Move mouse cursor (with a button held, emits a drag)") .arg("x", "X coordinate") .arg("y", "Y coordinate") .arg("button", "Button held during the move (0:left, 1:right, 2:middle; omit or -1 for a plain move)", false) @@ -261,17 +660,17 @@ inline void registerDebuggerTools() { } // Update global mouse state - ::trussc::internal::mouseX = x; - ::trussc::internal::mouseY = y; + ::trussc::internal::currentWindowContext().mouseX = x; + ::trussc::internal::currentWindowContext().mouseY = y; return json{{"status", "ok"}}; }); - // Split press/release. A drag gesture is press → mouse_move(button) × N → - // release; mouse_click fires press+release back-to-back, so drag consumers + // Split press/release. A drag gesture is press → tc_mouse_move(button) × N → + // release; tc_mouse_click fires press+release back-to-back, so drag consumers // (e.g. EasyCam orbit) never see an open gesture. Anchoring the global // mouse position at press keeps the first drag delta sane. - tool("mouse_press", "Press and hold a mouse button (start of a drag; pair with mouse_move + mouse_release)") + tool("tc_mouse_press", "Press and hold a mouse button (start of a drag; pair with tc_mouse_move + tc_mouse_release)") .arg("x", "X coordinate") .arg("y", "Y coordinate") .arg("button", "Button (0:left, 1:right, 2:middle)", false) @@ -281,8 +680,8 @@ inline void registerDebuggerTools() { args.button = a.value("button", 0); args.syncLegacy(); - ::trussc::internal::mouseX = args.pos.x; - ::trussc::internal::mouseY = args.pos.y; + ::trussc::internal::currentWindowContext().mouseX = args.pos.x; + ::trussc::internal::currentWindowContext().mouseY = args.pos.y; events().mousePressed.notify(args); if (::trussc::internal::appMousePressedFunc) @@ -291,7 +690,7 @@ inline void registerDebuggerTools() { return json{{"status", "ok"}}; }); - tool("mouse_release", "Release a mouse button (end of a drag)") + tool("tc_mouse_release", "Release a mouse button (end of a drag)") .arg("x", "X coordinate") .arg("y", "Y coordinate") .arg("button", "Button (0:left, 1:right, 2:middle)", false) @@ -301,8 +700,8 @@ inline void registerDebuggerTools() { args.button = a.value("button", 0); args.syncLegacy(); - ::trussc::internal::mouseX = args.pos.x; - ::trussc::internal::mouseY = args.pos.y; + ::trussc::internal::currentWindowContext().mouseX = args.pos.x; + ::trussc::internal::currentWindowContext().mouseY = args.pos.y; events().mouseReleased.notify(args); if (::trussc::internal::appMouseReleasedFunc) @@ -311,7 +710,7 @@ inline void registerDebuggerTools() { return json{{"status", "ok"}}; }); - tool("mouse_click", "Click mouse button (optionally with modifier keys held)") + tool("tc_mouse_click", "Click mouse button (optionally with modifier keys held)") .arg("x", "X coordinate") .arg("y", "Y coordinate") .arg("button", "Button (0:left, 1:right, 2:middle)", false) @@ -344,12 +743,12 @@ inline void registerDebuggerTools() { return json{{"status", "ok"}}; }); - tool("mouse_scroll", "Scroll mouse wheel") + tool("tc_mouse_scroll", "Scroll mouse wheel") .arg("dx", "Horizontal scroll delta") .arg("dy", "Vertical scroll delta") .bind([](float dx, float dy) { ScrollEventArgs args; - args.pos = args.globalPos = Vec2(::trussc::internal::mouseX, ::trussc::internal::mouseY); + args.pos = args.globalPos = Vec2(::trussc::internal::currentWindowContext().mouseX, ::trussc::internal::currentWindowContext().mouseY); args.scroll = Vec2(dx, dy); args.syncLegacy(); events().mouseScrolled.notify(args); @@ -360,7 +759,7 @@ inline void registerDebuggerTools() { // --- Key Tools --- - tool("key_press", "Press a key") + tool("tc_key_press", "Press a key") .arg("key", "Key code (sokol_app keycode)") .bind([](int key) { KeyEventArgs args; @@ -371,7 +770,7 @@ inline void registerDebuggerTools() { return json{{"status", "ok"}}; }); - tool("key_release", "Release a key") + tool("tc_key_release", "Release a key") .arg("key", "Key code (sokol_app keycode)") .bind([](int key) { KeyEventArgs args; @@ -384,8 +783,8 @@ inline void registerDebuggerTools() { // --- Node Tools --- - tool("select_node", "Select a node by instance id (0 clears the selection); drives the same selection an inspector shows") - .arg("id", "Instance id from get_node_tree (0 to clear)") + tool("tc_select_node", "Select a node by instance id (0 clears the selection); drives the same selection an inspector shows") + .arg("id", "Instance id from tc_get_node_tree (0 to clear)") .bind([](int id) { if (id == 0) { setSelectedNode(nullptr); @@ -400,8 +799,8 @@ inline void registerDebuggerTools() { return json{{"status", "ok"}, {"selected", nodeToJson(*n, 0)}}; }); - tool("set_node_members", "Set reflected members of a node — or one of its mods — from a JSON object (same encoding as get_node_tree: Vec3 [x,y,z], Color [r,g,b,a], rotation in degrees, enums by label string)") - .arg("id", "Instance id from get_node_tree") + tool("tc_set_node_members", "Set reflected members of a node — or one of its mods — from a JSON object (same encoding as tc_get_node_tree: Vec3 [x,y,z], Color [r,g,b,a], rotation in degrees, enums by label string)") + .arg("id", "Instance id from tc_get_node_tree") .arg("members", "Member values to apply, e.g. {\"pos\":[10,20,0],\"visible\":true}") .arg("mod", "Mod short type name (e.g. \"LayoutMod\") to target a mod attached to the node instead of the node itself", false) .bind([](const json& args) -> json { diff --git a/core/include/tc/utils/tcSystemFont.h b/core/include/tc/utils/tcSystemFont.h index e5c87cd4..c90f5115 100644 --- a/core/include/tc/utils/tcSystemFont.h +++ b/core/include/tc/utils/tcSystemFont.h @@ -1,5 +1,6 @@ #pragma once #include "tc/utils/tcAnnotations.h" +#include "tc/utils/tcFileIO.h" // fs alias // ============================================================================= // System font lookup — resolve a font name (PostScript name or family/display @@ -27,7 +28,7 @@ namespace trussc { // `name` accepts whatever the platform's lookup API accepts — typically // either a PostScript name ("HiraginoSans-W3") or a family / display name // ("Hiragino Sans"). The PostScript form is the most portable. -TC_PLATFORMS("macos,windows,linux,ios") std::string systemFontPath(const std::string& name); +TC_PLATFORMS("macos,windows,linux,ios") fs::path systemFontPath(const std::string& name); // Enumerate the names of all fonts known to the OS. The exact form (PS name // vs. family) is platform-specific; expect deduplicated family-style names diff --git a/core/include/tc/utils/tcUtils.h b/core/include/tc/utils/tcUtils.h index 273e466d..a2c48ece 100644 --- a/core/include/tc/utils/tcUtils.h +++ b/core/include/tc/utils/tcUtils.h @@ -11,11 +11,12 @@ #include #include #include +#include "tc/utils/tcFileIO.h" // fs alias + path boundary helpers #include "../sound/tcSound.h" // Forward declaration for tcPlatform.h (avoid circular include) namespace trussc { - std::string getExecutableDir(); + fs::path getExecutableDir(); } namespace trussc { @@ -38,26 +39,19 @@ namespace internal { // in the iOS build, and the runtime probe also runs too early in _setup_cb // to see a valid executable path — so it happens lazily on first use). #ifdef __APPLE__ - inline std::string dataPathRoot = "../../../data/"; + inline fs::path dataPathRoot = "../../../data"; #else - inline std::string dataPathRoot = "data/"; + inline fs::path dataPathRoot = "data"; #endif - inline bool dataPathRootIsAbsolute = false; inline bool dataPathRootUserSet = false; // user called setDataPathRoot() inline bool dataPathRootProbed = false; // lazy Apple probe done } // Set the data path root -// If relative path, resolved relative to executable directory -// If absolute path (starts with /), used as-is -inline void setDataPathRoot(const std::string& path) { +// If relative, resolved relative to executable directory +// If absolute, used as-is (fs::path::is_absolute — handles "C:/..." on Windows too) +inline void setDataPathRoot(const fs::path& path) { internal::dataPathRoot = path; - // Add trailing slash if missing - if (!internal::dataPathRoot.empty() && internal::dataPathRoot.back() != '/') { - internal::dataPathRoot += '/'; - } - // Record whether path is absolute - internal::dataPathRootIsAbsolute = (!path.empty() && path[0] == '/'); internal::dataPathRootUserSet = true; // explicit choice wins over the probe } @@ -67,19 +61,19 @@ namespace internal { inline void resolveDataPathRootOnce() { #ifdef __APPLE__ if (dataPathRootProbed || dataPathRootUserSet) return; - std::string exe = getExecutableDir(); + fs::path exe = getExecutableDir(); // Don't latch until the executable path is actually available — early on // iOS it can be empty/"/", which would resolve the checks against the CWD. - if (exe.empty() || exe == "/") return; + if (exe.empty() || exe == fs::path("/")) return; dataPathRootProbed = true; std::error_code ec; // Check the flat-bundle layout FIRST (unambiguous on iOS: data/ sits right // next to the executable). macOS dev has no Contents/MacOS/data, so it // correctly falls through to the ../../../data (bin/data) layout. - if (std::filesystem::exists(exe + "data", ec)) { - dataPathRoot = "data/"; // iOS flat bundle / distributed - } else if (std::filesystem::exists(exe + "../../../data", ec)) { - dataPathRoot = "../../../data/"; // macOS dev / bin layout + if (std::filesystem::exists(exe / "data", ec)) { + dataPathRoot = "data"; // iOS flat bundle / distributed + } else if (std::filesystem::exists(exe / "../../../data", ec)) { + dataPathRoot = "../../../data"; // macOS dev / bin layout } // else: keep the compile-time default #endif @@ -87,29 +81,25 @@ inline void resolveDataPathRootOnce() { } // namespace internal // Get the data path root -inline std::string getDataPathRoot() { +inline fs::path getDataPathRoot() { internal::resolveDataPathRootOnce(); return internal::dataPathRoot; } // Get data path for a filename -// - If filename is absolute, return as-is +// - If filename is absolute, return as-is (like oF) // - Otherwise, resolved relative to executable directory + dataPathRoot -inline std::string getDataPath(const std::string& filename) { +inline fs::path getDataPath(const fs::path& filename) { internal::resolveDataPathRootOnce(); - - // If filename is absolute, return as-is (like oF) - // Uses std::filesystem for cross-platform support (Unix: /path, Windows: C:\path) - if (!filename.empty() && std::filesystem::path(filename).is_absolute()) { + if (!filename.empty() && filename.is_absolute()) { return filename; } - if (internal::dataPathRootIsAbsolute) { - // Absolute dataPathRoot: use as-is - return internal::dataPathRoot + filename; + if (internal::dataPathRoot.is_absolute()) { + return internal::dataPathRoot / filename; } else { - // Relative path: resolve relative to executable directory - return getExecutableDir() + internal::dataPathRoot + filename; + // Relative root: resolve relative to executable directory + return getExecutableDir() / internal::dataPathRoot / filename; } } diff --git a/core/include/tc/utils/tcXml.h b/core/include/tc/utils/tcXml.h index 82bcc8fc..671ef879 100644 --- a/core/include/tc/utils/tcXml.h +++ b/core/include/tc/utils/tcXml.h @@ -27,8 +27,10 @@ class Xml { Xml() = default; // Load from file (relative paths resolved via getDataPath, like loadJson) - bool load(const std::string& path) { - std::string fullPath = getDataPath(path); + bool load(const fs::path& path) { + fs::path fullPath = getDataPath(path); + // fullPath.c_str() is wchar_t* on Windows — pugixml has a wide + // load_file overload there, so non-ASCII paths survive. XmlParseResult result = doc_.load_file(fullPath.c_str()); if (!result) { logError() << "XML load error: " << path @@ -52,8 +54,9 @@ class Xml { } // Save to file (relative paths resolved via getDataPath, like saveJson) - bool save(const std::string& path, const std::string& indent = " ") const { - std::string fullPath = getDataPath(path); + bool save(const fs::path& path, const std::string& indent = " ") const { + fs::path fullPath = getDataPath(path); + // Wide save_file overload on Windows (see load) bool success = doc_.save_file(fullPath.c_str(), indent.c_str()); if (!success) { logError() << "XML write error: " << path; @@ -114,7 +117,7 @@ class Xml { // --------------------------------------------------------------------------- // Load XML from file -inline Xml loadXml(const std::string& path) { +inline Xml loadXml(const fs::path& path) { Xml xml; xml.load(path); return xml; diff --git a/core/include/tc/video/tcVideoPlayer.h b/core/include/tc/video/tcVideoPlayer.h index 4ab447d8..23841387 100644 --- a/core/include/tc/video/tcVideoPlayer.h +++ b/core/include/tc/video/tcVideoPlayer.h @@ -53,19 +53,32 @@ class TC_PLATFORMS("macos,windows,linux,ios,web") VideoPlayer : public VideoPlay // Load / Close // ========================================================================= - bool load(const std::string& path) override { + LoadResult load(const fs::path& path) override { if (initialized_) { close(); } // Resolve relative paths via getDataPath; URLs pass through untouched // (the web backend streams straight from them) - bool isUrl = path.rfind("http://", 0) == 0 || path.rfind("https://", 0) == 0; - std::string resolvedPath = isUrl ? path : getDataPath(path); + const std::string pathStr = path.string(); + bool isUrl = pathStr.rfind("http://", 0) == 0 || pathStr.rfind("https://", 0) == 0; + fs::path resolvedPath = isUrl ? path : getDataPath(path); + + // Missing files are classified up front (URLs skip the check — + // the platform backends stream straight from them). + if (!isUrl) { + std::error_code ec; + if (!fs::exists(resolvedPath, ec)) { + return LoadResult::fail(LoadError::FileNotFound, + "file not found: " + internal::pathToUtf8(resolvedPath)); + } + } // Platform-specific load if (!loadPlatform(resolvedPath)) { - return false; + return LoadResult::fail(LoadError::DecodeFailed, + "platform decoder failed to open: " + + internal::pathToUtf8(resolvedPath)); } // Remember the resolved path so instance-level frame extraction @@ -102,7 +115,7 @@ class TC_PLATFORMS("macos,windows,linux,ios,web") VideoPlayer : public VideoPlay // the black-clear are alternatives, never both. bool postered = autoPoster_ && loadPosterFrame(0.0f); if (!postered) clearTexture(); - return true; + return LoadResult::success(); } void close() override { @@ -355,8 +368,8 @@ class TC_PLATFORMS("macos,windows,linux,ios,web") VideoPlayer : public VideoPlay /// Path of the currently loaded video file (empty string when not loaded). /// This is the resolved path (relative paths passed to load() are resolved - /// via getDataPath). - const std::string& getPath() const { return sourcePath_; } + /// via getDataPath), as UTF-8. + fs::path getPath() const { return sourcePath_; } protected: // ------------------------------------------------------------------------- @@ -438,7 +451,7 @@ class TC_PLATFORMS("macos,windows,linux,ios,web") VideoPlayer : public VideoPlay // Resolved path of the currently loaded video (empty when not loaded). // Used by getPath() and the instance-level frame-extraction overloads. - std::string sourcePath_; + fs::path sourcePath_; // ------------------------------------------------------------------------- // Internal methods @@ -542,7 +555,7 @@ class TC_PLATFORMS("macos,windows,linux,ios,web") VideoPlayer : public VideoPlay // ------------------------------------------------------------------------- // Platform-specific methods (implemented in tcVideoPlayer_mac.mm, etc.) // ------------------------------------------------------------------------- - bool loadPlatform(const std::string& path); + bool loadPlatform(const fs::path& path); void closePlatform(); void playPlatform(); void stopPlatform(); @@ -599,7 +612,7 @@ class TC_PLATFORMS("macos,windows,linux,ios,web") VideoPlayer : public VideoPlay /// @param timeSec Time in seconds to extract from /// @param outDuration If non-null, receives video duration in seconds /// @return true on success - static bool extractFrame(const std::string& path, Pixels& outPixels, + static bool extractFrame(const fs::path& path, Pixels& outPixels, float timeSec, float* outDuration = nullptr) { return extractFramePlatform(path, outPixels, timeSec, outDuration); } @@ -611,7 +624,7 @@ class TC_PLATFORMS("macos,windows,linux,ios,web") VideoPlayer : public VideoPlay /// @param timeSec Upper-bound time in seconds /// @param outDuration If non-null, receives video duration in seconds /// @return true on success - static bool extractKeyFrame(const std::string& path, Pixels& outPixels, + static bool extractKeyFrame(const fs::path& path, Pixels& outPixels, float timeSec, float* outDuration = nullptr) { return extractKeyFramePlatform(path, outPixels, timeSec, outDuration); } @@ -639,7 +652,7 @@ class TC_PLATFORMS("macos,windows,linux,ios,web") VideoPlayer : public VideoPlay // ------------------------------------------------------------------------- /// Extract the exact frame at a time into a ready-to-draw Image (static). - static bool extractFrame(const std::string& path, Image& outImage, + static bool extractFrame(const fs::path& path, Image& outImage, float timeSec, float* outDuration = nullptr) { Pixels px; if (!extractFramePlatform(path, px, timeSec, outDuration)) return false; @@ -649,7 +662,7 @@ class TC_PLATFORMS("macos,windows,linux,ios,web") VideoPlayer : public VideoPlay /// Extract the nearest keyframe at or before a time into a ready-to-draw /// Image (static, faster). - static bool extractKeyFrame(const std::string& path, Image& outImage, + static bool extractKeyFrame(const fs::path& path, Image& outImage, float timeSec, float* outDuration = nullptr) { Pixels px; if (!extractKeyFramePlatform(path, px, timeSec, outDuration)) return false; @@ -683,9 +696,9 @@ class TC_PLATFORMS("macos,windows,linux,ios,web") VideoPlayer : public VideoPlay img.update(); } - static bool extractFramePlatform(const std::string& path, Pixels& outPixels, + static bool extractFramePlatform(const fs::path& path, Pixels& outPixels, float timeSec, float* outDuration); - static bool extractKeyFramePlatform(const std::string& path, Pixels& outPixels, + static bool extractKeyFramePlatform(const fs::path& path, Pixels& outPixels, float timeSec, float* outDuration); // Allow platform implementations to access internals diff --git a/core/include/tc/video/tcVideoPlayerBase.h b/core/include/tc/video/tcVideoPlayerBase.h index 898d57f0..eea93f40 100644 --- a/core/include/tc/video/tcVideoPlayerBase.h +++ b/core/include/tc/video/tcVideoPlayerBase.h @@ -8,7 +8,9 @@ #include #include #include +#include #include "tc/gpu/tcHasTexture.h" +#include "tc/utils/tcLoadResult.h" namespace trussc { @@ -28,7 +30,7 @@ class VideoPlayerBase : public HasTexture { // Load / Close (must be implemented by derived class) // ========================================================================= - virtual bool load(const std::string& path) = 0; + virtual LoadResult load(const fs::path& path) = 0; virtual void close() = 0; virtual bool isLoaded() const { return initialized_; } diff --git a/core/include/tc/video/tcVideoRecorder.h b/core/include/tc/video/tcVideoRecorder.h index d5aac5e9..7b125998 100644 --- a/core/include/tc/video/tcVideoRecorder.h +++ b/core/include/tc/video/tcVideoRecorder.h @@ -108,7 +108,7 @@ class TC_PLATFORMS("macos,windows,linux,android,ios") VideoWriter { // Open the encoder. `path` is resolved via getDataPath() when relative; the // parent directory is created if missing. Returns false if the encoder (or // the requested codec on this platform) could not be created. - bool open(const std::string& path, int width, int height, + bool open(const fs::path& path, int width, int height, const VideoRecordSettings& settings = {}) { close(); if (width <= 0 || height <= 0) { @@ -123,8 +123,7 @@ class TC_PLATFORMS("macos,windows,linux,android,ios") VideoWriter { path_ = getDataPath(path); { // native encoders won't create intermediate directories std::error_code ec; - std::filesystem::path parent = - std::filesystem::path(path_).parent_path(); + fs::path parent = path_.parent_path(); if (!parent.empty()) std::filesystem::create_directories(parent, ec); } width_ = width; @@ -134,7 +133,8 @@ class TC_PLATFORMS("macos,windows,linux,android,ios") VideoWriter { frameCount_ = 0; scratch_.resize((size_t)width_ * height_ * 4); - if (!openPlatform(path_, width_, height_, fps_, settings_)) { + // Platform encoders take UTF-8 (converted to native inside) + if (!openPlatform(internal::pathToUtf8(path_), width_, height_, fps_, settings_)) { logError("VideoWriter") << "failed to open " << videoCodecName(settings_.codec) << " encoder for " << path_; @@ -162,7 +162,7 @@ class TC_PLATFORMS("macos,windows,linux,android,ios") VideoWriter { int getWidth() const { return width_; } int getHeight() const { return height_; } float getFps() const { return fps_; } - const std::string& getPath() const { return path_; } + fs::path getPath() const { return path_; } const VideoRecordSettings& getSettings() const { return settings_; } // Append one frame at the fixed-rate clock (frameIndex / fps) — deterministic @@ -266,7 +266,7 @@ class TC_PLATFORMS("macos,windows,linux,android,ios") VideoWriter { #endif internal::VideoWriterPlatformData* platform_ = nullptr; - std::string path_; + fs::path path_; std::vector scratch_; int width_ = 0; int height_ = 0; @@ -287,14 +287,14 @@ class TC_PLATFORMS("macos,windows,linux,android,ios") ScreenRecorder { ScreenRecorder& operator=(const ScreenRecorder&) = delete; // Record the whole window (swapchain) — the same fully-composited image - // get_screenshot returns. Size is taken from the framebuffer. - bool start(const std::string& path, const VideoRecordSettings& settings = {}) { + // tc_get_screenshot returns. Size is taken from the framebuffer. + bool start(const fs::path& path, const VideoRecordSettings& settings = {}) { return startSource(Source::Swapchain, nullptr, sapp_width(), sapp_height(), path, settings); } // Record an offscreen Fbo every frame (clean output, GUI-free). - bool start(const Fbo& fbo, const std::string& path, + bool start(const Fbo& fbo, const fs::path& path, const VideoRecordSettings& settings = {}) { return startSource(Source::Fbo, &fbo, fbo.getWidth(), fbo.getHeight(), path, settings); @@ -303,12 +303,12 @@ class TC_PLATFORMS("macos,windows,linux,android,ios") ScreenRecorder { // Fixed-duration convenience: record for `durationSec` seconds, then auto-stop // and finalize the file at exactly that length. Same as filling // settings.duration. (durationSec <= 0 records until stop(), like the default.) - bool start(const std::string& path, float durationSec) { + bool start(const fs::path& path, float durationSec) { VideoRecordSettings s; s.duration = durationSec; return start(path, s); } - bool start(const Fbo& fbo, const std::string& path, float durationSec) { + bool start(const Fbo& fbo, const fs::path& path, float durationSec) { VideoRecordSettings s; s.duration = durationSec; return start(fbo, path, s); @@ -337,14 +337,14 @@ class TC_PLATFORMS("macos,windows,linux,android,ios") ScreenRecorder { bool isRecording() const { return writer_.isOpen(); } int getFrameCount() const { return writer_.getFrameCount(); } - const std::string& getPath() const { return writer_.getPath(); } + fs::path getPath() const { return writer_.getPath(); } VideoWriter& writer() { return writer_; } // for advanced introspection private: enum class Source { None, Swapchain, Fbo }; bool startSource(Source src, const Fbo* fbo, int w, int h, - const std::string& path, + const fs::path& path, const VideoRecordSettings& settings) { stop(); if (!writer_.open(path, w, h, settings)) return false; @@ -469,7 +469,7 @@ namespace internal { // Start recording the whole window to a video file. `path` is required; // relative paths resolve via getDataPath(). Auto-finalizes on app exit. -TC_PLATFORMS("macos,windows,linux,android,ios") inline bool startRecording(const std::string& path, +TC_PLATFORMS("macos,windows,linux,android,ios") inline bool startRecording(const fs::path& path, const VideoRecordSettings& settings = {}) { return internal::globalScreenRecorder().start(path, settings); } @@ -477,7 +477,7 @@ TC_PLATFORMS("macos,windows,linux,android,ios") inline bool startRecording(const // Fixed-duration convenience: record the whole window for `durationSec` seconds, // then auto-stop and finalize at exactly that length. (durationSec <= 0 behaves // like the unlimited overload above — record until stopRecording().) -TC_PLATFORMS("macos,windows,linux,android,ios") inline bool startRecording(const std::string& path, +TC_PLATFORMS("macos,windows,linux,android,ios") inline bool startRecording(const fs::path& path, float durationSec) { return internal::globalScreenRecorder().start(path, durationSec); } @@ -486,14 +486,14 @@ TC_PLATFORMS("macos,windows,linux,android,ios") inline bool startRecording(const // The Fbo must stay alive while recording — if it is destroyed mid-recording, // the recording stops and finalizes automatically. TC_PLATFORMS("macos,windows,linux,android,ios") inline bool startRecording(const Fbo& fbo, - const std::string& path, + const fs::path& path, const VideoRecordSettings& settings = {}) { return internal::globalScreenRecorder().start(fbo, path, settings); } // Fixed-duration Fbo recording (see above). TC_PLATFORMS("macos,windows,linux,android,ios") inline bool startRecording(const Fbo& fbo, - const std::string& path, + const fs::path& path, float durationSec) { return internal::globalScreenRecorder().start(fbo, path, durationSec); } @@ -501,6 +501,6 @@ TC_PLATFORMS("macos,windows,linux,android,ios") inline bool startRecording(const inline void stopRecording() { internal::globalScreenRecorder().stop(); } inline bool isRecording() { return internal::globalScreenRecorder().isRecording(); } inline int recordingFrameCount() { return internal::globalScreenRecorder().getFrameCount(); } -inline const std::string& recordingPath() { return internal::globalScreenRecorder().getPath(); } +inline fs::path recordingPath() { return internal::globalScreenRecorder().getPath(); } } // namespace trussc diff --git a/core/include/tcBaseApp.h b/core/include/tcBaseApp.h index 06a6319a..2f030149 100644 --- a/core/include/tcBaseApp.h +++ b/core/include/tcBaseApp.h @@ -40,13 +40,18 @@ class App : public RectNode { audioInListener_ = AudioEngine::getInstance().audioIn.listen( [this](AudioInBuffer& b) { audioIn(b); }); - // The App is the scene-graph root; expose it via getRootNode() so - // tools (e.g. the MCP node tools) can walk the tree. - internal::rootNode = this; + // The FIRST App becomes the scene-graph root of the active window + // (normally the main App created by runApp) — exposed via + // getRootNode() so tools (e.g. the MCP node tools) can walk the tree. + // Later App instances don't clobber it: they are secondary-window + // content, registered explicitly by Window::setApp(). + if (internal::currentWindowContext().rootNode == nullptr) { + internal::currentWindowContext().rootNode = this; + } } virtual ~App() { - if (internal::rootNode == this) internal::rootNode = nullptr; + if (internal::currentWindowContext().rootNode == this) internal::currentWindowContext().rootNode = nullptr; } // ------------------------------------------------------------------------- diff --git a/core/include/tcMath.h b/core/include/tcMath.h index 82e19852..ad816167 100644 --- a/core/include/tcMath.h +++ b/core/include/tcMath.h @@ -28,7 +28,7 @@ struct Vec2 { // Constructors Vec2() = default; Vec2(float x_, float y_) : x(x_), y(y_) {} - Vec2(float v) : x(v), y(v) {} + explicit Vec2(float v) : x(v), y(v) {} Vec2(const Vec2&) = default; Vec2& operator=(const Vec2&) = default; @@ -145,7 +145,7 @@ struct Vec3 { // Constructors Vec3() = default; Vec3(float x_, float y_, float z_) : x(x_), y(y_), z(z_) {} - Vec3(float v) : x(v), y(v), z(v) {} + explicit Vec3(float v) : x(v), y(v), z(v) {} Vec3(const Vec2& v, float z_ = 0.0f) : x(v.x), y(v.y), z(z_) {} Vec3(const Vec3&) = default; Vec3& operator=(const Vec3&) = default; @@ -247,7 +247,7 @@ struct IVec2 { IVec2() = default; IVec2(int x_, int y_) : x(x_), y(y_) {} - IVec2(int v) : x(v), y(v) {} + explicit IVec2(int v) : x(v), y(v) {} IVec2 operator+(const IVec2& v) const { return IVec2(x + v.x, y + v.y); } IVec2 operator-(const IVec2& v) const { return IVec2(x - v.x, y - v.y); } @@ -277,7 +277,7 @@ struct IVec3 { IVec3() = default; IVec3(int x_, int y_, int z_) : x(x_), y(y_), z(z_) {} - IVec3(int v) : x(v), y(v), z(v) {} + explicit IVec3(int v) : x(v), y(v), z(v) {} IVec3(const IVec2& v, int z_ = 0) : x(v.x), y(v.y), z(z_) {} IVec3 operator+(const IVec3& v) const { return IVec3(x + v.x, y + v.y, z + v.z); } @@ -311,7 +311,7 @@ struct Vec4 { // Constructors Vec4() = default; Vec4(float x_, float y_, float z_, float w_) : x(x_), y(y_), z(z_), w(w_) {} - Vec4(float v) : x(v), y(v), z(v), w(v) {} + explicit Vec4(float v) : x(v), y(v), z(v), w(v) {} Vec4(const Vec3& v, float w_ = 1.0f) : x(v.x), y(v.y), z(v.z), w(w_) {} Vec4(const Vec2& v, float z_ = 0.0f, float w_ = 1.0f) : x(v.x), y(v.y), z(z_), w(w_) {} Vec4(const Vec4&) = default; @@ -328,6 +328,8 @@ struct Vec4 { // Arithmetic operators Vec4 operator+(const Vec4& v) const { return Vec4(x + v.x, y + v.y, z + v.z, w + v.w); } Vec4 operator-(const Vec4& v) const { return Vec4(x - v.x, y - v.y, z - v.z, w - v.w); } + Vec4 operator*(const Vec4& v) const { return Vec4(x * v.x, y * v.y, z * v.z, w * v.w); } + Vec4 operator/(const Vec4& v) const { return Vec4(x / v.x, y / v.y, z / v.z, w / v.w); } Vec4 operator*(float s) const { return Vec4(x * s, y * s, z * s, w * s); } Vec4 operator/(float s) const { return Vec4(x / s, y / s, z / s, w / s); } Vec4 operator-() const { return Vec4(-x, -y, -z, -w); } @@ -335,6 +337,8 @@ struct Vec4 { // Compound assignment operators Vec4& operator+=(const Vec4& v) { x += v.x; y += v.y; z += v.z; w += v.w; return *this; } Vec4& operator-=(const Vec4& v) { x -= v.x; y -= v.y; z -= v.z; w -= v.w; return *this; } + Vec4& operator*=(const Vec4& v) { x *= v.x; y *= v.y; z *= v.z; w *= v.w; return *this; } + Vec4& operator/=(const Vec4& v) { x /= v.x; y /= v.y; z /= v.z; w /= v.w; return *this; } Vec4& operator*=(float s) { x *= s; y *= s; z *= s; w *= s; return *this; } Vec4& operator/=(float s) { x /= s; y /= s; z /= s; w /= s; return *this; } diff --git a/core/include/tcNode.h b/core/include/tcNode.h index 51e9a0b7..3243522c 100644 --- a/core/include/tcNode.h +++ b/core/include/tcNode.h @@ -65,15 +65,10 @@ class Mod; using NodePtr = std::shared_ptr; using NodeWeakPtr = std::weak_ptr; -// Hover state cache (updated once per frame) +// Hover state cache (updated once per frame): hoveredNode / prevHoveredNode / +// grabbedNode / grabbedButton / selectedNode / rootNode moved to WindowContext +// (tc/app/tcWindowContext.h) — each window's node tree has its own. namespace internal { - inline Node* hoveredNode = nullptr; // Currently hovered node - inline Node* prevHoveredNode = nullptr; // Previously hovered node - inline Node* grabbedNode = nullptr; // Node grabbed by mouse press - inline int grabbedButton = -1; // Mouse button that caused the grab - inline Node* selectedNode = nullptr; // Last node clicked (selection) - inline Node* rootNode = nullptr; // The running App (set by the framework) - // Overlay (e.g. tcxImGui) capture queries. An overlay registers these so the // framework knows when the pointer is over it / it owns keyboard focus. Null // when no overlay is present, so plain apps are unaffected. @@ -94,7 +89,8 @@ inline bool isOverlayFocused() { return internal::overlayFocusedQuery && interna // ============================================================================= class Node : public std::enable_shared_from_this { - friend class App; // Allow App to call dispatch methods + friend class App; // Allow App to call dispatch methods + friend class Window; // Secondary windows drive their own tree (tcWindow.h) friend class Mod; // Allow Mod to access owner_ public: @@ -340,7 +336,7 @@ class Node : public std::enable_shared_from_this { bool isEventsEnabled() const { return eventsEnabled_; } // Whether mouse is over this node (auto-updated each frame, O(1)) - bool isMouseOver() const { return internal::hoveredNode == this; } + bool isMouseOver() const { return internal::currentWindowContext().hoveredNode == this; } // ------------------------------------------------------------------------- // Identity / Name @@ -846,13 +842,13 @@ class Node : public std::enable_shared_from_this { } // Clear global references to this node (prevent dangling pointers) - if (internal::hoveredNode == this) internal::hoveredNode = nullptr; - if (internal::prevHoveredNode == this) internal::prevHoveredNode = nullptr; - if (internal::grabbedNode == this) { - internal::grabbedNode = nullptr; - internal::grabbedButton = -1; + if (internal::currentWindowContext().hoveredNode == this) internal::currentWindowContext().hoveredNode = nullptr; + if (internal::currentWindowContext().prevHoveredNode == this) internal::currentWindowContext().prevHoveredNode = nullptr; + if (internal::currentWindowContext().grabbedNode == this) { + internal::currentWindowContext().grabbedNode = nullptr; + internal::currentWindowContext().grabbedButton = -1; } - if (internal::selectedNode == this) internal::selectedNode = nullptr; + if (internal::currentWindowContext().selectedNode == this) internal::currentWindowContext().selectedNode = nullptr; dead_ = true; cleanup(); @@ -870,8 +866,8 @@ class Node : public std::enable_shared_from_this { // Stamp the camera scope this node is being drawn under (pointer // compare first — steady state is one assignment skip per frame). - if (internal::currentCameraContext && cameraContext_ != internal::currentCameraContext) { - cameraContext_ = internal::currentCameraContext; + if (internal::currentWindowContext().currentCameraContext && cameraContext_ != internal::currentWindowContext().currentCameraContext) { + cameraContext_ = internal::currentWindowContext().currentCameraContext; } pushMatrix(); @@ -946,14 +942,14 @@ class Node : public std::enable_shared_from_this { HitResult result = findHitNodeFromScreen(e.globalPos.x, e.globalPos.y); // Selection: clicking a node selects it; clicking empty space clears it. - internal::selectedNode = result.hit() ? result.node.get() : nullptr; + internal::currentWindowContext().selectedNode = result.hit() ? result.node.get() : nullptr; if (result.hit()) { MouseEventArgs local = result.node->localizeMouse(e); if (result.node->fireMousePress(local)) { // Set grabbed node for drag tracking - internal::grabbedNode = result.node.get(); - internal::grabbedButton = e.button; + internal::currentWindowContext().grabbedNode = result.node.get(); + internal::currentWindowContext().grabbedButton = e.button; return result.node; } } @@ -963,16 +959,16 @@ class Node : public std::enable_shared_from_this { Ptr dispatchMouseRelease(const MouseEventArgs& e) { // Send release to grabbed node if it exists - if (internal::grabbedNode && internal::grabbedButton == e.button) { - MouseEventArgs local = internal::grabbedNode->localizeMouse(e); - internal::grabbedNode->fireMouseRelease(local); + if (internal::currentWindowContext().grabbedNode && internal::currentWindowContext().grabbedButton == e.button) { + MouseEventArgs local = internal::currentWindowContext().grabbedNode->localizeMouse(e); + internal::currentWindowContext().grabbedNode->fireMouseRelease(local); Ptr result = std::dynamic_pointer_cast( - internal::grabbedNode->shared_from_this()); + internal::currentWindowContext().grabbedNode->shared_from_this()); // Clear grabbed state - internal::grabbedNode = nullptr; - internal::grabbedButton = -1; + internal::currentWindowContext().grabbedNode = nullptr; + internal::currentWindowContext().grabbedButton = -1; return result; } @@ -992,10 +988,10 @@ class Node : public std::enable_shared_from_this { Ptr dispatchMouseMove(const internal::MouseEventRaw& e) { // Send drag event to grabbed node - if (internal::grabbedNode) { - internal::MouseEventRaw local = internal::grabbedNode->localizeMouse(e); - local.button = internal::grabbedButton; - internal::grabbedNode->fireMouseDrag(internal::toDragArgs(local)); + if (internal::currentWindowContext().grabbedNode) { + internal::MouseEventRaw local = internal::currentWindowContext().grabbedNode->localizeMouse(e); + local.button = internal::currentWindowContext().grabbedButton; + internal::currentWindowContext().grabbedNode->fireMouseDrag(internal::toDragArgs(local)); } // Also send move event to hit node (for hover, etc.) @@ -1046,7 +1042,7 @@ class Node : public std::enable_shared_from_this { // Update hover state (call once per frame) void updateHoverState(float screenX, float screenY) { // Save previous frame's hovered node - internal::prevHoveredNode = internal::hoveredNode; + internal::currentWindowContext().prevHoveredNode = internal::currentWindowContext().hoveredNode; // Search for new hovered node. When an overlay (e.g. a tcxImGui panel) // has the pointer, the tree hovers nothing — so a node under the panel @@ -1057,15 +1053,15 @@ class Node : public std::enable_shared_from_this { HitResult result = findHitNodeFromScreen(screenX, screenY); hit = result.hit() ? result.node.get() : nullptr; } - internal::hoveredNode = hit; + internal::currentWindowContext().hoveredNode = hit; // Fire Enter/Leave events - if (internal::prevHoveredNode != internal::hoveredNode) { - if (internal::prevHoveredNode) { - internal::prevHoveredNode->fireMouseLeave(); + if (internal::currentWindowContext().prevHoveredNode != internal::currentWindowContext().hoveredNode) { + if (internal::currentWindowContext().prevHoveredNode) { + internal::currentWindowContext().prevHoveredNode->fireMouseLeave(); } - if (internal::hoveredNode) { - internal::hoveredNode->fireMouseEnter(); + if (internal::currentWindowContext().hoveredNode) { + internal::currentWindowContext().hoveredNode->fireMouseEnter(); } } } @@ -1464,12 +1460,12 @@ inline void Mod::removeSelf() { // Selection — the last-clicked node, held by the Node system (set in // dispatchMousePress, cleared when the node is destroyed). A tool such as an // inspector can both read it and drive it via setSelectedNode(). -inline Node* getSelectedNode() { return internal::selectedNode; } -inline void setSelectedNode(Node* n) { internal::selectedNode = n; } +inline Node* getSelectedNode() { return internal::currentWindowContext().selectedNode; } +inline void setSelectedNode(Node* n) { internal::currentWindowContext().selectedNode = n; } // The running App as the root of the node tree (set by the framework while the // app is alive, null otherwise). Lets tools — e.g. the MCP node tools — walk // the whole tree without the app passing itself around. -inline Node* getRootNode() { return internal::rootNode; } +inline Node* getRootNode() { return internal::currentWindowContext().rootNode; } } // namespace trussc diff --git a/core/include/tcPlatform.h b/core/include/tcPlatform.h index ea04711f..8c072184 100644 --- a/core/include/tcPlatform.h +++ b/core/include/tcPlatform.h @@ -136,10 +136,10 @@ void setWindowDecorated(bool decorated); void setWindowSizeLogical(int width, int height); // Get absolute path of executable -std::string getExecutablePath(); +fs::path getExecutablePath(); // Get directory containing executable (with trailing /) -std::string getExecutableDir(); +fs::path getExecutableDir(); // --------------------------------------------------------------------------- // Immersive mode (hide system UI) diff --git a/core/platform/android/sokol_impl.cpp b/core/platform/android/sokol_impl.cpp index 5fbaab7a..e7e05026 100644 --- a/core/platform/android/sokol_impl.cpp +++ b/core/platform/android/sokol_impl.cpp @@ -1,16 +1,16 @@ // ============================================================================= // sokol backend implementation (Android) +// sokol_app_tc.h owns the sapp_* implementation on Android; sokol_app.h is +// included declarations-only (no SOKOL_APP_IMPL). // ============================================================================= // NOTE: SOKOL_NO_ENTRY is NOT defined on Android. -// sokol_app.h provides ANativeActivity_onCreate() as the entry point, -// which calls sokol_main() to get the sapp_desc. +// sokol_app_tc.h exports ANativeActivity_onCreate() as the entry point, +// which calls sokol_main() (defined below) to get the sapp_desc. // sapp_run() is NOT supported on Android. // ============================================================================= #ifdef __ANDROID__ -#define SOKOL_IMPL - #if defined(__GNUC__) || defined(__clang__) # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wunused-variable" @@ -18,8 +18,13 @@ # pragma GCC diagnostic ignored "-Wmissing-field-initializers" #endif + +#define SOKOL_IMPL #include "sokol_log.h" -#include "sokol_app.h" + +#define SOKOL_APP_TC_IMPL +#include "sokol_app_tc.h" + #include "sokol_gfx.h" #include "sokol_glue.h" #include "util/sokol_gl_tc.h" @@ -32,9 +37,8 @@ // --------------------------------------------------------------------------- // Android entry point bridge // --------------------------------------------------------------------------- -// sokol_app.h (without SOKOL_NO_ENTRY) declares sokol_main() but does not -// define it. On Android, ANativeActivity_onCreate calls sokol_main() to get -// the sapp_desc. +// sokol_app_tc.h's exported ANativeActivity_onCreate calls sokol_main() to +// get the sapp_desc (declared by sokol_app.h, defined by nobody else). // // Strategy: user's main() calls runApp() which stores the descriptor // in trussc::internal::g_androidDesc. sokol_main() calls main() then returns diff --git a/core/platform/android/tcFileDialog_android.cpp b/core/platform/android/tcFileDialog_android.cpp index 5e5d2eac..568b6b6f 100644 --- a/core/platform/android/tcFileDialog_android.cpp +++ b/core/platform/android/tcFileDialog_android.cpp @@ -42,7 +42,7 @@ void confirmDialogAsync(const std::string& title, FileDialogResult loadDialog(const std::string& title, const std::string& message, - const std::string& defaultPath, + const fs::path& defaultPath, bool folderSelection) { (void)title; (void)message; (void)defaultPath; (void)folderSelection; return FileDialogResult{}; @@ -50,7 +50,7 @@ FileDialogResult loadDialog(const std::string& title, void loadDialogAsync(const std::string& title, const std::string& message, - const std::string& defaultPath, + const fs::path& defaultPath, bool folderSelection, std::function callback) { if (callback) callback(FileDialogResult{}); @@ -58,16 +58,16 @@ void loadDialogAsync(const std::string& title, FileDialogResult saveDialog(const std::string& title, const std::string& message, - const std::string& defaultPath, - const std::string& defaultName) { + const fs::path& defaultPath, + const fs::path& defaultName) { (void)title; (void)message; (void)defaultPath; (void)defaultName; return FileDialogResult{}; } void saveDialogAsync(const std::string& title, const std::string& message, - const std::string& defaultPath, - const std::string& defaultName, + const fs::path& defaultPath, + const fs::path& defaultName, std::function callback) { if (callback) callback(FileDialogResult{}); } diff --git a/core/platform/android/tcPlatform_android.cpp b/core/platform/android/tcPlatform_android.cpp index 7e57ace0..1bc61235 100644 --- a/core/platform/android/tcPlatform_android.cpp +++ b/core/platform/android/tcPlatform_android.cpp @@ -9,7 +9,7 @@ #include #include #include -#include "sokol_app.h" +#include "sokol_app_tc.h" // GLES3 for screen capture #include @@ -209,20 +209,20 @@ void setWindowSizeLogical(int width, int height) { (void)height; } -std::string getExecutablePath() { +fs::path getExecutablePath() { // Android doesn't have a traditional executable path. // Return empty — use getExecutableDir() for asset access. - return ""; + return {}; } -std::string getExecutableDir() { +fs::path getExecutableDir() { // On Android, assets are accessed via AAssetManager, not filesystem paths. // For files that need a real path, use internal storage. auto* activity = (ANativeActivity*)sapp_android_get_native_activity(); if (activity && activity->internalDataPath) { - return std::string(activity->internalDataPath) + "/"; + return fs::path(activity->internalDataPath); } - return "/data/local/tmp/"; + return fs::path("/data/local/tmp"); } // --------------------------------------------------------------------------- @@ -258,7 +258,7 @@ bool captureWindow(Pixels& outPixels) { bool internal::captureWindowToFile(const std::filesystem::path& path) { if (path.is_relative()) { - return internal::captureWindowToFile(getDataPath(path.string())); + return internal::captureWindowToFile(getDataPath(path)); } Pixels pixels; if (!captureWindow(pixels)) { @@ -266,7 +266,7 @@ bool internal::captureWindowToFile(const std::filesystem::path& path) { } std::string ext = path.extension().string(); - std::string pathStr = path.string(); + std::string pathStr = internal::pathToUtf8(path); // UTF-8 for stb (STBIW_WINDOWS_UTF8) int width = pixels.getWidth(); int height = pixels.getHeight(); diff --git a/core/platform/android/tcSerial_android.cpp b/core/platform/android/tcSerial_android.cpp index f54662af..f0d37666 100644 --- a/core/platform/android/tcSerial_android.cpp +++ b/core/platform/android/tcSerial_android.cpp @@ -35,7 +35,7 @@ #include #include #include -#include "sokol_app.h" +#include "sokol_app_tc.h" namespace trussc { namespace androidserial { diff --git a/core/platform/android/tcSound_android.cpp b/core/platform/android/tcSound_android.cpp index 8a96e2c1..9c8a1e26 100644 --- a/core/platform/android/tcSound_android.cpp +++ b/core/platform/android/tcSound_android.cpp @@ -13,14 +13,16 @@ namespace trussc { -bool SoundBuffer::loadAac(const std::string& path) { +LoadResult SoundBuffer::loadAac(const fs::path& path) { logWarning("SoundBuffer") << "AAC loading not yet implemented on Android"; - return false; + return LoadResult::fail(LoadError::UnsupportedFormat, + "AAC loading not yet implemented on Android"); } -bool SoundBuffer::loadAacFromMemory(const void* data, size_t dataSize) { +LoadResult SoundBuffer::loadAacFromMemory(const void* data, size_t dataSize) { logWarning("SoundBuffer") << "AAC loading from memory not yet implemented on Android"; - return false; + return LoadResult::fail(LoadError::UnsupportedFormat, + "AAC loading from memory not yet implemented on Android"); } } // namespace trussc diff --git a/core/platform/android/tcSystemFont_android.cpp b/core/platform/android/tcSystemFont_android.cpp index d67574f7..0fbd8b72 100644 --- a/core/platform/android/tcSystemFont_android.cpp +++ b/core/platform/android/tcSystemFont_android.cpp @@ -12,7 +12,7 @@ namespace trussc { -std::string systemFontPath(const std::string& /*name*/) { +fs::path systemFontPath(const std::string& /*name*/) { return ""; } diff --git a/core/platform/android/tcVideoPlayer_android.cpp b/core/platform/android/tcVideoPlayer_android.cpp index ec136485..2e2634b5 100644 --- a/core/platform/android/tcVideoPlayer_android.cpp +++ b/core/platform/android/tcVideoPlayer_android.cpp @@ -12,7 +12,7 @@ namespace trussc { -bool VideoPlayer::loadPlatform(const std::string& path) { +bool VideoPlayer::loadPlatform(const fs::path& path) { logWarning("VideoPlayer") << "Not yet implemented on Android"; return false; } @@ -46,13 +46,13 @@ int VideoPlayer::getAudioChannelsPlatform() const { return 0; } bool VideoPlayer::isUsingHwAccelPlatform() const { return false; } std::string VideoPlayer::getHwAccelNamePlatform() const { return "none"; } -bool VideoPlayer::extractFramePlatform(const std::string& path, Pixels& outPixels, +bool VideoPlayer::extractFramePlatform(const fs::path& path, Pixels& outPixels, float timeSec, float* outDuration) { (void)path; (void)outPixels; (void)timeSec; (void)outDuration; return false; } -bool VideoPlayer::extractKeyFramePlatform(const std::string& path, Pixels& outPixels, +bool VideoPlayer::extractKeyFramePlatform(const fs::path& path, Pixels& outPixels, float timeSec, float* outDuration) { (void)path; (void)outPixels; (void)timeSec; (void)outDuration; return false; diff --git a/core/platform/ios/sokol_impl.mm b/core/platform/ios/sokol_impl.mm index 2dba7871..680a0412 100644 --- a/core/platform/ios/sokol_impl.mm +++ b/core/platform/ios/sokol_impl.mm @@ -1,8 +1,9 @@ // ============================================================================= // sokol backend implementation (iOS / Metal) +// sokol_app_tc.h owns the sapp_* implementation on iOS; sokol_app.h is +// included declarations-only (no SOKOL_APP_IMPL). // ============================================================================= -#define SOKOL_IMPL #define SOKOL_NO_ENTRY // main() is defined by the app #if defined(__GNUC__) || defined(__clang__) @@ -12,8 +13,13 @@ # pragma GCC diagnostic ignored "-Wmissing-field-initializers" #endif + +#define SOKOL_IMPL #include "sokol_log.h" -#include "sokol_app.h" + +#define SOKOL_APP_TC_IMPL +#include "sokol_app_tc.h" + #include "sokol_gfx.h" #include "sokol_glue.h" #include "util/sokol_gl_tc.h" diff --git a/core/platform/ios/tcFileDialog_ios.mm b/core/platform/ios/tcFileDialog_ios.mm index b9a01af7..451fc7be 100644 --- a/core/platform/ios/tcFileDialog_ios.mm +++ b/core/platform/ios/tcFileDialog_ios.mm @@ -14,7 +14,7 @@ #import #include "tc/utils/tcLog.h" -#include "sokol_app.h" +#include "sokol_app_tc.h" // --------------------------------------------------------------------------- // Helper: get root view controller from sokol window @@ -74,7 +74,7 @@ bool confirmDialog(const std::string& title, const std::string& message) { FileDialogResult loadDialog(const std::string& title, const std::string& message, - const std::string& defaultPath, + const fs::path& defaultPath, bool folderSelection) { logError() << "[FileDialog] Sync loadDialog is not supported on iOS. Use loadDialogAsync."; return FileDialogResult{}; @@ -82,8 +82,8 @@ FileDialogResult loadDialog(const std::string& title, FileDialogResult saveDialog(const std::string& title, const std::string& message, - const std::string& defaultPath, - const std::string& defaultName) { + const fs::path& defaultPath, + const fs::path& defaultName) { logError() << "[FileDialog] Sync saveDialog is not supported on iOS. Use saveDialogAsync."; return FileDialogResult{}; } @@ -170,7 +170,7 @@ void confirmDialogAsync(const std::string& title, // --------------------------------------------------------------------------- void loadDialogAsync(const std::string& title, const std::string& message, - const std::string& defaultPath, + const fs::path& defaultPath, bool folderSelection, std::function callback) { auto cb = callback; @@ -228,8 +228,8 @@ void loadDialogAsync(const std::string& title, // --------------------------------------------------------------------------- void saveDialogAsync(const std::string& title, const std::string& message, - const std::string& defaultPath, - const std::string& defaultName, + const fs::path& defaultPath, + const fs::path& defaultName, std::function callback) { auto cb = callback; diff --git a/core/platform/ios/tcPlatform_ios.mm b/core/platform/ios/tcPlatform_ios.mm index fbbf5b9a..d21316da 100644 --- a/core/platform/ios/tcPlatform_ios.mm +++ b/core/platform/ios/tcPlatform_ios.mm @@ -16,7 +16,7 @@ #import #import -#include "sokol_app.h" +#include "sokol_app_tc.h" // --------------------------------------------------------------------------- // Sensor managers (lazily initialized) @@ -118,16 +118,15 @@ void setWindowSizeLogical(int width, int height) { (void)height; } -std::string getExecutablePath() { +fs::path getExecutablePath() { NSString* path = [[NSBundle mainBundle] executablePath]; - return path ? std::string([path UTF8String]) : ""; + return path ? fs::path([path UTF8String]) : fs::path(); } -std::string getExecutableDir() { - NSString* path = [[NSBundle mainBundle] executablePath]; - NSString* dir = [path stringByDeletingLastPathComponent]; - if (!dir || dir.length == 0) return ""; - return std::string([dir UTF8String]) + "/"; +fs::path getExecutableDir() { + fs::path exePath = getExecutablePath(); + if (exePath.empty()) return {}; + return exePath.parent_path(); } // --------------------------------------------------------------------------- @@ -139,7 +138,7 @@ bool captureWindow(Pixels& outPixels) { // swapchain pass). NOT sapp_get_swapchain() — that advances to the next, // unrendered drawable, scrambling captures on GPU-heavy frames. Fall back // only if no pass ran yet. (Mirrors the macOS fix.) - const void* drawablePtr = internal::lastSwapchainDrawable; + const void* drawablePtr = internal::currentWindowContext().lastSwapchainDrawable; if (!drawablePtr) drawablePtr = sapp_get_swapchain().metal.current_drawable; id drawable = (__bridge id)drawablePtr; if (!drawable) { @@ -228,7 +227,7 @@ bool captureWindow(Pixels& outPixels) { bool internal::captureWindowToFile(const std::filesystem::path& path) { if (path.is_relative()) { - return internal::captureWindowToFile(getDataPath(path.string())); + return internal::captureWindowToFile(getDataPath(path)); } Pixels pixels; if (!captureWindow(pixels)) { diff --git a/core/platform/ios/tcSound_ios.mm b/core/platform/ios/tcSound_ios.mm index 16b6d839..834008a4 100644 --- a/core/platform/ios/tcSound_ios.mm +++ b/core/platform/ios/tcSound_ios.mm @@ -48,14 +48,23 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { // ----------------------------------------------------------------------------- // SoundBuffer::loadAac - macOS implementation (file-based) // ----------------------------------------------------------------------------- -bool SoundBuffer::loadAac(const std::string& path) { +LoadResult SoundBuffer::loadAac(const fs::path& path) { // Convert path to URL - NSString* nsPath = [NSString stringWithUTF8String:path.c_str()]; + std::string pathStr = internal::pathToUtf8(path); + + std::error_code ec; + if (!fs::exists(path, ec)) { + return LoadResult::fail(LoadError::FileNotFound, + "file not found: " + pathStr); + } + + NSString* nsPath = [NSString stringWithUTF8String:pathStr.c_str()]; NSURL* fileURL = [NSURL fileURLWithPath:nsPath]; if (!fileURL) { - printf("SoundBuffer: invalid path: %s\n", path.c_str()); - return false; + printf("SoundBuffer: invalid path: %s\n", pathStr.c_str()); + return LoadResult::fail(LoadError::Unknown, + "invalid path: " + pathStr); } // Open audio file @@ -64,7 +73,9 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { if (status != noErr || !extAudioFile) { printf("SoundBuffer: Failed to open AAC file: %s (status: %d)\n", path.c_str(), (int)status); - return false; + return LoadResult::fail(LoadError::DecodeFailed, + "failed to open AAC file: " + pathStr + + " (status=" + std::to_string((int)status) + ")"); } // Get source format @@ -77,7 +88,9 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { if (status != noErr) { printf("SoundBuffer: Failed to get AAC format\n"); ExtAudioFileDispose(extAudioFile); - return false; + return LoadResult::fail(LoadError::DecodeFailed, + "failed to get AAC format (status=" + + std::to_string((int)status) + ")"); } // Set output format (32-bit float PCM) @@ -98,7 +111,9 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { if (status != noErr) { printf("SoundBuffer: Failed to set output format\n"); ExtAudioFileDispose(extAudioFile); - return false; + return LoadResult::fail(LoadError::DecodeFailed, + "failed to set output format (status=" + + std::to_string((int)status) + ")"); } // Get total frame count @@ -111,7 +126,9 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { if (status != noErr || totalFrames <= 0) { printf("SoundBuffer: Failed to get frame count\n"); ExtAudioFileDispose(extAudioFile); - return false; + return LoadResult::fail(LoadError::DecodeFailed, + "failed to get frame count (status=" + + std::to_string((int)status) + ")"); } // Allocate buffer @@ -135,7 +152,9 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { if (status != noErr) { printf("SoundBuffer: Failed to read AAC data (status: %d)\n", (int)status); samples.clear(); - return false; + return LoadResult::fail(LoadError::DecodeFailed, + "failed to read AAC data (status=" + + std::to_string((int)status) + ")"); } // Update actual sample count @@ -145,16 +164,16 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { printf("SoundBuffer: loaded AAC %s (%d ch, %d Hz, %zu samples)\n", path.c_str(), channels, sampleRate, numSamples); - return true; + return LoadResult::success(); } // ----------------------------------------------------------------------------- // SoundBuffer::loadAacFromMemory - macOS implementation (memory-based) // ----------------------------------------------------------------------------- -bool SoundBuffer::loadAacFromMemory(const void* data, size_t dataSize) { +LoadResult SoundBuffer::loadAacFromMemory(const void* data, size_t dataSize) { if (!data || dataSize == 0) { printf("SoundBuffer: AAC data is empty\n"); - return false; + return LoadResult::fail(LoadError::DecodeFailed, "empty memory range"); } // Setup memory file data @@ -203,7 +222,9 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { if (status != noErr || !audioFile) { printf("SoundBuffer: Failed to open AAC data (status: %d)\n", (int)status); - return false; + return LoadResult::fail(LoadError::DecodeFailed, + "failed to open AAC data (status=" + + std::to_string((int)status) + ")"); } // Get source format @@ -213,7 +234,9 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { if (status != noErr) { printf("SoundBuffer: Failed to get AAC format\n"); AudioFileClose(audioFile); - return false; + return LoadResult::fail(LoadError::DecodeFailed, + "failed to get AAC format (status=" + + std::to_string((int)status) + ")"); } // Create ExtAudioFile for conversion @@ -222,7 +245,9 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { if (status != noErr || !extAudioFile) { printf("SoundBuffer: Failed to wrap audio file\n"); AudioFileClose(audioFile); - return false; + return LoadResult::fail(LoadError::DecodeFailed, + "failed to wrap audio file (status=" + + std::to_string((int)status) + ")"); } // Set output format (32-bit float PCM) @@ -244,7 +269,9 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { printf("SoundBuffer: Failed to set output format\n"); ExtAudioFileDispose(extAudioFile); AudioFileClose(audioFile); - return false; + return LoadResult::fail(LoadError::DecodeFailed, + "failed to set output format (status=" + + std::to_string((int)status) + ")"); } // Get total frame count @@ -258,7 +285,9 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { printf("SoundBuffer: Failed to get frame count\n"); ExtAudioFileDispose(extAudioFile); AudioFileClose(audioFile); - return false; + return LoadResult::fail(LoadError::DecodeFailed, + "failed to get frame count (status=" + + std::to_string((int)status) + ")"); } // Allocate buffer @@ -283,7 +312,9 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { if (status != noErr) { printf("SoundBuffer: Failed to read AAC data (status: %d)\n", (int)status); samples.clear(); - return false; + return LoadResult::fail(LoadError::DecodeFailed, + "failed to read AAC data (status=" + + std::to_string((int)status) + ")"); } // Update actual sample count (in case fewer frames were read) @@ -293,7 +324,7 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { printf("SoundBuffer: decoded AAC from memory (%d ch, %d Hz, %zu samples)\n", channels, sampleRate, numSamples); - return true; + return LoadResult::success(); } } // namespace trussc diff --git a/core/platform/ios/tcSystemFont_ios.mm b/core/platform/ios/tcSystemFont_ios.mm index ce0aeb05..3b7f9995 100644 --- a/core/platform/ios/tcSystemFont_ios.mm +++ b/core/platform/ios/tcSystemFont_ios.mm @@ -37,7 +37,7 @@ } // namespace -std::string systemFontPath(const std::string& name) { +fs::path systemFontPath(const std::string& name) { if (name.empty()) return ""; CFStringRef cfName = CFStringCreateWithCString(nullptr, name.c_str(), kCFStringEncodingUTF8); if (!cfName) return ""; diff --git a/core/platform/ios/tcVideoPlayer_ios.mm b/core/platform/ios/tcVideoPlayer_ios.mm index 43365a64..afc2ef9b 100644 --- a/core/platform/ios/tcVideoPlayer_ios.mm +++ b/core/platform/ios/tcVideoPlayer_ios.mm @@ -642,11 +642,11 @@ - (NSData*)extractAudioData { namespace trussc { -bool VideoPlayer::loadPlatform(const std::string& path) { +bool VideoPlayer::loadPlatform(const fs::path& path) { // Create Objective-C implementation TCVideoPlayerImpl* impl = [[TCVideoPlayerImpl alloc] init]; - NSString* nsPath = [NSString stringWithUTF8String:path.c_str()]; + NSString* nsPath = [NSString stringWithUTF8String:internal::pathToUtf8(path).c_str()]; if (![impl loadWithPath:nsPath]) { return false; @@ -943,18 +943,18 @@ static bool tcv_extract_frame_ios(const std::string& path, Pixels& outPixels, } } -bool VideoPlayer::extractFramePlatform(const std::string& path, Pixels& outPixels, +bool VideoPlayer::extractFramePlatform(const fs::path& path, Pixels& outPixels, float timeSec, float* outDuration) { - return tcv_extract_frame_ios(path, outPixels, timeSec, outDuration, /*exact=*/true); + return tcv_extract_frame_ios(path.string(), outPixels, timeSec, outDuration, /*exact=*/true); } -bool VideoPlayer::extractKeyFramePlatform(const std::string& path, Pixels& outPixels, +bool VideoPlayer::extractKeyFramePlatform(const fs::path& path, Pixels& outPixels, float timeSec, float* outDuration) { - if (tcv_extract_frame_ios(path, outPixels, timeSec, outDuration, /*exact=*/false)) { + if (tcv_extract_frame_ios(path.string(), outPixels, timeSec, outDuration, /*exact=*/false)) { return true; } // No keyframe reachable - fall back to an exact decode. - return tcv_extract_frame_ios(path, outPixels, timeSec, outDuration, /*exact=*/true); + return tcv_extract_frame_ios(path.string(), outPixels, timeSec, outDuration, /*exact=*/true); } } // namespace trussc diff --git a/core/platform/linux/sokol_impl.cpp b/core/platform/linux/sokol_impl.cpp index 0a76b000..14865699 100644 --- a/core/platform/linux/sokol_impl.cpp +++ b/core/platform/linux/sokol_impl.cpp @@ -1,8 +1,7 @@ // ============================================================================= -// sokol バックエンド実装 (Windows / Linux) +// sokol バックエンド実装 (Linux / Raspberry Pi) // ============================================================================= -#define SOKOL_IMPL #define SOKOL_NO_ENTRY // main() を自分で定義するため // Suppress warnings from third-party sokol headers. They have intentional @@ -14,8 +13,10 @@ # pragma GCC diagnostic ignored "-Wmissing-field-initializers" #endif +#define SOKOL_IMPL #include "sokol_log.h" -#include "sokol_app.h" +#define SOKOL_APP_TC_IMPL +#include "sokol_app_tc.h" #include "sokol_gfx.h" #include "sokol_glue.h" #include "util/sokol_gl_tc.h" diff --git a/core/platform/linux/tcFileDialog_linux.cpp b/core/platform/linux/tcFileDialog_linux.cpp index d2579252..3351615f 100644 --- a/core/platform/linux/tcFileDialog_linux.cpp +++ b/core/platform/linux/tcFileDialog_linux.cpp @@ -25,13 +25,6 @@ bool initGtk() { return true; } -// Extract filename from path -std::string extractFileName(const std::string& path) { - size_t pos = path.find_last_of('/'); - if (pos == std::string::npos) return path; - return path.substr(pos + 1); -} - // Process GTK events void processGtkEvents() { while (gtk_events_pending()) { @@ -114,7 +107,7 @@ void confirmDialogAsync(const std::string& title, // ----------------------------------------------------------------------------- FileDialogResult loadDialog(const std::string& title, const std::string& message, - const std::string& defaultPath, + const fs::path& defaultPath, bool folderSelection) { FileDialogResult result; (void)message; // GTK file chooser doesn't have a message field @@ -151,7 +144,7 @@ FileDialogResult loadDialog(const std::string& title, if (filename) { result.success = true; result.filePath = filename; - result.fileName = extractFileName(result.filePath); + result.fileName = result.filePath.filename(); g_free(filename); } } @@ -164,7 +157,7 @@ FileDialogResult loadDialog(const std::string& title, void loadDialogAsync(const std::string& title, const std::string& message, - const std::string& defaultPath, + const fs::path& defaultPath, bool folderSelection, std::function callback) { FileDialogResult result = loadDialog(title, message, defaultPath, folderSelection); @@ -176,8 +169,8 @@ void loadDialogAsync(const std::string& title, // ----------------------------------------------------------------------------- FileDialogResult saveDialog(const std::string& title, const std::string& message, - const std::string& defaultPath, - const std::string& defaultName) { + const fs::path& defaultPath, + const fs::path& defaultName) { FileDialogResult result; (void)message; // GTK file chooser doesn't have a message field @@ -210,7 +203,7 @@ FileDialogResult saveDialog(const std::string& title, if (filename) { result.success = true; result.filePath = filename; - result.fileName = extractFileName(result.filePath); + result.fileName = result.filePath.filename(); g_free(filename); } } @@ -223,8 +216,8 @@ FileDialogResult saveDialog(const std::string& title, void saveDialogAsync(const std::string& title, const std::string& message, - const std::string& defaultPath, - const std::string& defaultName, + const fs::path& defaultPath, + const fs::path& defaultName, std::function callback) { FileDialogResult result = saveDialog(title, message, defaultPath, defaultName); if (callback) callback(result); diff --git a/core/platform/linux/tcPlatform_linux.cpp b/core/platform/linux/tcPlatform_linux.cpp index be0ac9ee..1f8c7073 100644 --- a/core/platform/linux/tcPlatform_linux.cpp +++ b/core/platform/linux/tcPlatform_linux.cpp @@ -10,7 +10,7 @@ #include #include #include -#include "sokol_app.h" +#include "sokol_app_tc.h" // OpenGL for screen capture #include @@ -20,7 +20,8 @@ namespace trussc { void bringWindowToFront() { Display* display = XOpenDisplay(nullptr); if (!display) return; - Window window = (Window)(uintptr_t)sapp_x11_get_window(); + // ::Window = the X11 window id type (trussc::Window shadows it in this ns) + ::Window window = (::Window)(uintptr_t)sapp_x11_get_window(); if (window) { XRaiseWindow(display, window); XSetInputFocus(display, window, RevertToParent, CurrentTime); @@ -82,7 +83,8 @@ void setWindowPosition(int x, int y) { void setWindowDecorated(bool decorated) { Display* display = XOpenDisplay(nullptr); if (!display) return; - Window window = (Window)(uintptr_t)sapp_x11_get_window(); + // ::Window = the X11 window id type (trussc::Window shadows it in this ns) + ::Window window = (::Window)(uintptr_t)sapp_x11_get_window(); if (window) { // Motif WM hints: clear the decorations flag to drop the WM-drawn frame. struct { @@ -109,23 +111,20 @@ void setWindowSizeLogical(int width, int height) { logWarning("Platform") << "setWindowSize not yet implemented on Linux"; } -std::string getExecutablePath() { +fs::path getExecutablePath() { char path[PATH_MAX]; ssize_t len = readlink("/proc/self/exe", path, sizeof(path) - 1); if (len != -1) { path[len] = '\0'; - return std::string(path); + return fs::path(path); } - return ""; + return {}; } -std::string getExecutableDir() { - std::string exePath = getExecutablePath(); - size_t lastSlash = exePath.rfind('/'); - if (lastSlash != std::string::npos) { - return exePath.substr(0, lastSlash + 1); - } - return "./"; +fs::path getExecutableDir() { + fs::path exePath = getExecutablePath(); + if (!exePath.empty()) return exePath.parent_path(); + return fs::path("."); } // --------------------------------------------------------------------------- @@ -164,7 +163,7 @@ bool captureWindow(Pixels& outPixels) { bool internal::captureWindowToFile(const std::filesystem::path& path) { if (path.is_relative()) { - return internal::captureWindowToFile(getDataPath(path.string())); + return internal::captureWindowToFile(getDataPath(path)); } Pixels pixels; if (!captureWindow(pixels)) { @@ -173,7 +172,7 @@ bool internal::captureWindowToFile(const std::filesystem::path& path) { // Use stb_image_write to save std::string ext = path.extension().string(); - std::string pathStr = path.string(); + std::string pathStr = internal::pathToUtf8(path); // UTF-8 for stb (STBIW_WINDOWS_UTF8) int width = pixels.getWidth(); int height = pixels.getHeight(); diff --git a/core/platform/linux/tcSound_linux.cpp b/core/platform/linux/tcSound_linux.cpp index 54a08f0a..99f8f487 100644 --- a/core/platform/linux/tcSound_linux.cpp +++ b/core/platform/linux/tcSound_linux.cpp @@ -251,14 +251,26 @@ class GstAacDecoder { size_t memoryPos_ = 0; }; -bool SoundBuffer::loadAac(const std::string& path) { +LoadResult SoundBuffer::loadAac(const fs::path& path) { + std::error_code ec; + if (!fs::exists(path, ec)) { + return LoadResult::fail(LoadError::FileNotFound, + "file not found: " + internal::pathToUtf8(path)); + } GstAacDecoder decoder; - return decoder.decodeFile(path, *this); + return decoder.decodeFile(internal::pathToUtf8(path), *this) + ? LoadResult::success() + : LoadResult::fail(LoadError::DecodeFailed, + "GStreamer failed to decode AAC: " + + internal::pathToUtf8(path)); } -bool SoundBuffer::loadAacFromMemory(const void* data, size_t dataSize) { +LoadResult SoundBuffer::loadAacFromMemory(const void* data, size_t dataSize) { GstAacDecoder decoder; - return decoder.decodeMemory(data, dataSize, *this); + return decoder.decodeMemory(data, dataSize, *this) + ? LoadResult::success() + : LoadResult::fail(LoadError::DecodeFailed, + "GStreamer failed to decode AAC from memory"); } } // namespace trussc diff --git a/core/platform/linux/tcSystemFont_linux.cpp b/core/platform/linux/tcSystemFont_linux.cpp index 90ee23d1..b0ea627b 100644 --- a/core/platform/linux/tcSystemFont_linux.cpp +++ b/core/platform/linux/tcSystemFont_linux.cpp @@ -45,7 +45,7 @@ bool matchLooksLikeRequest(FcPattern* match, const std::string& wanted) { } // namespace -std::string systemFontPath(const std::string& name) { +fs::path systemFontPath(const std::string& name) { if (name.empty()) return ""; // FcConfigGetCurrent() lazily calls FcInit() on first use, so we don't diff --git a/core/platform/linux/tcVideoPlayer_linux.cpp b/core/platform/linux/tcVideoPlayer_linux.cpp index 28b5dd64..2b580ec7 100644 --- a/core/platform/linux/tcVideoPlayer_linux.cpp +++ b/core/platform/linux/tcVideoPlayer_linux.cpp @@ -1212,10 +1212,11 @@ class NV12VideoShader : public Shader { namespace trussc { -bool VideoPlayer::loadPlatform(const std::string& path) { +bool VideoPlayer::loadPlatform(const fs::path& path) { auto impl = new TCVideoPlayerImpl(); - if (!impl->load(path, this)) { + // FFmpeg takes narrow UTF-8 (Linux native) + if (!impl->load(internal::pathToUtf8(path), this)) { delete impl; return false; } @@ -1539,18 +1540,19 @@ static bool tcv_extract_frame_linux(const std::string& path, Pixels& outPixels, return ok; } -bool VideoPlayer::extractFramePlatform(const std::string& path, Pixels& outPixels, +bool VideoPlayer::extractFramePlatform(const fs::path& path, Pixels& outPixels, float timeSec, float* outDuration) { - return tcv_extract_frame_linux(path, outPixels, timeSec, outDuration, /*exact=*/true); + return tcv_extract_frame_linux(internal::pathToUtf8(path), outPixels, timeSec, outDuration, /*exact=*/true); } -bool VideoPlayer::extractKeyFramePlatform(const std::string& path, Pixels& outPixels, +bool VideoPlayer::extractKeyFramePlatform(const fs::path& path, Pixels& outPixels, float timeSec, float* outDuration) { - if (tcv_extract_frame_linux(path, outPixels, timeSec, outDuration, /*exact=*/false)) { + std::string p = internal::pathToUtf8(path); + if (tcv_extract_frame_linux(p, outPixels, timeSec, outDuration, /*exact=*/false)) { return true; } // No keyframe reachable — fall back to an exact decode. - return tcv_extract_frame_linux(path, outPixels, timeSec, outDuration, /*exact=*/true); + return tcv_extract_frame_linux(p, outPixels, timeSec, outDuration, /*exact=*/true); } } // namespace trussc diff --git a/core/platform/linux/tcWindowLinux.cpp b/core/platform/linux/tcWindowLinux.cpp new file mode 100644 index 00000000..43f67011 --- /dev/null +++ b/core/platform/linux/tcWindowLinux.cpp @@ -0,0 +1,334 @@ +// ============================================================================= +// tcWindowLinux.cpp - TrussC adapter for secondary windows (Linux / X11) +// ============================================================================= +// The native windowing layer (X11 window + shared GLX context with one +// GLXWindow drawable per window, timer-paced ticks at the measured refresh +// rate, sapp_event conversion with the full sokol_app XKB keytable) lives in +// sokol/sokol_app_tc.h and knows nothing about TrussC. This file maps its C +// API onto TrussC: +// tick_cb -> WindowContext switch + per-window dt + update/draw the tree +// event_cb -> CoreEvents + App hooks + Node-tree dispatch (same keycode +// semantics as the main window: key == SAPP_KEYCODE_*) +// close_cb -> Window::close() +// All rendering shares the one sokol_gfx context; each window gets its own +// sokol_gl context (same pattern as Fbo). Mirror of platform/win/tcWindowWin.cpp. + +#include "TrussC.h" + +#if defined(__linux__) && defined(SOKOL_GLCORE) && !defined(__ANDROID__) +#include "sokol_app_tc.h" // declarations only (impl lives in sokol_impl.cpp) + +using namespace trussc; + +namespace { + +struct AdapterState { + sapp_window win{0}; +}; + +// Swapchain provider handed to WindowContext::acquireSwapchain. On GL the +// "swapchain" is just the default framebuffer of whatever drawable is +// current -- which is this window's during its tick. +sg_swapchain acquireSecondarySwapchain(void* user) { + auto* st = static_cast(user); + sg_swapchain sc = {}; + if (!st) return sc; + const int fbw = sapp_window_framebuffer_width(st->win); + const int fbh = sapp_window_framebuffer_height(st->win); + if (fbw <= 0 || fbh <= 0) return sc; + sc.width = fbw; + sc.height = fbh; + sc.sample_count = sapp_window_sample_count(st->win); + sc.color_format = SG_PIXELFORMAT_RGBA8; + sc.depth_format = SG_PIXELFORMAT_DEPTH_STENCIL; + sc.gl.framebuffer = sapp_window_gl_framebuffer(st->win); + return sc; +} + +// --- tick: drive this window's update/draw with its context active --------- +void windowTick(sapp_window swin, void* user) { + Window* win = static_cast(user); + if (!win) return; + auto& ctx = win->context(); + + const int fbw = sapp_window_framebuffer_width(swin); + const int fbh = sapp_window_framebuffer_height(swin); + if (fbw <= 0 || fbh <= 0) return; + ctx.fbWidth = fbw; + ctx.fbHeight = fbh; + ctx.dpiScale = sapp_window_dpi_scale(swin); + // Keep a RectNode root in sync with the window's logical size (same + // contract as the main App on SAPP_EVENTTYPE_RESIZED). + win->syncRootSize((float)sapp_window_width(swin), (float)sapp_window_height(swin)); + + // Per-window delta time: wall-clock between THIS window's processed + // ticks, so getDeltaTime() inside this window's update/draw is correct + // even when its display runs at a different refresh rate than the main + // window's. A long gap (occlusion) yields one large delta, exactly like + // an event-driven main window. + { + auto callNow = std::chrono::high_resolution_clock::now(); + if (!ctx.lastUpdateCallTimeInitialized) { + ctx.lastUpdateCallTimeInitialized = true; + ctx.updateDeltaTime = sapp_window_frame_duration(swin); + } else { + ctx.updateDeltaTime = std::chrono::duration(callNow - ctx.lastUpdateCallTime).count(); + } + ctx.lastUpdateCallTime = callNow; + } + + auto* prev = internal::currentWindowCtx; + internal::currentWindowCtx = &ctx; + + // Own sokol_gl context, created lazily on the first tick (sgl is set up + // by then). Same lifecycle caveats as Fbo contexts on sgl resize. + if (ctx.swapchainTarget.context.id == SG_INVALID_ID) { + sgl_context_desc_t cdesc = {}; + cdesc.max_vertices = 65536; + cdesc.max_commands = 16384; + cdesc.color_format = SG_PIXELFORMAT_RGBA8; + cdesc.depth_format = SG_PIXELFORMAT_DEPTH_STENCIL; + cdesc.sample_count = sapp_window_sample_count(swin); + ctx.swapchainTarget.context = sgl_make_context(&cdesc); + } + sgl_set_context(ctx.swapchainTarget.context); + sgl_defaults(); + + beginFrame(); + + win->events().update.notify(); + win->tickTree(); + + const Color& cc = win->clearColor_; + clear(cc.r, cc.g, cc.b, cc.a); + win->events().draw.notify(); + win->drawTreeNow(); + + present(); + win->events().afterFrame.notify(); + + sgl_set_context(sgl_default_context()); + internal::currentWindowCtx = prev; +} + +// --- events: map sapp_event onto CoreEvents / App hooks / tree dispatch ---- +void windowEvent(const sapp_event* ev, sapp_window swin, void* user) { + Window* win = static_cast(user); + if (!win) return; + auto& ctx = win->context(); + auto* prev = internal::currentWindowCtx; + internal::currentWindowCtx = &ctx; + + // Raw event pass-through (same hook addons use on the main window) + win->events().rawEvent.notify(*ev); + + // sapp coords arrive in framebuffer pixels; TrussC windows use logical + // points (secondary windows have no pixelPerfect mode for now) + const float dpi = sapp_window_dpi_scale(swin); + const float scale = (dpi > 0.0f) ? (1.0f / dpi) : 1.0f; + const bool hasShift = (ev->modifiers & SAPP_MODIFIER_SHIFT) != 0; + const bool hasCtrl = (ev->modifiers & SAPP_MODIFIER_CTRL) != 0; + const bool hasAlt = (ev->modifiers & SAPP_MODIFIER_ALT) != 0; + const bool hasSuper = (ev->modifiers & SAPP_MODIFIER_SUPER) != 0; + + switch (ev->type) { + case SAPP_EVENTTYPE_MOUSE_DOWN: { + const float x = ev->mouse_x * scale, y = ev->mouse_y * scale; + ctx.mouseX = x; ctx.mouseY = y; + ctx.mouseButton = (int)ev->mouse_button; + ctx.mousePressed = true; + MouseEventArgs e; + e.pos = e.globalPos = Vec2(x, y); + e.button = (int)ev->mouse_button; + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + e.syncLegacy(); + win->events().mousePressed.notify(e); + if (win->getApp()) win->getApp()->mousePressed(e); + win->dispatchMousePressToTree(e); + break; + } + case SAPP_EVENTTYPE_MOUSE_UP: { + const float x = ev->mouse_x * scale, y = ev->mouse_y * scale; + ctx.mouseX = x; ctx.mouseY = y; + ctx.mouseButton = -1; + ctx.mousePressed = false; + MouseEventArgs e; + e.pos = e.globalPos = Vec2(x, y); + e.button = (int)ev->mouse_button; + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + e.syncLegacy(); + win->events().mouseReleased.notify(e); + if (win->getApp()) win->getApp()->mouseReleased(e); + win->dispatchMouseReleaseToTree(e); + break; + } + case SAPP_EVENTTYPE_MOUSE_ENTER: + case SAPP_EVENTTYPE_MOUSE_MOVE: { + const float x = ev->mouse_x * scale, y = ev->mouse_y * scale; + ctx.pmouseX = ctx.mouseX; ctx.pmouseY = ctx.mouseY; + ctx.mouseX = x; ctx.mouseY = y; + internal::MouseEventRaw raw; + raw.pos = raw.globalPos = Vec2(x, y); + raw.delta = raw.globalDelta = Vec2(ev->mouse_dx * scale, ev->mouse_dy * scale); + raw.shift = hasShift; raw.ctrl = hasCtrl; raw.alt = hasAlt; raw.super = hasSuper; + if (ctx.mousePressed && ctx.mouseButton >= 0) { + raw.button = ctx.mouseButton; + MouseDragEventArgs e = internal::toDragArgs(raw); + win->events().mouseDragged.notify(e); + if (win->getApp()) win->getApp()->mouseDragged(e); + } else { + MouseMoveEventArgs e = internal::toMoveArgs(raw); + win->events().mouseMoved.notify(e); + if (win->getApp()) win->getApp()->mouseMoved(e); + } + break; + } + case SAPP_EVENTTYPE_MOUSE_LEAVE: { + // Park the cursor offscreen so the next tick's updateHoverState + // clears this window's hover (hoveredNode lives in ITS context). + ctx.pmouseX = ctx.mouseX; ctx.pmouseY = ctx.mouseY; + ctx.mouseX = -1.0f; ctx.mouseY = -1.0f; + break; + } + case SAPP_EVENTTYPE_MOUSE_SCROLL: { + ScrollEventArgs e; + e.pos = e.globalPos = Vec2(ctx.mouseX, ctx.mouseY); + e.scroll = Vec2(ev->scroll_x, ev->scroll_y); + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + e.syncLegacy(); + win->events().mouseScrolled.notify(e); + if (win->getApp()) win->getApp()->mouseScrolled(e); + break; + } + case SAPP_EVENTTYPE_KEY_DOWN: { + KeyEventArgs e; + e.key = ev->key_code; // SAPP_KEYCODE_*, identical to the main window + e.isRepeat = ev->key_repeat; + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + if (!ev->key_repeat) ctx.keysPressed.insert(e.key); + win->events().keyPressed.notify(e); + if (win->getApp()) win->getApp()->keyPressed(e); + break; + } + case SAPP_EVENTTYPE_KEY_UP: { + KeyEventArgs e; + e.key = ev->key_code; + e.isRepeat = false; + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + ctx.keysPressed.erase(e.key); + win->events().keyReleased.notify(e); + if (win->getApp()) win->getApp()->keyReleased(e); + break; + } + case SAPP_EVENTTYPE_SUSPENDED: + logNotice("Window") << "second window occluded - skipping frames (no stall)"; + break; + case SAPP_EVENTTYPE_RESUMED: + logNotice("Window") << "second window visible again - resuming ticks"; + break; + default: + // CHAR / RESIZED / FOCUSED / UNFOCUSED: rawEvent carries them; + // size sync happens per tick. + break; + } + + internal::currentWindowCtx = prev; +} + +void windowClosed(sapp_window swin, void* user) { + (void)swin; + Window* win = static_cast(user); + if (win) win->close(); // tears down native + app state +} + +} // namespace + +// --------------------------------------------------------------------------- +// Window methods (Linux implementations) +// --------------------------------------------------------------------------- +namespace trussc { + +Window::~Window() { close(); } + +void Window::close() { + auto* st = static_cast(native_); + if (!st) return; + native_ = nullptr; // re-entrancy guard (close_cb) + ctx_.acquireSwapchain = nullptr; + ctx_.acquireSwapchainUser = nullptr; + sapp_destroy_window(st->win); + if (ctx_.swapchainTarget.context.id != SG_INVALID_ID) { + sgl_destroy_context(ctx_.swapchainTarget.context); + ctx_.swapchainTarget.context.id = SG_INVALID_ID; + ctx_.swapchainTarget.cache.clear(); + } + delete st; + // The window's own exit event: per-window resource holders (e.g. the + // tcxImGui per-window manager) tear down here, while this window's + // CoreEvents is still alive. + events_.exit.notify(); + if (app_) { + app_->exit(); + app_->cleanup(); + internal::attachedApps.erase(app_.get()); + app_.reset(); + ctx_.rootNode = nullptr; + } +} + +void Window::setTitle(const std::string& title) { + title_ = title; + auto* st = static_cast(native_); + if (st) sapp_window_set_title(st->win, title.c_str()); +} + +int Window::getWidth() const { + auto* st = static_cast(native_); + return st ? sapp_window_width(st->win) : 0; +} + +int Window::getHeight() const { + auto* st = static_cast(native_); + return st ? sapp_window_height(st->win) : 0; +} + +std::shared_ptr createWindow(const WindowSettings& settings) { + if (headless::isActive()) { + logError("Window") << "createWindow(): not available in headless mode"; + return nullptr; + } + auto win = std::shared_ptr(new Window()); + win->title_ = settings.title; + win->ctx_.swapchainColorFormat = SG_PIXELFORMAT_RGBA8; + win->ctx_.swapchainSampleCount = settings.sampleCount > 0 ? settings.sampleCount : 1; + auto* st = new AdapterState(); + + sapp_window_desc d = {}; + d.x = -1; + d.y = -1; + d.width = settings.width; + d.height = settings.height; + d.title = settings.title.c_str(); + d.borderless = !settings.decorated; + d.no_high_dpi = !settings.highDpi; + d.sample_count = settings.sampleCount; // NOTE: forced to the shared fbconfig's count on GLX + d.tick_cb = &windowTick; + d.event_cb = &windowEvent; + d.close_cb = &windowClosed; + d.user_data = win.get(); + st->win = sapp_create_window(&d); + if (st->win.id == 0) { + delete st; + logError("Window") << "createWindow(): native window creation failed"; + return nullptr; + } + + win->native_ = st; + win->ctx_.acquireSwapchain = &acquireSecondarySwapchain; + win->ctx_.acquireSwapchainUser = st; + return win; +} + +} // namespace trussc + +#endif // __linux__ && SOKOL_GLCORE && !__ANDROID__ diff --git a/core/platform/mac/sokol_impl.mm b/core/platform/mac/sokol_impl.mm index 80211689..eaf4763c 100644 --- a/core/platform/mac/sokol_impl.mm +++ b/core/platform/mac/sokol_impl.mm @@ -2,7 +2,6 @@ // sokol バックエンド実装 (macOS / Metal) // ============================================================================= -#define SOKOL_IMPL #define SOKOL_NO_ENTRY // main() を自分で定義するため #if defined(__GNUC__) || defined(__clang__) @@ -12,8 +11,13 @@ # pragma GCC diagnostic ignored "-Wmissing-field-initializers" #endif +// sokol_app_tc.h is the self-contained sapp_* implementation on macOS +// (main window, run loop, events, multi-window API). + +#define SOKOL_IMPL #include "sokol_log.h" -#include "sokol_app.h" +#define SOKOL_APP_TC_IMPL +#include "sokol_app_tc.h" #include "sokol_gfx.h" #include "sokol_glue.h" #include "util/sokol_gl_tc.h" diff --git a/core/platform/mac/tcFileDialog_mac.mm b/core/platform/mac/tcFileDialog_mac.mm index cd85b476..60ef6642 100644 --- a/core/platform/mac/tcFileDialog_mac.mm +++ b/core/platform/mac/tcFileDialog_mac.mm @@ -90,7 +90,7 @@ void confirmDialogAsync(const std::string& title, // ----------------------------------------------------------------------------- FileDialogResult loadDialog(const std::string& title, const std::string& message, - const std::string& defaultPath, + const fs::path& defaultPath, bool folderSelection) { FileDialogResult result; @@ -137,7 +137,7 @@ FileDialogResult loadDialog(const std::string& title, void loadDialogAsync(const std::string& title, const std::string& message, - const std::string& defaultPath, + const fs::path& defaultPath, bool folderSelection, std::function callback) { @autoreleasepool { @@ -194,8 +194,8 @@ void loadDialogAsync(const std::string& title, // ----------------------------------------------------------------------------- FileDialogResult saveDialog(const std::string& title, const std::string& message, - const std::string& defaultPath, - const std::string& defaultName) { + const fs::path& defaultPath, + const fs::path& defaultName) { FileDialogResult result; @autoreleasepool { @@ -233,8 +233,8 @@ FileDialogResult saveDialog(const std::string& title, void saveDialogAsync(const std::string& title, const std::string& message, - const std::string& defaultPath, - const std::string& defaultName, + const fs::path& defaultPath, + const fs::path& defaultName, std::function callback) { @autoreleasepool { NSSavePanel* panel = [NSSavePanel savePanel]; diff --git a/core/platform/mac/tcPixels_mac.mm b/core/platform/mac/tcPixels_mac.mm index 9a076712..e9e28b07 100644 --- a/core/platform/mac/tcPixels_mac.mm +++ b/core/platform/mac/tcPixels_mac.mm @@ -15,7 +15,7 @@ bool Pixels::loadPlatform(const fs::path& path) { @autoreleasepool { - NSString* nsPath = [NSString stringWithUTF8String:path.string().c_str()]; + NSString* nsPath = [NSString stringWithUTF8String:internal::pathToUtf8(path).c_str()]; NSURL* url = [NSURL fileURLWithPath:nsPath]; // Create image source diff --git a/core/platform/mac/tcPlatform_mac.mm b/core/platform/mac/tcPlatform_mac.mm index 8364eb01..80144050 100644 --- a/core/platform/mac/tcPlatform_mac.mm +++ b/core/platform/mac/tcPlatform_mac.mm @@ -18,7 +18,7 @@ #import // sokol_app の swapchain 取得関数 -#include "sokol_app.h" +#include "sokol_app_tc.h" namespace trussc { @@ -180,15 +180,13 @@ void setWindowSizeLogical(int width, int height) { } } -std::string getExecutablePath() { +fs::path getExecutablePath() { NSString* path = [[NSBundle mainBundle] executablePath]; - return std::string([path UTF8String]); + return fs::path([path UTF8String]); } -std::string getExecutableDir() { - NSString* path = [[NSBundle mainBundle] executablePath]; - NSString* dir = [path stringByDeletingLastPathComponent]; - return std::string([dir UTF8String]) + "/"; +fs::path getExecutableDir() { + return getExecutablePath().parent_path(); } // --------------------------------------------------------------------------- @@ -252,7 +250,7 @@ void setWindowSizeLogical(int width, int height) { // Read back the drawable this frame ACTUALLY rendered into (recorded by the // swapchain pass). NOT sapp_get_swapchain() — that advances to the next, // unrendered drawable. Fall back only if no pass ran yet. - const void* drawablePtr = internal::lastSwapchainDrawable; + const void* drawablePtr = internal::currentWindowContext().lastSwapchainDrawable; if (!drawablePtr) drawablePtr = sapp_get_swapchain().metal.current_drawable; id drawable = (__bridge id)drawablePtr; if (!drawable) return false; @@ -357,7 +355,7 @@ bool captureWindow(Pixels& outPixels) { bool internal::captureWindowToFile(const std::filesystem::path& path) { // Resolve relative paths if (path.is_relative()) { - return internal::captureWindowToFile(getDataPath(path.string())); + return internal::captureWindowToFile(getDataPath(path)); } // Capture to Pixels Pixels pixels; diff --git a/core/platform/mac/tcSound_mac.mm b/core/platform/mac/tcSound_mac.mm index 14446c44..cf439c33 100644 --- a/core/platform/mac/tcSound_mac.mm +++ b/core/platform/mac/tcSound_mac.mm @@ -47,14 +47,23 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { // ----------------------------------------------------------------------------- // SoundBuffer::loadAac - macOS implementation (file-based) // ----------------------------------------------------------------------------- -bool SoundBuffer::loadAac(const std::string& path) { +LoadResult SoundBuffer::loadAac(const fs::path& path) { // Convert path to URL - NSString* nsPath = [NSString stringWithUTF8String:path.c_str()]; + std::string pathStr = internal::pathToUtf8(path); + + std::error_code ec; + if (!fs::exists(path, ec)) { + return LoadResult::fail(LoadError::FileNotFound, + "file not found: " + pathStr); + } + + NSString* nsPath = [NSString stringWithUTF8String:pathStr.c_str()]; NSURL* fileURL = [NSURL fileURLWithPath:nsPath]; if (!fileURL) { - printf("SoundBuffer: invalid path: %s\n", path.c_str()); - return false; + printf("SoundBuffer: invalid path: %s\n", pathStr.c_str()); + return LoadResult::fail(LoadError::Unknown, + "invalid path: " + pathStr); } // Open audio file @@ -63,7 +72,9 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { if (status != noErr || !extAudioFile) { printf("SoundBuffer: Failed to open AAC file: %s (status: %d)\n", path.c_str(), (int)status); - return false; + return LoadResult::fail(LoadError::DecodeFailed, + "failed to open AAC file: " + pathStr + + " (status=" + std::to_string((int)status) + ")"); } // Get source format @@ -76,7 +87,9 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { if (status != noErr) { printf("SoundBuffer: Failed to get AAC format\n"); ExtAudioFileDispose(extAudioFile); - return false; + return LoadResult::fail(LoadError::DecodeFailed, + "failed to get AAC format (status=" + + std::to_string((int)status) + ")"); } // Set output format (32-bit float PCM) @@ -97,7 +110,9 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { if (status != noErr) { printf("SoundBuffer: Failed to set output format\n"); ExtAudioFileDispose(extAudioFile); - return false; + return LoadResult::fail(LoadError::DecodeFailed, + "failed to set output format (status=" + + std::to_string((int)status) + ")"); } // Get total frame count @@ -110,7 +125,9 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { if (status != noErr || totalFrames <= 0) { printf("SoundBuffer: Failed to get frame count\n"); ExtAudioFileDispose(extAudioFile); - return false; + return LoadResult::fail(LoadError::DecodeFailed, + "failed to get frame count (status=" + + std::to_string((int)status) + ")"); } // Allocate buffer @@ -134,7 +151,9 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { if (status != noErr) { printf("SoundBuffer: Failed to read AAC data (status: %d)\n", (int)status); samples.clear(); - return false; + return LoadResult::fail(LoadError::DecodeFailed, + "failed to read AAC data (status=" + + std::to_string((int)status) + ")"); } // Update actual sample count @@ -144,16 +163,16 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { printf("SoundBuffer: loaded AAC %s (%d ch, %d Hz, %zu samples)\n", path.c_str(), channels, sampleRate, numSamples); - return true; + return LoadResult::success(); } // ----------------------------------------------------------------------------- // SoundBuffer::loadAacFromMemory - macOS implementation (memory-based) // ----------------------------------------------------------------------------- -bool SoundBuffer::loadAacFromMemory(const void* data, size_t dataSize) { +LoadResult SoundBuffer::loadAacFromMemory(const void* data, size_t dataSize) { if (!data || dataSize == 0) { printf("SoundBuffer: AAC data is empty\n"); - return false; + return LoadResult::fail(LoadError::DecodeFailed, "empty memory range"); } // Setup memory file data @@ -202,7 +221,9 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { if (status != noErr || !audioFile) { printf("SoundBuffer: Failed to open AAC data (status: %d)\n", (int)status); - return false; + return LoadResult::fail(LoadError::DecodeFailed, + "failed to open AAC data (status=" + + std::to_string((int)status) + ")"); } // Get source format @@ -212,7 +233,9 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { if (status != noErr) { printf("SoundBuffer: Failed to get AAC format\n"); AudioFileClose(audioFile); - return false; + return LoadResult::fail(LoadError::DecodeFailed, + "failed to get AAC format (status=" + + std::to_string((int)status) + ")"); } // Create ExtAudioFile for conversion @@ -221,7 +244,9 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { if (status != noErr || !extAudioFile) { printf("SoundBuffer: Failed to wrap audio file\n"); AudioFileClose(audioFile); - return false; + return LoadResult::fail(LoadError::DecodeFailed, + "failed to wrap audio file (status=" + + std::to_string((int)status) + ")"); } // Set output format (32-bit float PCM) @@ -243,7 +268,9 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { printf("SoundBuffer: Failed to set output format\n"); ExtAudioFileDispose(extAudioFile); AudioFileClose(audioFile); - return false; + return LoadResult::fail(LoadError::DecodeFailed, + "failed to set output format (status=" + + std::to_string((int)status) + ")"); } // Get total frame count @@ -257,7 +284,9 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { printf("SoundBuffer: Failed to get frame count\n"); ExtAudioFileDispose(extAudioFile); AudioFileClose(audioFile); - return false; + return LoadResult::fail(LoadError::DecodeFailed, + "failed to get frame count (status=" + + std::to_string((int)status) + ")"); } // Allocate buffer @@ -282,7 +311,9 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { if (status != noErr) { printf("SoundBuffer: Failed to read AAC data (status: %d)\n", (int)status); samples.clear(); - return false; + return LoadResult::fail(LoadError::DecodeFailed, + "failed to read AAC data (status=" + + std::to_string((int)status) + ")"); } // Update actual sample count (in case fewer frames were read) @@ -292,7 +323,7 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { printf("SoundBuffer: decoded AAC from memory (%d ch, %d Hz, %zu samples)\n", channels, sampleRate, numSamples); - return true; + return LoadResult::success(); } } // namespace trussc diff --git a/core/platform/mac/tcSystemFont_mac.mm b/core/platform/mac/tcSystemFont_mac.mm index f75a3ab4..5dff532e 100644 --- a/core/platform/mac/tcSystemFont_mac.mm +++ b/core/platform/mac/tcSystemFont_mac.mm @@ -31,7 +31,7 @@ } // namespace -std::string systemFontPath(const std::string& name) { +fs::path systemFontPath(const std::string& name) { if (name.empty()) return ""; CFStringRef cfName = CFStringCreateWithCString(nullptr, name.c_str(), kCFStringEncodingUTF8); if (!cfName) return ""; diff --git a/core/platform/mac/tcVideoPlayer_mac.mm b/core/platform/mac/tcVideoPlayer_mac.mm index dc945267..b4830ace 100644 --- a/core/platform/mac/tcVideoPlayer_mac.mm +++ b/core/platform/mac/tcVideoPlayer_mac.mm @@ -643,11 +643,11 @@ - (NSData*)extractAudioData { namespace trussc { -bool VideoPlayer::loadPlatform(const std::string& path) { +bool VideoPlayer::loadPlatform(const fs::path& path) { // Create Objective-C implementation TCVideoPlayerImpl* impl = [[TCVideoPlayerImpl alloc] init]; - NSString* nsPath = [NSString stringWithUTF8String:path.c_str()]; + NSString* nsPath = [NSString stringWithUTF8String:internal::pathToUtf8(path).c_str()]; if (![impl loadWithPath:nsPath]) { return false; @@ -944,18 +944,19 @@ static bool tcv_extract_frame_mac(const std::string& path, Pixels& outPixels, } } -bool VideoPlayer::extractFramePlatform(const std::string& path, Pixels& outPixels, +bool VideoPlayer::extractFramePlatform(const fs::path& path, Pixels& outPixels, float timeSec, float* outDuration) { - return tcv_extract_frame_mac(path, outPixels, timeSec, outDuration, /*exact=*/true); + return tcv_extract_frame_mac(internal::pathToUtf8(path), outPixels, timeSec, outDuration, /*exact=*/true); } -bool VideoPlayer::extractKeyFramePlatform(const std::string& path, Pixels& outPixels, +bool VideoPlayer::extractKeyFramePlatform(const fs::path& path, Pixels& outPixels, float timeSec, float* outDuration) { - if (tcv_extract_frame_mac(path, outPixels, timeSec, outDuration, /*exact=*/false)) { + std::string p = internal::pathToUtf8(path); + if (tcv_extract_frame_mac(p, outPixels, timeSec, outDuration, /*exact=*/false)) { return true; } // No keyframe reachable — fall back to an exact decode. - return tcv_extract_frame_mac(path, outPixels, timeSec, outDuration, /*exact=*/true); + return tcv_extract_frame_mac(p, outPixels, timeSec, outDuration, /*exact=*/true); } } // namespace trussc diff --git a/core/platform/mac/tcWindowMac.mm b/core/platform/mac/tcWindowMac.mm new file mode 100644 index 00000000..6820e36a --- /dev/null +++ b/core/platform/mac/tcWindowMac.mm @@ -0,0 +1,338 @@ +// ============================================================================= +// tcWindowMac.mm - TrussC adapter for secondary windows (macOS) +// ============================================================================= +// The native windowing layer (NSWindow + CAMetalLayer + per-window +// CADisplayLink, occlusion-gated ticks, sapp_event conversion with the full +// sokol_app keycode table) lives in sokol/sokol_app_tc.h and knows nothing +// about TrussC. This file maps its C API onto TrussC: +// tick_cb -> WindowContext switch + per-window dt + update/draw the tree +// event_cb -> CoreEvents + App hooks + Node-tree dispatch (same keycode +// semantics as the main window: key == SAPP_KEYCODE_*) +// close_cb -> Window::close() +// All rendering shares the one sokol_gfx context; each window gets its own +// sokol_gl context (same pattern as Fbo). + +#include "TrussC.h" + +#if defined(__APPLE__) +#include "sokol_app_tc.h" // declarations only (impl lives in sokol_impl.mm) + +using namespace trussc; + +namespace { + +struct AdapterState { + sapp_window win{0}; +}; + +// Swapchain provider handed to WindowContext::acquireSwapchain. The drawable +// was acquired by the native layer at the top of the current tick (it is only +// ever acquired for a visible window; never blocks here). +sg_swapchain acquireSecondarySwapchain(void* user) { + auto* st = static_cast(user); + sg_swapchain sc = {}; + if (!st) return sc; + const void* drawable = sapp_window_metal_current_drawable(st->win); + if (!drawable) return sc; + sc.width = sapp_window_framebuffer_width(st->win); + sc.height = sapp_window_framebuffer_height(st->win); + sc.sample_count = sapp_window_sample_count(st->win); + sc.color_format = SG_PIXELFORMAT_BGRA8; + sc.depth_format = SG_PIXELFORMAT_DEPTH_STENCIL; + sc.metal.current_drawable = drawable; + sc.metal.depth_stencil_texture = sapp_window_metal_depth_stencil_texture(st->win); + if (sc.sample_count > 1) { + sc.metal.msaa_color_texture = sapp_window_metal_msaa_color_texture(st->win); + } + return sc; +} + +// --- tick: drive this window's update/draw with its context active --------- +void windowTick(sapp_window swin, void* user) { + Window* win = static_cast(user); + if (!win) return; + auto& ctx = win->context(); + + const int fbw = sapp_window_framebuffer_width(swin); + const int fbh = sapp_window_framebuffer_height(swin); + if (fbw <= 0 || fbh <= 0) return; + ctx.fbWidth = fbw; + ctx.fbHeight = fbh; + ctx.dpiScale = sapp_window_dpi_scale(swin); + // Keep a RectNode root in sync with the window's logical size (same + // contract as the main App on SAPP_EVENTTYPE_RESIZED). + win->syncRootSize((float)sapp_window_width(swin), (float)sapp_window_height(swin)); + + // Per-window delta time: wall-clock between THIS window's processed + // ticks, so getDeltaTime() inside this window's update/draw is correct + // even when its display runs at a different refresh rate than the main + // window's. A long gap (occlusion) yields one large delta, exactly like + // an event-driven main window. + { + auto callNow = std::chrono::high_resolution_clock::now(); + if (!ctx.lastUpdateCallTimeInitialized) { + ctx.lastUpdateCallTimeInitialized = true; + ctx.updateDeltaTime = sapp_window_frame_duration(swin); + } else { + ctx.updateDeltaTime = std::chrono::duration(callNow - ctx.lastUpdateCallTime).count(); + } + ctx.lastUpdateCallTime = callNow; + } + + auto* prev = internal::currentWindowCtx; + internal::currentWindowCtx = &ctx; + + // Own sokol_gl context, created lazily on the first tick (sgl is set up + // by then). Same lifecycle caveats as Fbo contexts on sgl resize. + if (ctx.swapchainTarget.context.id == SG_INVALID_ID) { + sgl_context_desc_t cdesc = {}; + cdesc.max_vertices = 65536; + cdesc.max_commands = 16384; + cdesc.color_format = SG_PIXELFORMAT_BGRA8; + cdesc.depth_format = SG_PIXELFORMAT_DEPTH_STENCIL; + cdesc.sample_count = sapp_window_sample_count(swin); + ctx.swapchainTarget.context = sgl_make_context(&cdesc); + } + sgl_set_context(ctx.swapchainTarget.context); + sgl_defaults(); + + beginFrame(); + + win->events().update.notify(); + win->tickTree(); + + const Color& cc = win->clearColor_; + clear(cc.r, cc.g, cc.b, cc.a); + win->events().draw.notify(); + win->drawTreeNow(); + + present(); + win->events().afterFrame.notify(); + + sgl_set_context(sgl_default_context()); + internal::currentWindowCtx = prev; +} + +// --- events: map sapp_event onto CoreEvents / App hooks / tree dispatch ---- +void windowEvent(const sapp_event* ev, sapp_window swin, void* user) { + Window* win = static_cast(user); + if (!win) return; + auto& ctx = win->context(); + auto* prev = internal::currentWindowCtx; + internal::currentWindowCtx = &ctx; + + // Raw event pass-through (same hook addons use on the main window) + win->events().rawEvent.notify(*ev); + + // sapp coords arrive in framebuffer pixels; TrussC windows use logical + // points (secondary windows have no pixelPerfect mode for now) + const float dpi = sapp_window_dpi_scale(swin); + const float scale = (dpi > 0.0f) ? (1.0f / dpi) : 1.0f; + const bool hasShift = (ev->modifiers & SAPP_MODIFIER_SHIFT) != 0; + const bool hasCtrl = (ev->modifiers & SAPP_MODIFIER_CTRL) != 0; + const bool hasAlt = (ev->modifiers & SAPP_MODIFIER_ALT) != 0; + const bool hasSuper = (ev->modifiers & SAPP_MODIFIER_SUPER) != 0; + + switch (ev->type) { + case SAPP_EVENTTYPE_MOUSE_DOWN: { + const float x = ev->mouse_x * scale, y = ev->mouse_y * scale; + ctx.mouseX = x; ctx.mouseY = y; + ctx.mouseButton = (int)ev->mouse_button; + ctx.mousePressed = true; + MouseEventArgs e; + e.pos = e.globalPos = Vec2(x, y); + e.button = (int)ev->mouse_button; + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + e.syncLegacy(); + win->events().mousePressed.notify(e); + if (win->getApp()) win->getApp()->mousePressed(e); + win->dispatchMousePressToTree(e); + break; + } + case SAPP_EVENTTYPE_MOUSE_UP: { + const float x = ev->mouse_x * scale, y = ev->mouse_y * scale; + ctx.mouseX = x; ctx.mouseY = y; + ctx.mouseButton = -1; + ctx.mousePressed = false; + MouseEventArgs e; + e.pos = e.globalPos = Vec2(x, y); + e.button = (int)ev->mouse_button; + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + e.syncLegacy(); + win->events().mouseReleased.notify(e); + if (win->getApp()) win->getApp()->mouseReleased(e); + win->dispatchMouseReleaseToTree(e); + break; + } + case SAPP_EVENTTYPE_MOUSE_ENTER: + case SAPP_EVENTTYPE_MOUSE_MOVE: { + const float x = ev->mouse_x * scale, y = ev->mouse_y * scale; + ctx.pmouseX = ctx.mouseX; ctx.pmouseY = ctx.mouseY; + ctx.mouseX = x; ctx.mouseY = y; + internal::MouseEventRaw raw; + raw.pos = raw.globalPos = Vec2(x, y); + raw.delta = raw.globalDelta = Vec2(ev->mouse_dx * scale, ev->mouse_dy * scale); + raw.shift = hasShift; raw.ctrl = hasCtrl; raw.alt = hasAlt; raw.super = hasSuper; + if (ctx.mousePressed && ctx.mouseButton >= 0) { + raw.button = ctx.mouseButton; + MouseDragEventArgs e = internal::toDragArgs(raw); + win->events().mouseDragged.notify(e); + if (win->getApp()) win->getApp()->mouseDragged(e); + } else { + MouseMoveEventArgs e = internal::toMoveArgs(raw); + win->events().mouseMoved.notify(e); + if (win->getApp()) win->getApp()->mouseMoved(e); + } + break; + } + case SAPP_EVENTTYPE_MOUSE_LEAVE: { + // Park the cursor offscreen so the next tick's updateHoverState + // clears this window's hover (hoveredNode lives in ITS context). + ctx.pmouseX = ctx.mouseX; ctx.pmouseY = ctx.mouseY; + ctx.mouseX = -1.0f; ctx.mouseY = -1.0f; + break; + } + case SAPP_EVENTTYPE_MOUSE_SCROLL: { + ScrollEventArgs e; + e.pos = e.globalPos = Vec2(ctx.mouseX, ctx.mouseY); + e.scroll = Vec2(ev->scroll_x, ev->scroll_y); + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + e.syncLegacy(); + win->events().mouseScrolled.notify(e); + if (win->getApp()) win->getApp()->mouseScrolled(e); + break; + } + case SAPP_EVENTTYPE_KEY_DOWN: { + KeyEventArgs e; + e.key = ev->key_code; // SAPP_KEYCODE_*, identical to the main window + e.isRepeat = ev->key_repeat; + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + if (!ev->key_repeat) ctx.keysPressed.insert(e.key); + win->events().keyPressed.notify(e); + if (win->getApp()) win->getApp()->keyPressed(e); + break; + } + case SAPP_EVENTTYPE_KEY_UP: { + KeyEventArgs e; + e.key = ev->key_code; + e.isRepeat = false; + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + ctx.keysPressed.erase(e.key); + win->events().keyReleased.notify(e); + if (win->getApp()) win->getApp()->keyReleased(e); + break; + } + case SAPP_EVENTTYPE_SUSPENDED: + logNotice("Window") << "second window occluded - skipping frames (no stall)"; + break; + case SAPP_EVENTTYPE_RESUMED: + logNotice("Window") << "second window visible again - resuming ticks"; + break; + default: + // CHAR / RESIZED / FOCUSED / UNFOCUSED: rawEvent carries them; + // size sync happens per tick. + break; + } + + internal::currentWindowCtx = prev; +} + +void windowClosed(sapp_window swin, void* user) { + (void)swin; + Window* win = static_cast(user); + if (win) win->close(); // tears down native + app state +} + +} // namespace + +// --------------------------------------------------------------------------- +// Window methods (macOS implementations) +// --------------------------------------------------------------------------- +namespace trussc { + +Window::~Window() { close(); } + +void Window::close() { + auto* st = static_cast(native_); + if (!st) return; + native_ = nullptr; // re-entrancy guard (close_cb) + ctx_.acquireSwapchain = nullptr; + ctx_.acquireSwapchainUser = nullptr; + sapp_destroy_window(st->win); + if (ctx_.swapchainTarget.context.id != SG_INVALID_ID) { + sgl_destroy_context(ctx_.swapchainTarget.context); + ctx_.swapchainTarget.context.id = SG_INVALID_ID; + ctx_.swapchainTarget.cache.clear(); + } + delete st; + // The window's own exit event: per-window resource holders (e.g. the + // tcxImGui per-window manager) tear down here, while this window's + // CoreEvents is still alive. + events_.exit.notify(); + if (app_) { + app_->exit(); + app_->cleanup(); + internal::attachedApps.erase(app_.get()); + app_.reset(); + ctx_.rootNode = nullptr; + } +} + +void Window::setTitle(const std::string& title) { + title_ = title; + auto* st = static_cast(native_); + if (st) sapp_window_set_title(st->win, title.c_str()); +} + +int Window::getWidth() const { + auto* st = static_cast(native_); + return st ? sapp_window_width(st->win) : 0; +} + +int Window::getHeight() const { + auto* st = static_cast(native_); + return st ? sapp_window_height(st->win) : 0; +} + +std::shared_ptr createWindow(const WindowSettings& settings) { + if (headless::isActive()) { + logError("Window") << "createWindow(): not available in headless mode"; + return nullptr; + } + auto win = std::shared_ptr(new Window()); + win->title_ = settings.title; + win->ctx_.swapchainColorFormat = SG_PIXELFORMAT_BGRA8; + win->ctx_.swapchainSampleCount = settings.sampleCount > 0 ? settings.sampleCount : 1; + auto* st = new AdapterState(); + + sapp_window_desc d = {}; + d.x = -1; + d.y = -1; + d.width = settings.width; + d.height = settings.height; + d.title = settings.title.c_str(); + d.borderless = !settings.decorated; + d.no_high_dpi = !settings.highDpi; + d.sample_count = settings.sampleCount; + // Share the device sokol_gfx renders with (sokol_app created it) + d.mtl_device = sglue_environment().metal.device; + d.tick_cb = &windowTick; + d.event_cb = &windowEvent; + d.close_cb = &windowClosed; + d.user_data = win.get(); + st->win = sapp_create_window(&d); + if (st->win.id == 0) { + delete st; + logError("Window") << "createWindow(): native window creation failed"; + return nullptr; + } + + win->native_ = st; + win->ctx_.acquireSwapchain = &acquireSecondarySwapchain; + win->ctx_.acquireSwapchainUser = st; + return win; +} + +} // namespace trussc + +#endif // __APPLE__ diff --git a/core/platform/web/sokol_impl.cpp b/core/platform/web/sokol_impl.cpp index e3b956db..169a0088 100644 --- a/core/platform/web/sokol_impl.cpp +++ b/core/platform/web/sokol_impl.cpp @@ -1,10 +1,11 @@ // ============================================================================= -// sokol バックエンド実装 (Emscripten / WebGL2) +// sokol バックエンド実装 (Emscripten / WebGPU or WebGL2) // ============================================================================= -#define SOKOL_IMPL #define SOKOL_NO_ENTRY // main() を自分で定義するため +// Suppress warnings from third-party sokol headers. They have intentional +// dead-code paths and unused helpers that trip -Wunused-* under -Wall/-Wextra. #if defined(__GNUC__) || defined(__clang__) # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wunused-variable" @@ -12,8 +13,10 @@ # pragma GCC diagnostic ignored "-Wmissing-field-initializers" #endif +#define SOKOL_IMPL #include "sokol_log.h" -#include "sokol_app.h" +#define SOKOL_APP_TC_IMPL +#include "sokol_app_tc.h" #include "sokol_gfx.h" #include "sokol_glue.h" #include "util/sokol_gl_tc.h" diff --git a/core/platform/web/tcFileDialog_web.cpp b/core/platform/web/tcFileDialog_web.cpp index c107eec9..c08b7e20 100644 --- a/core/platform/web/tcFileDialog_web.cpp +++ b/core/platform/web/tcFileDialog_web.cpp @@ -56,7 +56,7 @@ void confirmDialogAsync(const std::string& title, // ----------------------------------------------------------------------------- FileDialogResult loadDialog(const std::string& title, const std::string& message, - const std::string& defaultPath, + const fs::path& defaultPath, bool folderSelection) { (void)title; (void)message; (void)defaultPath; (void)folderSelection; logWarning("tcFileDialog") << "loadDialog is not supported on Web/WASM"; @@ -65,7 +65,7 @@ FileDialogResult loadDialog(const std::string& title, void loadDialogAsync(const std::string& title, const std::string& message, - const std::string& defaultPath, + const fs::path& defaultPath, bool folderSelection, std::function callback) { FileDialogResult result = loadDialog(title, message, defaultPath, folderSelection); @@ -77,8 +77,8 @@ void loadDialogAsync(const std::string& title, // ----------------------------------------------------------------------------- FileDialogResult saveDialog(const std::string& title, const std::string& message, - const std::string& defaultPath, - const std::string& defaultName) { + const fs::path& defaultPath, + const fs::path& defaultName) { (void)title; (void)message; (void)defaultPath; (void)defaultName; logWarning("tcFileDialog") << "saveDialog is not supported on Web/WASM"; return FileDialogResult(); @@ -86,8 +86,8 @@ FileDialogResult saveDialog(const std::string& title, void saveDialogAsync(const std::string& title, const std::string& message, - const std::string& defaultPath, - const std::string& defaultName, + const fs::path& defaultPath, + const fs::path& defaultName, std::function callback) { FileDialogResult result = saveDialog(title, message, defaultPath, defaultName); if (callback) callback(result); diff --git a/core/platform/web/tcPlatform_web.cpp b/core/platform/web/tcPlatform_web.cpp index a447934c..e70f8390 100644 --- a/core/platform/web/tcPlatform_web.cpp +++ b/core/platform/web/tcPlatform_web.cpp @@ -47,12 +47,12 @@ void setWindowSizeLogical(int width, int height) { emscripten_set_canvas_element_size("canvas", width, height); } -std::string getExecutablePath() { - return "/"; +fs::path getExecutablePath() { + return fs::path("/"); } -std::string getExecutableDir() { - return "/"; +fs::path getExecutableDir() { + return fs::path("/"); } bool captureWindow(Pixels& outPixels) { diff --git a/core/platform/web/tcSound_web.cpp b/core/platform/web/tcSound_web.cpp index ee51aa6c..52a3f4f9 100644 --- a/core/platform/web/tcSound_web.cpp +++ b/core/platform/web/tcSound_web.cpp @@ -119,11 +119,11 @@ EM_JS(void, copyAacData, (float* outPtr, int totalSamples), { // ----------------------------------------------------------------------------- // SoundBuffer::loadAac - Web implementation (deferred loading) // ----------------------------------------------------------------------------- -bool SoundBuffer::loadAac(const std::string& path) { +LoadResult SoundBuffer::loadAac(const fs::path& path) { printf("SoundBuffer: deferring AAC load: %s [Web]\n", path.c_str()); - // Save path for deferred loading - deferredAacPath_ = path; + // Save path for deferred loading (web paths are plain UTF-8 strings) + deferredAacPath_ = path.string(); // Set placeholder values so the app doesn't crash channels = 2; @@ -131,7 +131,7 @@ bool SoundBuffer::loadAac(const std::string& path) { numSamples = 44100; // 1 second placeholder samples.resize(numSamples * channels, 0.0f); - return true; // Will actually load when play() is called + return LoadResult::success(); // Will actually load when play() is called } // ----------------------------------------------------------------------------- @@ -221,15 +221,16 @@ void SoundBuffer::ensureAacLoaded() { // ----------------------------------------------------------------------------- // SoundBuffer::loadAacFromMemory - Web implementation // ----------------------------------------------------------------------------- -bool SoundBuffer::loadAacFromMemory(const void* data, size_t dataSize) { +LoadResult SoundBuffer::loadAacFromMemory(const void* data, size_t dataSize) { if (!data || dataSize == 0) { logWarning("SoundBuffer") << "loadAacFromMemory() called with empty data"; - return false; + return LoadResult::fail(LoadError::DecodeFailed, "empty memory range"); } // TODO: Implement memory-based AAC decode for Web logWarning("SoundBuffer") << "loadAacFromMemory() not yet implemented for Web (use file path instead)"; - return false; + return LoadResult::fail(LoadError::UnsupportedFormat, + "AAC decode from memory not yet implemented for Web"); } } // namespace trussc diff --git a/core/platform/web/tcSystemFont_web.cpp b/core/platform/web/tcSystemFont_web.cpp index e793b70f..10724cd2 100644 --- a/core/platform/web/tcSystemFont_web.cpp +++ b/core/platform/web/tcSystemFont_web.cpp @@ -11,7 +11,7 @@ namespace trussc { -std::string systemFontPath(const std::string& /*name*/) { +fs::path systemFontPath(const std::string& /*name*/) { return ""; } diff --git a/core/platform/web/tcVideoPlayer_web.cpp b/core/platform/web/tcVideoPlayer_web.cpp index 96976f0e..61ba690f 100644 --- a/core/platform/web/tcVideoPlayer_web.cpp +++ b/core/platform/web/tcVideoPlayer_web.cpp @@ -16,7 +16,8 @@ namespace trussc { // VideoPlayer Web implementation // --------------------------------------------------------------------------- -bool VideoPlayer::loadPlatform(const std::string& path) { +bool VideoPlayer::loadPlatform(const fs::path& path) { + std::string pathStr = internal::pathToUtf8(path); // Create video element in JavaScript char script[8192]; snprintf(script, sizeof(script), R"JS( @@ -101,7 +102,7 @@ bool VideoPlayer::loadPlatform(const std::string& path) { return 1; })(); - )JS", path.c_str()); + )JS", pathStr.c_str()); int result = emscripten_run_script_int(script); if (result <= 0) { @@ -117,7 +118,7 @@ bool VideoPlayer::loadPlatform(const std::string& path) { pixels_ = new unsigned char[width_ * height_ * 4]; std::memset(pixels_, 0, width_ * height_ * 4); - printf("VideoPlayer: loading '%s' [Web]\n", path.c_str()); + printf("VideoPlayer: loading '%s' [Web]\n", pathStr.c_str()); return true; } @@ -401,11 +402,11 @@ std::string VideoPlayer::getHwAccelNamePlatform() const { // element preloads its first frame and update() uploads it before play(). // ============================================================================= -bool VideoPlayer::extractFramePlatform(const std::string&, Pixels&, float, float*) { +bool VideoPlayer::extractFramePlatform(const fs::path&, Pixels&, float, float*) { return false; } -bool VideoPlayer::extractKeyFramePlatform(const std::string&, Pixels&, float, float*) { +bool VideoPlayer::extractKeyFramePlatform(const fs::path&, Pixels&, float, float*) { return false; } diff --git a/core/platform/win/sokol_impl.cpp b/core/platform/win/sokol_impl.cpp index 156c1a73..c59eb1cf 100644 --- a/core/platform/win/sokol_impl.cpp +++ b/core/platform/win/sokol_impl.cpp @@ -1,9 +1,10 @@ // ============================================================================= -// sokol バックエンド実装 (Windows / Linux) +// sokol backend implementation (Windows) +// sokol_app_tc.h is the self-contained sapp_* implementation (main window, +// run loop, events, multi-window API). // ============================================================================= -#define SOKOL_IMPL -#define SOKOL_NO_ENTRY // main() を自分で定義するため +#define SOKOL_NO_ENTRY // main() is defined by the app #if defined(__GNUC__) || defined(__clang__) # pragma GCC diagnostic push @@ -12,8 +13,10 @@ # pragma GCC diagnostic ignored "-Wmissing-field-initializers" #endif +#define SOKOL_IMPL #include "sokol_log.h" -#include "sokol_app.h" +#define SOKOL_APP_TC_IMPL +#include "sokol_app_tc.h" #include "sokol_gfx.h" #include "sokol_glue.h" #include "util/sokol_gl_tc.h" diff --git a/core/platform/win/tcFileDialog_win.cpp b/core/platform/win/tcFileDialog_win.cpp index b8b187cd..55415061 100644 --- a/core/platform/win/tcFileDialog_win.cpp +++ b/core/platform/win/tcFileDialog_win.cpp @@ -34,13 +34,6 @@ std::string toUtf8(const wchar_t* wstr) { return result; } -// Extract filename from path -std::string extractFileName(const std::string& path) { - size_t pos = path.find_last_of("/\\"); - if (pos == std::string::npos) return path; - return path.substr(pos + 1); -} - } // anonymous namespace namespace trussc { @@ -84,7 +77,7 @@ void confirmDialogAsync(const std::string& title, // ----------------------------------------------------------------------------- FileDialogResult loadDialog(const std::string& title, const std::string& message, - const std::string& defaultPath, + const fs::path& defaultPath, bool folderSelection) { FileDialogResult result; (void)message; // Windows file dialog doesn't support message @@ -112,14 +105,14 @@ FileDialogResult loadDialog(const std::string& title, std::wstring dirW; if (!defaultPath.empty()) { - dirW = toWide(defaultPath); + dirW = defaultPath.wstring(); ofn.lpstrInitialDir = dirW.c_str(); } if (GetOpenFileNameW(&ofn)) { result.success = true; - result.filePath = toUtf8(szFileName); - result.fileName = extractFileName(result.filePath); + result.filePath = fs::path(szFileName); + result.fileName = result.filePath.filename(); } } else { // フォルダ選択ダイアログ(IFileDialog / モダンUI) @@ -139,7 +132,7 @@ FileDialogResult loadDialog(const std::string& title, // デフォルトパス設定 if (!defaultPath.empty()) { - std::wstring defaultPathW = toWide(defaultPath); + std::wstring defaultPathW = defaultPath.wstring(); IShellItem* pDefaultFolder = nullptr; if (SUCCEEDED(SHCreateItemFromParsingName(defaultPathW.c_str(), nullptr, IID_IShellItem, (void**)&pDefaultFolder))) { @@ -155,8 +148,8 @@ FileDialogResult loadDialog(const std::string& title, PWSTR pszPath = nullptr; if (SUCCEEDED(pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszPath))) { result.success = true; - result.filePath = toUtf8(pszPath); - result.fileName = extractFileName(result.filePath); + result.filePath = fs::path(pszPath); + result.fileName = result.filePath.filename(); CoTaskMemFree(pszPath); } pItem->Release(); @@ -172,7 +165,7 @@ FileDialogResult loadDialog(const std::string& title, void loadDialogAsync(const std::string& title, const std::string& message, - const std::string& defaultPath, + const fs::path& defaultPath, bool folderSelection, std::function callback) { FileDialogResult result = loadDialog(title, message, defaultPath, folderSelection); @@ -184,8 +177,8 @@ void loadDialogAsync(const std::string& title, // ----------------------------------------------------------------------------- FileDialogResult saveDialog(const std::string& title, const std::string& message, - const std::string& defaultPath, - const std::string& defaultName) { + const fs::path& defaultPath, + const fs::path& defaultName) { FileDialogResult result; (void)message; // Windows file dialog doesn't support message @@ -195,7 +188,7 @@ FileDialogResult saveDialog(const std::string& title, wchar_t szFileName[MAX_PATH] = L""; if (!defaultName.empty()) { - std::wstring nameW = toWide(defaultName); + std::wstring nameW = defaultName.wstring(); wcsncpy_s(szFileName, MAX_PATH, nameW.c_str(), _TRUNCATE); } @@ -217,14 +210,14 @@ FileDialogResult saveDialog(const std::string& title, std::wstring dirW; if (!defaultPath.empty()) { - dirW = toWide(defaultPath); + dirW = defaultPath.wstring(); ofn.lpstrInitialDir = dirW.c_str(); } if (GetSaveFileNameW(&ofn)) { result.success = true; - result.filePath = toUtf8(szFileName); - result.fileName = extractFileName(result.filePath); + result.filePath = fs::path(szFileName); + result.fileName = result.filePath.filename(); } return result; @@ -232,8 +225,8 @@ FileDialogResult saveDialog(const std::string& title, void saveDialogAsync(const std::string& title, const std::string& message, - const std::string& defaultPath, - const std::string& defaultName, + const fs::path& defaultPath, + const fs::path& defaultName, std::function callback) { FileDialogResult result = saveDialog(title, message, defaultPath, defaultName); if (callback) callback(result); diff --git a/core/platform/win/tcPlatform_win.cpp b/core/platform/win/tcPlatform_win.cpp index ce91c181..cdec7b03 100644 --- a/core/platform/win/tcPlatform_win.cpp +++ b/core/platform/win/tcPlatform_win.cpp @@ -18,7 +18,7 @@ #include // sokol_app のウィンドウ取得 -#include "sokol_app.h" +#include "sokol_app_tc.h" // stb_image_write(スクリーンショット保存用) #include "stb/stb_image_write.h" @@ -178,34 +178,21 @@ void setWindowSizeLogical(int width, int height) { // --------------------------------------------------------------------------- // getExecutablePath - 実行ファイルの絶対パスを取得 // --------------------------------------------------------------------------- -std::string getExecutablePath() { +fs::path getExecutablePath() { + // Native UTF-16 straight into fs::path — no UTF-8 round trip, so + // non-ASCII install paths survive intact. wchar_t path[MAX_PATH] = { 0 }; GetModuleFileNameW(nullptr, path, MAX_PATH); - - // UTF-16 から UTF-8 に変換 - int size = WideCharToMultiByte(CP_UTF8, 0, path, -1, nullptr, 0, nullptr, nullptr); - if (size <= 0) return ""; - - std::string result(size - 1, '\0'); - WideCharToMultiByte(CP_UTF8, 0, path, -1, result.data(), size, nullptr, nullptr); - - return result; + return fs::path(path); } // --------------------------------------------------------------------------- // getExecutableDir - 実行ファイルがあるディレクトリを取得 // --------------------------------------------------------------------------- -std::string getExecutableDir() { - std::string path = getExecutablePath(); - if (path.empty()) return ""; - - // 最後の \ または / を探す - size_t pos = path.find_last_of("\\/"); - if (pos != std::string::npos) { - return path.substr(0, pos + 1); - } - - return path; +fs::path getExecutableDir() { + fs::path path = getExecutablePath(); + if (path.empty()) return {}; + return path.parent_path(); } // --------------------------------------------------------------------------- @@ -364,7 +351,7 @@ bool captureWindow(Pixels& outPixels) { // --------------------------------------------------------------------------- bool internal::captureWindowToFile(const std::filesystem::path& path) { if (path.is_relative()) { - return internal::captureWindowToFile(getDataPath(path.string())); + return internal::captureWindowToFile(getDataPath(path)); } // Capture to Pixels Pixels pixels; @@ -385,7 +372,7 @@ bool internal::captureWindowToFile(const std::filesystem::path& path) { } int result = 0; - std::string pathStr = path.string(); + std::string pathStr = internal::pathToUtf8(path); // UTF-8 for stb (STBIW_WINDOWS_UTF8) if (ext == ".png") { result = stbi_write_png(pathStr.c_str(), width, height, 4, data, width * 4); diff --git a/core/platform/win/tcSound_win.cpp b/core/platform/win/tcSound_win.cpp index 8d9efc67..afa78e81 100644 --- a/core/platform/win/tcSound_win.cpp +++ b/core/platform/win/tcSound_win.cpp @@ -132,20 +132,28 @@ namespace { } } -bool SoundBuffer::loadAac(const std::string& path) { +LoadResult SoundBuffer::loadAac(const fs::path& path) { EnsureMFStartup(); - // Convert path to wide string - int size_needed = MultiByteToWideChar(CP_UTF8, 0, path.c_str(), (int)path.size(), NULL, 0); - std::wstring wpath(size_needed, 0); - MultiByteToWideChar(CP_UTF8, 0, path.c_str(), (int)path.size(), &wpath[0], size_needed); + std::error_code ec; + if (!fs::exists(path, ec)) { + return LoadResult::fail(LoadError::FileNotFound, + "file not found: " + internal::pathToUtf8(path)); + } + + // fs::path is already wide on Windows + std::wstring wpath = path.wstring(); IMFSourceReader* pReader = NULL; HRESULT hr = MFCreateSourceReaderFromURL(wpath.c_str(), NULL, &pReader); if (FAILED(hr)) { printf("SoundBuffer: loadAac failed to open %s (0x%08X)\n", path.c_str(), hr); - return false; + char hrBuf[16]; + snprintf(hrBuf, sizeof(hrBuf), "0x%08X", (unsigned)hr); + return LoadResult::fail(LoadError::DecodeFailed, + "failed to open " + internal::pathToUtf8(path) + + " (hr=" + hrBuf + ")"); } bool result = ReadFromSourceReader(pReader, this); @@ -158,16 +166,18 @@ bool SoundBuffer::loadAac(const std::string& path) { printf("SoundBuffer: failed to decode AAC %s\n", path.c_str()); } - return result; + return result ? LoadResult::success() + : LoadResult::fail(LoadError::DecodeFailed, + "failed to decode AAC: " + internal::pathToUtf8(path)); } -bool SoundBuffer::loadAacFromMemory(const void* data, size_t dataSize) { +LoadResult SoundBuffer::loadAacFromMemory(const void* data, size_t dataSize) { EnsureMFStartup(); IStream* pStream = SHCreateMemStream((const BYTE*)data, (UINT)dataSize); if (!pStream) { printf("SoundBuffer: loadAacFromMemory failed to create stream\n"); - return false; + return LoadResult::fail(LoadError::DecodeFailed, "failed to create memory stream"); } IMFByteStream* pByteStream = NULL; @@ -176,7 +186,7 @@ bool SoundBuffer::loadAacFromMemory(const void* data, size_t dataSize) { if (FAILED(hr)) { printf("SoundBuffer: loadAacFromMemory failed to create MF byte stream\n"); - return false; + return LoadResult::fail(LoadError::DecodeFailed, "failed to create MF byte stream"); } IMFSourceReader* pReader = NULL; @@ -185,7 +195,7 @@ bool SoundBuffer::loadAacFromMemory(const void* data, size_t dataSize) { if (FAILED(hr)) { printf("SoundBuffer: loadAacFromMemory failed to create SourceReader\n"); - return false; + return LoadResult::fail(LoadError::DecodeFailed, "failed to create SourceReader"); } bool result = ReadFromSourceReader(pReader, this); @@ -198,7 +208,8 @@ bool SoundBuffer::loadAacFromMemory(const void* data, size_t dataSize) { printf("SoundBuffer: failed to decode AAC from memory\n"); } - return result; + return result ? LoadResult::success() + : LoadResult::fail(LoadError::DecodeFailed, "failed to decode AAC from memory"); } } // namespace trussc diff --git a/core/platform/win/tcSystemFont_win.cpp b/core/platform/win/tcSystemFont_win.cpp index ba982b50..cbaa01cf 100644 --- a/core/platform/win/tcSystemFont_win.cpp +++ b/core/platform/win/tcSystemFont_win.cpp @@ -79,7 +79,7 @@ std::string fontFilePath(IDWriteFont* font) { } // namespace -std::string systemFontPath(const std::string& name) { +fs::path systemFontPath(const std::string& name) { if (name.empty()) return ""; auto factory = getFactory(); if (!factory) return ""; diff --git a/core/platform/win/tcVideoPlayer_win.cpp b/core/platform/win/tcVideoPlayer_win.cpp index 2f941f1b..e4534b8e 100644 --- a/core/platform/win/tcVideoPlayer_win.cpp +++ b/core/platform/win/tcVideoPlayer_win.cpp @@ -845,10 +845,11 @@ void TCVideoPlayerImpl::onMediaEvent(DWORD event, DWORD_PTR param1, DWORD param2 namespace trussc { -bool VideoPlayer::loadPlatform(const std::string& path) { +bool VideoPlayer::loadPlatform(const fs::path& path) { auto impl = new TCVideoPlayerImpl(); - if (!impl->load(path, this)) { + // Impl-side strings are UTF-8 (converted to wide via CP_UTF8 internally) + if (!impl->load(internal::pathToUtf8(path), this)) { delete impl; return false; } @@ -1200,18 +1201,19 @@ static bool tcv_extract_frame_win(const std::string& path, Pixels& outPixels, return ok; } -bool VideoPlayer::extractFramePlatform(const std::string& path, Pixels& outPixels, +bool VideoPlayer::extractFramePlatform(const fs::path& path, Pixels& outPixels, float timeSec, float* outDuration) { - return tcv_extract_frame_win(path, outPixels, timeSec, outDuration, /*exact=*/true); + return tcv_extract_frame_win(internal::pathToUtf8(path), outPixels, timeSec, outDuration, /*exact=*/true); } -bool VideoPlayer::extractKeyFramePlatform(const std::string& path, Pixels& outPixels, +bool VideoPlayer::extractKeyFramePlatform(const fs::path& path, Pixels& outPixels, float timeSec, float* outDuration) { - if (tcv_extract_frame_win(path, outPixels, timeSec, outDuration, /*exact=*/false)) { + std::string p = internal::pathToUtf8(path); + if (tcv_extract_frame_win(p, outPixels, timeSec, outDuration, /*exact=*/false)) { return true; } // No keyframe reachable — fall back to an exact decode. - return tcv_extract_frame_win(path, outPixels, timeSec, outDuration, /*exact=*/true); + return tcv_extract_frame_win(p, outPixels, timeSec, outDuration, /*exact=*/true); } } // namespace trussc diff --git a/core/platform/win/tcWindowWin.cpp b/core/platform/win/tcWindowWin.cpp new file mode 100644 index 00000000..83156e66 --- /dev/null +++ b/core/platform/win/tcWindowWin.cpp @@ -0,0 +1,335 @@ +// ============================================================================= +// tcWindowWin.cpp - TrussC adapter for secondary windows (Windows) +// ============================================================================= +// The native windowing layer (HWND + D3D11 flip swapchain + per-window frame +// latency waitable ticks, occlusion-tested presents, sapp_event conversion +// with the full sokol_app scancode table) lives in sokol/sokol_app_tc.h and +// knows nothing about TrussC. This file maps its C API onto TrussC: +// tick_cb -> WindowContext switch + per-window dt + update/draw the tree +// event_cb -> CoreEvents + App hooks + Node-tree dispatch (same keycode +// semantics as the main window: key == SAPP_KEYCODE_*) +// close_cb -> Window::close() +// All rendering shares the one sokol_gfx context; each window gets its own +// sokol_gl context (same pattern as Fbo). Mirror of platform/mac/tcWindowMac.mm. + +#include "TrussC.h" + +#if defined(_WIN32) +#include "sokol_app_tc.h" // declarations only (impl lives in sokol_impl.cpp) + +using namespace trussc; + +namespace { + +struct AdapterState { + sapp_window win{0}; +}; + +// Swapchain provider handed to WindowContext::acquireSwapchain. Unlike Metal +// there is no per-frame drawable: the flip-model backbuffer views persist and +// are simply handed back (they are only valid during this window's tick). +sg_swapchain acquireSecondarySwapchain(void* user) { + auto* st = static_cast(user); + sg_swapchain sc = {}; + if (!st) return sc; + const void* renderView = sapp_window_d3d11_render_view(st->win); + if (!renderView) return sc; + sc.width = sapp_window_framebuffer_width(st->win); + sc.height = sapp_window_framebuffer_height(st->win); + sc.sample_count = sapp_window_sample_count(st->win); + sc.color_format = SG_PIXELFORMAT_BGRA8; + sc.depth_format = SG_PIXELFORMAT_DEPTH_STENCIL; + sc.d3d11.render_view = renderView; + sc.d3d11.resolve_view = sapp_window_d3d11_resolve_view(st->win); + sc.d3d11.depth_stencil_view = sapp_window_d3d11_depth_stencil_view(st->win); + return sc; +} + +// --- tick: drive this window's update/draw with its context active --------- +void windowTick(sapp_window swin, void* user) { + Window* win = static_cast(user); + if (!win) return; + auto& ctx = win->context(); + + const int fbw = sapp_window_framebuffer_width(swin); + const int fbh = sapp_window_framebuffer_height(swin); + if (fbw <= 0 || fbh <= 0) return; + ctx.fbWidth = fbw; + ctx.fbHeight = fbh; + ctx.dpiScale = sapp_window_dpi_scale(swin); + // Keep a RectNode root in sync with the window's logical size (same + // contract as the main App on SAPP_EVENTTYPE_RESIZED). + win->syncRootSize((float)sapp_window_width(swin), (float)sapp_window_height(swin)); + + // Per-window delta time: wall-clock between THIS window's processed + // ticks, so getDeltaTime() inside this window's update/draw is correct + // even when its display runs at a different refresh rate than the main + // window's. A long gap (occlusion) yields one large delta, exactly like + // an event-driven main window. + { + auto callNow = std::chrono::high_resolution_clock::now(); + if (!ctx.lastUpdateCallTimeInitialized) { + ctx.lastUpdateCallTimeInitialized = true; + ctx.updateDeltaTime = sapp_window_frame_duration(swin); + } else { + ctx.updateDeltaTime = std::chrono::duration(callNow - ctx.lastUpdateCallTime).count(); + } + ctx.lastUpdateCallTime = callNow; + } + + auto* prev = internal::currentWindowCtx; + internal::currentWindowCtx = &ctx; + + // Own sokol_gl context, created lazily on the first tick (sgl is set up + // by then). Same lifecycle caveats as Fbo contexts on sgl resize. + if (ctx.swapchainTarget.context.id == SG_INVALID_ID) { + sgl_context_desc_t cdesc = {}; + cdesc.max_vertices = 65536; + cdesc.max_commands = 16384; + cdesc.color_format = SG_PIXELFORMAT_BGRA8; + cdesc.depth_format = SG_PIXELFORMAT_DEPTH_STENCIL; + cdesc.sample_count = sapp_window_sample_count(swin); + ctx.swapchainTarget.context = sgl_make_context(&cdesc); + } + sgl_set_context(ctx.swapchainTarget.context); + sgl_defaults(); + + beginFrame(); + + win->events().update.notify(); + win->tickTree(); + + const Color& cc = win->clearColor_; + clear(cc.r, cc.g, cc.b, cc.a); + win->events().draw.notify(); + win->drawTreeNow(); + + present(); + win->events().afterFrame.notify(); + + sgl_set_context(sgl_default_context()); + internal::currentWindowCtx = prev; +} + +// --- events: map sapp_event onto CoreEvents / App hooks / tree dispatch ---- +void windowEvent(const sapp_event* ev, sapp_window swin, void* user) { + Window* win = static_cast(user); + if (!win) return; + auto& ctx = win->context(); + auto* prev = internal::currentWindowCtx; + internal::currentWindowCtx = &ctx; + + // Raw event pass-through (same hook addons use on the main window) + win->events().rawEvent.notify(*ev); + + // sapp coords arrive in framebuffer pixels; TrussC windows use logical + // points (secondary windows have no pixelPerfect mode for now) + const float dpi = sapp_window_dpi_scale(swin); + const float scale = (dpi > 0.0f) ? (1.0f / dpi) : 1.0f; + const bool hasShift = (ev->modifiers & SAPP_MODIFIER_SHIFT) != 0; + const bool hasCtrl = (ev->modifiers & SAPP_MODIFIER_CTRL) != 0; + const bool hasAlt = (ev->modifiers & SAPP_MODIFIER_ALT) != 0; + const bool hasSuper = (ev->modifiers & SAPP_MODIFIER_SUPER) != 0; + + switch (ev->type) { + case SAPP_EVENTTYPE_MOUSE_DOWN: { + const float x = ev->mouse_x * scale, y = ev->mouse_y * scale; + ctx.mouseX = x; ctx.mouseY = y; + ctx.mouseButton = (int)ev->mouse_button; + ctx.mousePressed = true; + MouseEventArgs e; + e.pos = e.globalPos = Vec2(x, y); + e.button = (int)ev->mouse_button; + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + e.syncLegacy(); + win->events().mousePressed.notify(e); + if (win->getApp()) win->getApp()->mousePressed(e); + win->dispatchMousePressToTree(e); + break; + } + case SAPP_EVENTTYPE_MOUSE_UP: { + const float x = ev->mouse_x * scale, y = ev->mouse_y * scale; + ctx.mouseX = x; ctx.mouseY = y; + ctx.mouseButton = -1; + ctx.mousePressed = false; + MouseEventArgs e; + e.pos = e.globalPos = Vec2(x, y); + e.button = (int)ev->mouse_button; + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + e.syncLegacy(); + win->events().mouseReleased.notify(e); + if (win->getApp()) win->getApp()->mouseReleased(e); + win->dispatchMouseReleaseToTree(e); + break; + } + case SAPP_EVENTTYPE_MOUSE_ENTER: + case SAPP_EVENTTYPE_MOUSE_MOVE: { + const float x = ev->mouse_x * scale, y = ev->mouse_y * scale; + ctx.pmouseX = ctx.mouseX; ctx.pmouseY = ctx.mouseY; + ctx.mouseX = x; ctx.mouseY = y; + internal::MouseEventRaw raw; + raw.pos = raw.globalPos = Vec2(x, y); + raw.delta = raw.globalDelta = Vec2(ev->mouse_dx * scale, ev->mouse_dy * scale); + raw.shift = hasShift; raw.ctrl = hasCtrl; raw.alt = hasAlt; raw.super = hasSuper; + if (ctx.mousePressed && ctx.mouseButton >= 0) { + raw.button = ctx.mouseButton; + MouseDragEventArgs e = internal::toDragArgs(raw); + win->events().mouseDragged.notify(e); + if (win->getApp()) win->getApp()->mouseDragged(e); + } else { + MouseMoveEventArgs e = internal::toMoveArgs(raw); + win->events().mouseMoved.notify(e); + if (win->getApp()) win->getApp()->mouseMoved(e); + } + break; + } + case SAPP_EVENTTYPE_MOUSE_LEAVE: { + // Park the cursor offscreen so the next tick's updateHoverState + // clears this window's hover (hoveredNode lives in ITS context). + ctx.pmouseX = ctx.mouseX; ctx.pmouseY = ctx.mouseY; + ctx.mouseX = -1.0f; ctx.mouseY = -1.0f; + break; + } + case SAPP_EVENTTYPE_MOUSE_SCROLL: { + ScrollEventArgs e; + e.pos = e.globalPos = Vec2(ctx.mouseX, ctx.mouseY); + e.scroll = Vec2(ev->scroll_x, ev->scroll_y); + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + e.syncLegacy(); + win->events().mouseScrolled.notify(e); + if (win->getApp()) win->getApp()->mouseScrolled(e); + break; + } + case SAPP_EVENTTYPE_KEY_DOWN: { + KeyEventArgs e; + e.key = ev->key_code; // SAPP_KEYCODE_*, identical to the main window + e.isRepeat = ev->key_repeat; + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + if (!ev->key_repeat) ctx.keysPressed.insert(e.key); + win->events().keyPressed.notify(e); + if (win->getApp()) win->getApp()->keyPressed(e); + break; + } + case SAPP_EVENTTYPE_KEY_UP: { + KeyEventArgs e; + e.key = ev->key_code; + e.isRepeat = false; + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + ctx.keysPressed.erase(e.key); + win->events().keyReleased.notify(e); + if (win->getApp()) win->getApp()->keyReleased(e); + break; + } + case SAPP_EVENTTYPE_SUSPENDED: + logNotice("Window") << "second window occluded - skipping frames (no stall)"; + break; + case SAPP_EVENTTYPE_RESUMED: + logNotice("Window") << "second window visible again - resuming ticks"; + break; + default: + // CHAR / RESIZED / FOCUSED / UNFOCUSED: rawEvent carries them; + // size sync happens per tick. + break; + } + + internal::currentWindowCtx = prev; +} + +void windowClosed(sapp_window swin, void* user) { + (void)swin; + Window* win = static_cast(user); + if (win) win->close(); // tears down native + app state +} + +} // namespace + +// --------------------------------------------------------------------------- +// Window methods (Windows implementations) +// --------------------------------------------------------------------------- +namespace trussc { + +Window::~Window() { close(); } + +void Window::close() { + auto* st = static_cast(native_); + if (!st) return; + native_ = nullptr; // re-entrancy guard (close_cb) + ctx_.acquireSwapchain = nullptr; + ctx_.acquireSwapchainUser = nullptr; + sapp_destroy_window(st->win); + if (ctx_.swapchainTarget.context.id != SG_INVALID_ID) { + sgl_destroy_context(ctx_.swapchainTarget.context); + ctx_.swapchainTarget.context.id = SG_INVALID_ID; + ctx_.swapchainTarget.cache.clear(); + } + delete st; + // The window's own exit event: per-window resource holders (e.g. the + // tcxImGui per-window manager) tear down here, while this window's + // CoreEvents is still alive. + events_.exit.notify(); + if (app_) { + app_->exit(); + app_->cleanup(); + internal::attachedApps.erase(app_.get()); + app_.reset(); + ctx_.rootNode = nullptr; + } +} + +void Window::setTitle(const std::string& title) { + title_ = title; + auto* st = static_cast(native_); + if (st) sapp_window_set_title(st->win, title.c_str()); +} + +int Window::getWidth() const { + auto* st = static_cast(native_); + return st ? sapp_window_width(st->win) : 0; +} + +int Window::getHeight() const { + auto* st = static_cast(native_); + return st ? sapp_window_height(st->win) : 0; +} + +std::shared_ptr createWindow(const WindowSettings& settings) { + if (headless::isActive()) { + logError("Window") << "createWindow(): not available in headless mode"; + return nullptr; + } + auto win = std::shared_ptr(new Window()); + win->title_ = settings.title; + win->ctx_.swapchainColorFormat = SG_PIXELFORMAT_BGRA8; + win->ctx_.swapchainSampleCount = settings.sampleCount > 0 ? settings.sampleCount : 1; + auto* st = new AdapterState(); + + sapp_window_desc d = {}; + d.x = -1; + d.y = -1; + d.width = settings.width; + d.height = settings.height; + d.title = settings.title.c_str(); + d.borderless = !settings.decorated; + d.no_high_dpi = !settings.highDpi; + d.sample_count = settings.sampleCount; + // (the shared D3D11 device is owned by sokol_app_tc.h; nothing to pass) + d.tick_cb = &windowTick; + d.event_cb = &windowEvent; + d.close_cb = &windowClosed; + d.user_data = win.get(); + st->win = sapp_create_window(&d); + if (st->win.id == 0) { + delete st; + logError("Window") << "createWindow(): native window creation failed"; + return nullptr; + } + + win->native_ = st; + win->ctx_.acquireSwapchain = &acquireSecondarySwapchain; + win->ctx_.acquireSwapchainUser = st; + return win; +} + +} // namespace trussc + +#endif // _WIN32 diff --git a/core/tests/filePath/.gitignore b/core/tests/filePath/.gitignore new file mode 100644 index 00000000..f16d06dc --- /dev/null +++ b/core/tests/filePath/.gitignore @@ -0,0 +1,40 @@ +# ============================================================================= +# TrussC Project .gitignore +# ============================================================================= + +# Generated by projectGenerator (regenerate with projectGenerator update) +CMakeLists.txt +CMakePresets.json + +# TrussC local config (path override, generated by projectGenerator) +.trussc + +# Build directories +build/ +build-*/ +emscripten/ +xcode*/ +vs/ + +# Build scripts (generated, OS dependent) +build-web.* + +# Binary output (keep data folder) +bin/* +!bin/data/ + +# IDE specific +.vscode/ +.vs/ +.cache/ + +# Generated shader headers (rebuilt by CMake) +*.glsl.h + +# OS specific +.DS_Store +Thumbs.db + +# Secrets (don't commit these!) +.env +secrets.* diff --git a/core/tests/filePath/addons.make b/core/tests/filePath/addons.make new file mode 100644 index 00000000..dc9fc4ff --- /dev/null +++ b/core/tests/filePath/addons.make @@ -0,0 +1 @@ +# TrussC addons - one addon per line diff --git a/core/tests/filePath/src/main.cpp b/core/tests/filePath/src/main.cpp new file mode 100644 index 00000000..5b48a2b2 --- /dev/null +++ b/core/tests/filePath/src/main.cpp @@ -0,0 +1,193 @@ +// ============================================================================= +// core/tests/filePath — behavioral regression test for fs::path unification. +// +// Headless, console, exit code = pass/fail (build_all.py runs it in CI on +// macOS / Linux / Windows). +// +// Guards the invariant: file paths survive end-to-end as fs::path — through +// getDataPath composition, the tc file utilities, and the C-library +// boundaries (stb, miniaudio, nlohmann, pugixml) — including non-ASCII +// (Japanese) names, spaces/parentheses, and absolute-path passthrough. +// On Windows this exercises the UTF-8 → wide conversions; a regression to +// ACP-narrow file IO fails here with Japanese names. +// ============================================================================= + +#include + +#include +#include +#include +#include +#include + +using namespace std; +using namespace tc; + +static int g_fail = 0; +static void check(const char* name, bool ok) { + std::printf("%-64s %s\n", name, ok ? "PASS" : "FAIL"); + std::fflush(stdout); // flush per line so CI logs survive a later crash + if (!ok) ++g_fail; +} + +// Minimal 16-bit mono PCM WAV (440 Hz-ish square, ~0.1 s @ 44100) +static vector makeWavBytes() { + const uint32_t sampleRate = 44100; + const uint32_t numSamples = 4410; + const uint32_t dataSize = numSamples * 2; + vector b; + auto push32 = [&](uint32_t v) { for (int i = 0; i < 4; i++) b.push_back(uint8_t(v >> (8 * i))); }; + auto push16 = [&](uint16_t v) { for (int i = 0; i < 2; i++) b.push_back(uint8_t(v >> (8 * i))); }; + auto pushStr = [&](const char* s) { while (*s) b.push_back(uint8_t(*s++)); }; + pushStr("RIFF"); push32(36 + dataSize); pushStr("WAVE"); + pushStr("fmt "); push32(16); push16(1); push16(1); + push32(sampleRate); push32(sampleRate * 2); push16(2); push16(16); + pushStr("data"); push32(dataSize); + for (uint32_t i = 0; i < numSamples; i++) { + push16(uint16_t(((i / 50) % 2) ? 12000 : -12000)); + } + return b; +} + +int main() { + // Sandbox under the OS temp dir. On Windows this is an absolute path like + // C:\Users\...\Temp — using it as the data-path root exercises the + // fs::is_absolute() root detection (the old path[0]=='/' check failed it). + const fs::path sandbox = fs::temp_directory_path() / "tc_filePath_test"; + std::error_code ec; + fs::remove_all(sandbox, ec); + fs::create_directories(sandbox); + + // --- 1. data-path root + getDataPath composition --- + { + setDataPathRoot(sandbox); + check("setDataPathRoot(absolute): root round-trips", + fs::path(getDataPathRoot()) == sandbox || + fs::path(getDataPathRoot()) == sandbox / ""); + + fs::path p = getDataPath("sub/file.txt"); + check("getDataPath(relative): resolves under the absolute root", + p.is_absolute() && p == sandbox / "sub" / "file.txt"); + + const fs::path abs = sandbox / "already_absolute.bin"; + check("getDataPath(absolute): passthrough unchanged", + fs::path(getDataPath(abs)) == abs); + } + + // --- 2. text round-trip: ASCII / Japanese name / Japanese content --- + { + check("saveTextFile: ASCII name", saveTextFile("ascii.txt", "hello")); + check("loadTextFile: ASCII round-trip", loadTextFile("ascii.txt") == "hello"); + + const string jpName = "日本語ファイル名.txt"; + const string jpBody = "こんにちはTrussC。改行\nもある。"; + check("saveTextFile: Japanese name", saveTextFile(jpName, jpBody)); + check("loadTextFile: Japanese name + content round-trip", + loadTextFile(jpName) == jpBody); + check("fileExists: Japanese name", fileExists(jpName)); + check("getFileSize: Japanese name", getFileSize(jpName) == (int64_t)jpBody.size()); + } + + // --- 3. spaces / parentheses / Japanese directory (exhibition-PC case) --- + { + const string dir = "新しいフォルダー (2)"; + check("createDirectory: Japanese + space + parens", createDirectory(dir)); + const string f = dir + "/テスト ファイル.txt"; + check("saveTextFile: file inside that directory", saveTextFile(f, "data")); + check("loadTextFile: reads it back", loadTextFile(f) == "data"); + + auto listing = listDirectory(dir); + bool found = false; + for (auto& e : listing) { + if (fs::path(e).filename() == fs::path("テスト ファイル.txt")) found = true; + } + check("listDirectory: finds the Japanese-named file", found); + + check("removeFile: Japanese path", removeFile(f) && !fileExists(f)); + } + + // --- 4. binary round-trip through the stb boundary (PNG) --- + { + Pixels px; + px.allocate(4, 4, 4); + for (int y = 0; y < 4; y++) + for (int x = 0; x < 4; x++) + px.setColor(x, y, Color(x / 3.0f, y / 3.0f, 0.5f, 1.0f)); + + const string imgPath = "画像/テスト画像.png"; + createDirectory("画像"); + check("Pixels::save: PNG with Japanese path", px.save(imgPath)); + + Pixels loaded; + bool ok = loaded.load(getDataPath(imgPath)).ok(); + check("Pixels::load: PNG back from Japanese path", ok); + bool same = ok && loaded.getWidth() == 4 && loaded.getHeight() == 4; + if (same) { + Color a = loaded.getColor(3, 0); + same = a.r > 0.9f && a.g < 0.1f; // (1, 0, 0.5) corner survived + } + check("Pixels round-trip: pixel data intact", same); + } + + // --- 5. sound decode through the miniaudio boundary (WAV) --- + { + const auto wav = makeWavBytes(); + const fs::path wavPath = sandbox / "音" / "テスト音声.wav"; + fs::create_directories(wavPath.parent_path()); + { + std::ofstream out(wavPath, std::ios::binary); + out.write(reinterpret_cast(wav.data()), wav.size()); + } + Sound snd; + check("Sound::load: WAV with Japanese absolute path", snd.load(wavPath).ok()); + check("Sound::load: decoded duration > 0", snd.getDuration() > 0.05f); + } + + // --- 6. JSON / XML round-trip with Japanese paths and values --- + { + Json j; + j["名前"] = "トラス"; + j["value"] = 42; + createDirectory("設定"); // save helpers don't create parent dirs + check("saveJson: Japanese path", saveJson(j, "設定/データ.json")); + Json k = loadJson("設定/データ.json"); + check("loadJson: values round-trip", + k.value("value", 0) == 42 && k.value("名前", string()) == "トラス"); + + Xml xml; + XmlNode root = xml.addRoot("root"); + root.append_attribute("名") = "値"; + check("Xml::save: Japanese path", xml.save("設定/データ.xml")); + Xml xml2; + check("Xml::load: back from Japanese path", xml2.load("設定/データ.xml")); + check("Xml round-trip: root + Japanese attribute survive", + string(xml2.root().name()) == "root" && + string(xml2.root().attribute("名").value()) == "値"); + } + + // --- 7. FileWriter / FileReader line round-trip --- + { + const string txtPath = "書き込み/ログ.txt"; + createDirectory("書き込み"); + { + FileWriter w; + check("FileWriter::open: Japanese path", w.open(txtPath)); + w.writeLine("一行目"); + w.writeLine("second line"); + } + { + FileReader r; + bool ok = r.open(txtPath); // relative: resolved via getDataPath + check("FileReader::open: Japanese path", ok); + check("FileReader: Japanese content line survives", + ok && r.readLine() == "一行目" && r.readLine() == "second line"); + } + } + + fs::remove_all(sandbox, ec); + + std::printf("\n%s (%d failure%s)\n", g_fail ? "FAILED" : "PASSED", + g_fail, g_fail == 1 ? "" : "s"); + std::fflush(stdout); + return g_fail ? 1 : 0; +} diff --git a/docs/AI_AUTOMATION.md b/docs/AI_AUTOMATION.md index 70cb99ef..80dbac7c 100644 --- a/docs/AI_AUTOMATION.md +++ b/docs/AI_AUTOMATION.md @@ -25,9 +25,16 @@ TRUSSC_MCP=1 TRUSSC_MCP_PORT=8080 ./myApp When enabled: 1. An **HTTP server** starts on the specified port (or an OS-assigned port). -2. **Inspection tools** (`get_screenshot`, `save_screenshot`) are automatically registered. +2. **Inspection tools** (`tc_get_screenshot`, `tc_save_screenshot`) are automatically registered. 3. The server endpoint URL is printed to stderr: `[MCP] HTTP server listening on http://localhost:PORT/mcp` +### Related: `TRUSSC_LOG_FILE` + +Independent of MCP mode, setting `TRUSSC_LOG_FILE=/path/to/app.log` makes the app +call `setLogFile()` before `setup()` runs, so every log line — including +setup-time output — is appended to that file with zero app code. This is how a +supervisor process (e.g. `anchorbolt start`) captures logs from an unmodified app. + ## Transport TrussC uses **HTTP transport** for MCP. All JSON-RPC messages are sent as HTTP POST requests to the `/mcp` endpoint. @@ -40,41 +47,55 @@ TrussC uses **HTTP transport** for MCP. All JSON-RPC messages are sent as HTTP P ## Standard MCP Tools +Tool names are namespaced by origin, so the framework never collides with +your own tools: + +- **`tc_*`** — provided by TrussC core (registered by default, or generated + by core sugar like `mcp::status()`) +- **`tcx__*`** — provided by an official addon (e.g. `tcx_imgui_click`) +- **anything else** — yours; custom tools created with `mcp::tool()` should + NOT use these prefixes + ### Inspection Tools (always available in MCP mode) | Tool | Arguments | Description | |------|-----------|-------------| -| `get_screenshot` | (none) | Get current screen as Base64 PNG image | -| `save_screenshot` | `path` | Save screenshot to file | -| `quit` | (none) | Quit the application gracefully | -| `get_node_tree` | `id`, `depth` (both optional) | Dump the node tree (or a subtree) as JSON: per node `{type, name, id, members, mods, children}`. Members are the `TC_REFLECT`ed values — rotation as euler degrees, colors as `[r,g,b,a]` floats 0-1, Vec3 as `[x,y,z]`, enums as their label string. `mods` lists each attached Mod as `{type, members}`. `depth` limits recursion (~270 bytes/node — on large scenes, explore with `depth` + drill into subtrees by `id`; cut-off nodes carry a `childCount`) | -| `get_selected_node` | (none) | The currently selected node (same shape, no children), or `null` | +| `tc_get_screenshot` | `format`, `width`, `quality`, `window` (all optional) | Screenshot as an MCP image content block (rendered inline by MCP clients) plus a text metadata block. Defaults to full-resolution lossless PNG; pass `width` for a downscaled monitoring thumbnail (aspect preserved, never upscales, clamped 16-4096) and `format: "jpg"` (+ `quality`, default 75) for small payloads. `window` = index from `tc_list_windows` (default 0 = main). Cheap to poll at any settings: only the framebuffer readback touches the frame loop — downscale + encode run on the HTTP worker thread (measured under continuous hammering at jpg/512: ~179 fps vs ~46 fps for the old synchronous encode; baseline ~236) | +| `tc_save_screenshot` | `path`, `window`? | Save screenshot to file. Optional `window` index from `tc_list_windows` (default 0 = main) | +| `tc_list_windows` | (none) | List open windows: index 0 = main, then secondary windows (title, size). Use the index as the `window` arg above | +| `tc_get_health` | (none) | Lightweight liveness snapshot: `{fps, frameCount, uptimeSec, width, height, version, pid, rssBytes, memoryBytes}`. Reads counters only (no GPU state), so it is cheap enough for a supervisor to poll. `pid` lets a supervisor confirm the reply comes from *its* child (port collisions); `rssBytes` is whole-process resident memory (the leak-hunting number); `memoryBytes` is sokol-tracked allocations only | +| `tc_get_status` | (none) | App-published ops status (see [Publishing custom ops status](#publishing-custom-ops-status)): `{values: [{name, value, mode}], images: [names]}`. `mode` is `"status"` (show as-is) or `"graph"` (plot over time). Empty when the app publishes nothing | +| `tc_get_status_image` | `name`, `width`, `quality` (last two optional) | Fetch an app-published image registered via `mcp::statusImage()`, downscaled + JPEG-encoded exactly like `tc_get_screenshot` (pixel grab on the main loop, encode on the HTTP worker — no frame stutter) | +| `tc_get_alerts` | - | Drain operator alerts raised via `mcp::alert()` — returns and clears the pending list, so exactly one consumer receives each alert | +| `tc_quit` | (none) | Quit the application gracefully | +| `tc_get_node_tree` | `id`, `depth` (both optional) | Dump the node tree (or a subtree) as JSON: per node `{type, name, id, members, mods, children}`. Members are the `TC_REFLECT`ed values — rotation as euler degrees, colors as `[r,g,b,a]` floats 0-1, Vec3 as `[x,y,z]`, enums as their label string. `mods` lists each attached Mod as `{type, members}`. `depth` limits recursion (~270 bytes/node — on large scenes, explore with `depth` + drill into subtrees by `id`; cut-off nodes carry a `childCount`) | +| `tc_get_selected_node` | (none) | The currently selected node (same shape, no children), or `null` | ### Recording Tools (always available in MCP mode) -The video counterpart of `save_screenshot` — capture the window to a video file with the native encoder (no ffmpeg). Always registered when MCP is enabled, but unlike the inspection tools these *write* (start/stop a recording), so they are listed separately. +The video counterpart of `tc_save_screenshot` — capture the window to a video file with the native encoder (no ffmpeg). Always registered when MCP is enabled, but unlike the inspection tools these *write* (start/stop a recording), so they are listed separately. | Tool | Arguments | Description | |------|-----------|-------------| -| `start_recording` | `path`, `duration`, `fps`, `codec` (all optional) | Start recording the window. Omit `path` for a timestamped `recording-.mp4` (`.mov` for ProRes) in the data dir; relative paths resolve under the data dir. `duration` (seconds) makes a fixed-length clip that **auto-stops and finalizes itself** at exactly that length (omit or `0` = unlimited). `fps` is the target frame rate (default 60; ProMotion frames are decimated to it). `codec` is `h264` (default) / `hevc` / `prores422` / `prores4444`. Returns `{status, path (resolved), fps, codec}` plus `duration` when a fixed length was set | -| `stop_recording` | (none) | Stop the current recording and finalize the file. A manual stop **always wins** over a pending fixed `duration` — it finalizes immediately at the current length. Returns `{status, recording:false, path, frames, length}` (`length` = measured output seconds). Idle is not an error: returns `{status:"ok", recording:false, message:"not recording"}` | +| `tc_start_recording` | `path`, `duration`, `fps`, `codec` (all optional) | Start recording the window. Omit `path` for a timestamped `recording-.mp4` (`.mov` for ProRes) in the data dir; relative paths resolve under the data dir. `duration` (seconds) makes a fixed-length clip that **auto-stops and finalizes itself** at exactly that length (omit or `0` = unlimited). `fps` is the target frame rate (default 60; ProMotion frames are decimated to it). `codec` is `h264` (default) / `hevc` / `prores422` / `prores4444`. Returns `{status, path (resolved), fps, codec}` plus `duration` when a fixed length was set | +| `tc_stop_recording` | (none) | Stop the current recording and finalize the file. A manual stop **always wins** over a pending fixed `duration` — it finalizes immediately at the current length. Returns `{status, recording:false, path, frames, length}` (`length` = measured output seconds). Idle is not an error: returns `{status:"ok", recording:false, message:"not recording"}` | ### Debugger Tools (opt-in via `mcp::registerDebuggerTools()`) | Tool | Arguments | Description | |------|-----------|-------------| -| `mouse_move` | `x`, `y`, `button` (optional) | Move cursor; with `button` held, emits a drag | -| `mouse_press` | `x`, `y`, `button` | Press and hold — start of a drag gesture | -| `mouse_release` | `x`, `y`, `button` | Release — end of a drag gesture | -| `mouse_click` | `x`, `y`, `button`, `shift`/`ctrl`/`alt`/`super` (optional) | Click mouse button (0:left, 1:right), optionally with modifier keys held — e.g. `super: true` Cmd+clicks | -| `mouse_scroll` | `dx`, `dy` | Scroll mouse wheel | -| `key_press` | `key` | Press a key (sokol_app keycode) | -| `key_release` | `key` | Release a key | -| `select_node` | `id` | Select a node by instance id (0 clears); drives the same selection an inspector shows | -| `set_node_members` | `id`, `members`, `mod` (optional) | Set reflected members from a JSON object (same encoding as `get_node_tree`; enums accept label string or int). Pass `mod` (a Mod's short type name, e.g. `"LayoutMod"`) to target a mod attached to the node instead of the node itself. Reports `applied` / `skipped` (type mismatch) / `readOnly` / `unknown` keys | +| `tc_mouse_move` | `x`, `y`, `button` (optional) | Move cursor; with `button` held, emits a drag | +| `tc_mouse_press` | `x`, `y`, `button` | Press and hold — start of a drag gesture | +| `tc_mouse_release` | `x`, `y`, `button` | Release — end of a drag gesture | +| `tc_mouse_click` | `x`, `y`, `button`, `shift`/`ctrl`/`alt`/`super` (optional) | Click mouse button (0:left, 1:right), optionally with modifier keys held — e.g. `super: true` Cmd+clicks | +| `tc_mouse_scroll` | `dx`, `dy` | Scroll mouse wheel | +| `tc_key_press` | `key` | Press a key (sokol_app keycode) | +| `tc_key_release` | `key` | Release a key | +| `tc_select_node` | `id` | Select a node by instance id (0 clears); drives the same selection an inspector shows | +| `tc_set_node_members` | `id`, `members`, `mod` (optional) | Set reflected members from a JSON object (same encoding as `tc_get_node_tree`; enums accept label string or int). Pass `mod` (a Mod's short type name, e.g. `"LayoutMod"`) to target a mod attached to the node instead of the node itself. Reports `applied` / `skipped` (type mismatch) / `readOnly` / `unknown` keys | The node tools make a scene **round-trippable for agents**: arrange things in a -GUI (e.g. with the `tcxNodeInspector` addon's gizmo), then `get_node_tree` to +GUI (e.g. with the `tcxNodeInspector` addon's gizmo), then `tc_get_node_tree` to read the exact values back and bake them into code — or drive the scene the -other way with `set_node_members`. +other way with `tc_set_node_members`. To enable debugger tools, call `mcp::registerDebuggerTools()` in your `setup()`. Registering the tools *is* the opt-in — there is no separate enable step, and @@ -94,6 +115,49 @@ The **tcxImGui** addon provides additional MCP tools for AI agents to inspect an See [addons/tcxImGui/README.md](../addons/tcxImGui/README.md) for full details on available tools and setup. +## Publishing Custom Ops Status + +Apps can publish their own monitoring data — one line per value, no +supervisor-side configuration. A supervisor (e.g. `anchorbolt start`) +discovers the `tc_get_status` tool via `tools/list` and forwards the +payload to its monitoring server, where numbers registered with +`statusGraph` are plotted over time and images become live streams in the +dashboard. + +```cpp +void tcApp::setup() { + mcp::status("scene", [&] { return sceneName; }); // shown as-is + mcp::statusGraph("visitors", [&] { return visitorCount; }); // plotted over time + mcp::statusImage("entranceCam", [&] { return camPixels; }); // e.g. a webcam +} +``` + +- `mcp::status(name, getter)` — a string or number, displayed as-is +- `mcp::statusGraph(name, getter)` — a number that wants to be a time series +- `mcp::statusImage(name, getter)` — `Pixels` fetched on demand via + `tc_get_status_image` (a webcam feed turns your installation's spare camera + into a monitoring camera with this one line) + +Getters run on the main loop inside tool handlers, so they can safely read +app state. Registering the same name again replaces the entry. + +### Operator Alerts + +For events a human should hear about — a sensor disconnected, a help button +pressed — the app can raise an alert: + +```cpp +mcp::alert("IR camera disconnected!"); +``` + +The message is written to the log (WARNING level) and queued; a supervisor +drains the standard `tc_get_alerts` tool on its health cadence and forwards +each entry to its notification sinks (Slack / Discord / ntfy...), so an +alert can literally end up on someone's phone. It is deliberately named +**alert**, not notify: raise them for exceptional, human-relevant events, +not as a message bus. Thread-safe (callable from sensor callbacks and async +timers); the queue is bounded at 100 pending, oldest dropped first. + ## Creating Custom Tools You can easily expose your own application logic to AI using the `mcp::tool` builder in `setup()`. @@ -153,6 +217,22 @@ curl -X POST http://localhost:8080/mcp \ } ``` +Image tools (`tc_get_screenshot`, `tc_get_status_image`) return an +MCP-standard **image content block** — clients like Claude Code render it +inline instead of receiving a Base64 wall — followed by a text block with +the metadata: + +```json +{ + "result": { + "content": [ + { "type": "image", "data": "", "mimeType": "image/png" }, + { "type": "text", "text": "{\"width\":1920,\"height\":1200}" } + ] + } +} +``` + ## Testing with curl ```bash @@ -167,29 +247,29 @@ curl -X POST http://localhost:8080/mcp \ # Take screenshot curl -X POST http://localhost:8080/mcp \ -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","method":"tools/call","id":2,"params":{"name":"save_screenshot","arguments":{"path":"/tmp/test.png"}}}' + -d '{"jsonrpc":"2.0","method":"tools/call","id":2,"params":{"name":"tc_save_screenshot","arguments":{"path":"/tmp/test.png"}}}' # Mouse click (requires registerDebuggerTools()) curl -X POST http://localhost:8080/mcp \ -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","method":"tools/call","id":3,"params":{"name":"mouse_click","arguments":{"x":100,"y":200}}}' + -d '{"jsonrpc":"2.0","method":"tools/call","id":3,"params":{"name":"tc_mouse_click","arguments":{"x":100,"y":200}}}' # Record a fixed 3-second clip (auto-stops & finalizes itself); omit "duration" -# for an unlimited recording you end with stop_recording. Omit "path" for a +# for an unlimited recording you end with tc_stop_recording. Omit "path" for a # timestamped file in the data dir; the response carries the resolved path. curl -X POST http://localhost:8080/mcp \ -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","method":"tools/call","id":4,"params":{"name":"start_recording","arguments":{"path":"/tmp/clip.mp4","duration":3}}}' + -d '{"jsonrpc":"2.0","method":"tools/call","id":4,"params":{"name":"tc_start_recording","arguments":{"path":"/tmp/clip.mp4","duration":3}}}' # Stop early (a manual stop always wins — valid shorter file); no-op if idle curl -X POST http://localhost:8080/mcp \ -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","method":"tools/call","id":5,"params":{"name":"stop_recording","arguments":{}}}' + -d '{"jsonrpc":"2.0","method":"tools/call","id":5,"params":{"name":"tc_stop_recording","arguments":{}}}' ``` ### Taking Screenshots from Shell -**IMPORTANT for AI agents**: Do NOT use macOS `screencapture`, OS screen recorders (QuickTime, `ffmpeg`-avfoundation, OBS, etc.), or similar OS commands. TrussC apps may render with Metal/OpenGL and the OS cannot capture the screen correctly. Always use the MCP tools — `save_screenshot` for a still, `start_recording` / `stop_recording` for video. +**IMPORTANT for AI agents**: Do NOT use macOS `screencapture`, OS screen recorders (QuickTime, `ffmpeg`-avfoundation, OBS, etc.), or similar OS commands. TrussC apps may render with Metal/OpenGL and the OS cannot capture the screen correctly. Always use the MCP tools — `tc_save_screenshot` for a still, `tc_start_recording` / `tc_stop_recording` for video. ```bash # Start app, wait, take screenshot, then kill @@ -200,7 +280,7 @@ curl -s -X POST http://localhost:8080/mcp \ -d '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{}}' curl -s -X POST http://localhost:8080/mcp \ -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","method":"tools/call","id":2,"params":{"name":"save_screenshot","arguments":{"path":"/tmp/screenshot.png"}}}' + -d '{"jsonrpc":"2.0","method":"tools/call","id":2,"params":{"name":"tc_save_screenshot","arguments":{"path":"/tmp/screenshot.png"}}}' kill %1 # Screenshot is now at /tmp/screenshot.png ``` @@ -232,10 +312,10 @@ Configure your MCP client with the HTTP URL: | Category | Tools | Enabled by | |----------|-------|------------| -| Inspection (read-only) | `get_screenshot`, `save_screenshot`, `get_node_tree`, `get_selected_node` | Automatic when MCP is enabled | -| Recording (window capture to video) | `start_recording`, `stop_recording` | Automatic when MCP is enabled | -| Debugger (input injection / scene mutation) | `mouse_click`, `mouse_press`, `mouse_release`, `key_press`, `mouse_move`, `mouse_scroll`, `key_release`, `select_node`, `set_node_members` | `mcp::registerDebuggerTools()` | -| ImGui (widget interaction) | `imgui_get_widgets`, `imgui_click`, `imgui_input`, `imgui_checkbox` | Requires tcxImGui addon + `mcp::registerDebuggerTools()` | +| Inspection (read-only) | `tc_get_screenshot`, `tc_save_screenshot`, `tc_get_health`, `tc_get_node_tree`, `tc_get_selected_node` | Automatic when MCP is enabled | +| Recording (window capture to video) | `tc_start_recording`, `tc_stop_recording` | Automatic when MCP is enabled | +| Debugger (input injection / scene mutation) | `tc_mouse_click`, `tc_mouse_press`, `tc_mouse_release`, `tc_key_press`, `tc_mouse_move`, `tc_mouse_scroll`, `tc_key_release`, `tc_select_node`, `tc_set_node_members` | `mcp::registerDebuggerTools()` | +| ImGui (widget interaction) | `tcx_imgui_get_widgets`, `tcx_imgui_click`, `tcx_imgui_input`, `tcx_imgui_checkbox` | Requires tcxImGui addon + `mcp::registerDebuggerTools()` | | Custom | `mcp::tool(...)` | Your code | ### Network exposure diff --git a/docs/FOR_AI_ASSISTANT.md b/docs/FOR_AI_ASSISTANT.md index d51287b9..9cdac83c 100644 --- a/docs/FOR_AI_ASSISTANT.md +++ b/docs/FOR_AI_ASSISTANT.md @@ -1007,10 +1007,10 @@ So: setActive / setVisible to temporarily stop/hide, destroy when you're done wi ### Custom-looking button — is click handling provided? -You don't write event handling from scratch. **RectNode has subscribable mouse events built in**, so you only custom-draw the look. RectNode exposes public Event members `mousePressed` / `mouseReleased` / `mouseDragged` / `mouseScrolled` (`Event` etc.), uses rectangle hit-testing, and **has `enableEvents()` already called in its constructor (events on by default).** Two ways to use it: +You don't write event handling from scratch. **RectNode has subscribable mouse events built in**, so you only custom-draw the look. RectNode exposes public Event members `mousePressed` / `mouseReleased` / `mouseDragged` / `mouseScrolled` (`Event` etc.) and uses rectangle hit-testing. **One required step: call `enableEvents()`** — a plain RectNode does NOT receive events by default (only ready-made widgets like `RectNodeButton` and `ScrollContainer` enable it in their constructors); forget it and the node silently never gets a click. Two ways to use it: - **Subscribe from outside (no subclass)**: `listener_ = button->mousePressed.listen([this](MouseEventArgs& e){ ... });` (keep the returned `EventListener` as a member). - **Subclass and override**: `bool onMousePress(const MouseEventArgs& e) override { ...; return true; }`. -Draw freely in local coordinates around (0,0) — rounded corners, image, shader, fully your own. (A plain `Node`, not a RectNode, needs `enableEvents()`. For a ready-made look, `RectNodeButton` — a simple color-on-press button — is built in.) +Draw freely in local coordinates around (0,0) — rounded corners, image, shader, fully your own. (For a ready-made look, `RectNodeButton` — a simple color-on-press button with events pre-enabled — is built in.) ## Events (loose coupling) @@ -1129,7 +1129,7 @@ Pass the length in seconds: `startRecording("out.mp4", 3.0f)` records exactly 3 ### Can an AI agent start/stop recording over MCP? -Yes — every TrussC app in MCP mode (`TRUSSC_MCP=1`) exposes `start_recording` and `stop_recording` as standard tools (the video counterpart of `save_screenshot`, always available). `start_recording` takes optional `path` (omit → timestamped file in the data dir), `duration` (fixed-length auto-stop), `fps`, and `codec`, and returns the resolved path; `stop_recording` returns `{path, frames, length}` and is a no-op (not an error) when nothing is recording. See docs/AI_AUTOMATION.md. +Yes — every TrussC app in MCP mode (`TRUSSC_MCP=1`) exposes `tc_start_recording` and `tc_stop_recording` as standard tools (the video counterpart of `tc_save_screenshot`, always available). `tc_start_recording` takes optional `path` (omit → timestamped file in the data dir), `duration` (fixed-length auto-stop), `fps`, and `codec`, and returns the resolved path; `tc_stop_recording` returns `{path, frames, length}` and is a no-op (not an error) when nothing is recording. See docs/AI_AUTOMATION.md. ### How do I avoid a black first frame when a VideoPlayer starts? @@ -1361,6 +1361,10 @@ If you want an app that runs without a window (update loop only, no `draw()`), u - Log to a file: `getLogger().setLogFile(path)` (unique name: `getTimestampString()`; hook all lines: `onLog.listen`) - CLI / console app: handle `argc`/`argv` in `main()` (no framework arg API); no args → `runApp()`; headless loop → `runHeadlessApp()` +### How are file paths handled? Japanese / non-ASCII filenames on Windows? + +All file-path parameters take `fs::path` (`std::filesystem::path`) — string literals and `std::string` convert implicitly, so just write `img.load("photo.png")` as always. `getDataPath()` also returns `fs::path`; join paths with `/` (`getDataPath("save") / "shot.png"`), not string concatenation. Non-ASCII paths (Japanese filenames, `新しいフォルダー (2)`, spaces) work on every platform including Windows: TrussC converts to the OS-native encoding at the C-library boundary internally, so there is nothing to configure. `setDataPathRoot()` accepts absolute roots on Windows (`C:/data`) too. If you need a narrow string from a path, use `path.string()` on macOS/Linux; avoid it for file IO on Windows (pass the `fs::path` through instead). + ### "Window / media / basics" → which API? - Window: `setWindowTitle()` / `setWindowSize()` / `setFullscreen(bool)` / `toggleFullscreen()` @@ -1381,7 +1385,7 @@ Every TrussC app can become an **MCP server.** Launch with `TRUSSC_MCP=1` and th ### How does an AI tune/verify a running app over MCP? -A rebuild-free loop: launch (`TRUSSC_MCP=1`) → `get_screenshot` to see the current state → `get_node_tree` to read pos / rotation (degrees) / color as numbers → `set_node_members` to nudge them directly → screenshot to check → repeat → finally bake the values into C++ source. Main tools: `get_node_tree` / `get_selected_node` / `select_node` / `set_node_members`. Mouse/key injection is opt-in via `mcp::registerDebuggerTools()` in `setup()`. Drive ImGui UIs with dedicated tools (`imgui_get_widgets` / `imgui_click` / `imgui_input` / `imgui_checkbox`) — raw `mouse_click` doesn't reach ImGui. Expose your own state with `TC_REFLECT`, or return JSON via `mcp::tool` / `mcp::resource`. This enables a closed AI development loop, so you can hand off long, autonomous development sessions. +A rebuild-free loop: launch (`TRUSSC_MCP=1`) → `tc_get_screenshot` to see the current state → `tc_get_node_tree` to read pos / rotation (degrees) / color as numbers → `tc_set_node_members` to nudge them directly → screenshot to check → repeat → finally bake the values into C++ source. Main tools: `tc_get_node_tree` / `tc_get_selected_node` / `tc_select_node` / `tc_set_node_members`. Mouse/key injection is opt-in via `mcp::registerDebuggerTools()` in `setup()`. Drive ImGui UIs with dedicated tools (`tcx_imgui_get_widgets` / `tcx_imgui_click` / `tcx_imgui_input` / `tcx_imgui_checkbox`) — raw `tc_mouse_click` doesn't reach ImGui. Expose your own state with `TC_REFLECT`, or return JSON via `mcp::tool` / `mcp::resource`. This enables a closed AI development loop, so you can hand off long, autonomous development sessions. ## Community & support @@ -1553,11 +1557,11 @@ bool isOverlayFocused() // True when an overlay currently owns keyboard focus ( bool isOverlayHovered() // True when an overlay currently has the pointer over it (e.g. cursor over a tcxImGui panel); guard raw mouse input so clicks on UI panels are not also handled by the app bool isShiftPressed() // True while either Shift key (left or right) is held bool isSuperPressed() // True while either Super / Cmd / Win key (left or right) is held -FileDialogResult loadDialog(const std::string & title = std::string(""), const std::string & message = std::string(""), const std::string & defaultPath = std::string(""), bool folderSelection = false) [macos,windows,linux,android] // Show file open dialog. Returns FileDialogResult with filePath, fileName, success -void loadDialogAsync(const std::string & title, const std::string & message, const std::string & defaultPath, bool folderSelection, std::function callback) // Show file open dialog asynchronously. Callback receives FileDialogResult +FileDialogResult loadDialog(const std::string & title = std::string(""), const std::string & message = std::string(""), const fs::path & defaultPath = fs::path(), bool folderSelection = false) [macos,windows,linux,android] // Show file open dialog. Returns FileDialogResult with filePath, fileName, success +void loadDialogAsync(const std::string & title, const std::string & message, const fs::path & defaultPath, bool folderSelection, std::function callback) // Show file open dialog asynchronously. Callback receives FileDialogResult void requestExitApp() // Request application exit. Can be cancelled by listening to events().exitRequested and setting args.cancel = true -FileDialogResult saveDialog(const std::string & title = std::string(""), const std::string & message = std::string(""), const std::string & defaultPath = std::string(""), const std::string & defaultName = std::string("")) [macos,windows,linux,android] // Show file save dialog. Returns FileDialogResult with filePath, fileName, success -void saveDialogAsync(const std::string & title, const std::string & message, const std::string & defaultPath, const std::string & defaultName, std::function callback) // Show file save dialog asynchronously. Callback receives FileDialogResult +FileDialogResult saveDialog(const std::string & title = std::string(""), const std::string & message = std::string(""), const fs::path & defaultPath = fs::path(), const fs::path & defaultName = fs::path()) [macos,windows,linux,android] // Show file save dialog. Returns FileDialogResult with filePath, fileName, success +void saveDialogAsync(const std::string & title, const std::string & message, const fs::path & defaultPath, const fs::path & defaultName, std::function callback) // Show file save dialog asynchronously. Callback receives FileDialogResult void setCursor(Cursor cursor) // Set the mouse cursor shape void setTouchAsMouse(bool enabled) // Enable/disable touch events firing as mouse events (for Android/iOS) void showCursor() // Show the mouse cursor (default) @@ -1743,7 +1747,7 @@ bool grabScreen(Pixels & outPixels) // Capture current screen to Pixels bool isFullscreen() // Check if window is fullscreen bool isRecording() // Check whether a recording is in progress int recordingFrameCount() // Number of frames captured so far in the current recording -const std::string & recordingPath() // Output file path of the current recording +fs::path recordingPath() // Output file path of the current recording void redraw(int count = 1) // Request extra redraws (useful for event-driven rendering) int runHeadlessApp(const HeadlessSettings & settings = HeadlessSettings()) // Run an app class without a window or graphics context (update loop only). Template on the app type; returns the process exit code bool saveScreenshot(const std::filesystem::path & path) // Save a screenshot of the rendered frame (png/jpg/bmp). Safe to call from anywhere; capture is deferred to after present(). Returns true when the destination was prepared and the capture queued (parent dir created/writable), not that the file is already written. @@ -1757,7 +1761,7 @@ void setWindowPosition(int x, int y) [macos,windows] // Set window position in void setWindowSize(int width, int height) // Set window size void setWindowSizeLogical(int width, int height) // Resize the window to the given logical size (logical pixels) void setWindowTitle(const std::string & title) // Set window title -bool startRecording(const std::string & path, const VideoRecordSettings & settings = {}) [+3] [macos,windows,linux,android,ios] // Start recording the window — or an Fbo (clean, GUI-free output) — to a video file (native encoder, no ffmpeg). Pass a seconds argument (or VideoRecordSettings.duration) for a fixed-length clip that auto-stops and finalizes itself; 0 = unlimited. Calling it again while recording finalizes the current file first, then starts fresh (same path = the old file is overwritten) +bool startRecording(const fs::path & path, const VideoRecordSettings & settings = {}) [+3] [macos,windows,linux,android,ios] // Start recording the window — or an Fbo (clean, GUI-free output) — to a video file (native encoder, no ffmpeg). Pass a seconds argument (or VideoRecordSettings.duration) for a fixed-length clip that auto-stops and finalizes itself; 0 = unlimited. Calling it again while recording finalizes the current file first, then starts fresh (same path = the old file is overwritten) void stopRecording() // Stop the current recording and finalize the file void toggleFullscreen() // Toggle fullscreen mode ``` @@ -1794,7 +1798,7 @@ Json reflectToJson(T & obj) // Return all reflected (TC_REFLECT) members of obj void runOnMainThread(std::function fn) // Run a callback on the main (scene) thread; immediately if already on it, otherwise queued to the next frame void setConsoleLogLevel(LogLevel level) // Set the minimum log level printed to the console void setFileLogLevel(LogLevel level) // Set the minimum log level written to the log file -bool setLogFile(const std::string & path) // Open a file to receive log output +bool setLogFile(const fs::path & path) // Open a file to receive log output const std::string & shortTypeName(const std::type_info & ti) // Short (unqualified) readable name for a type, cached per type std::vector splitString(const std::string & source, const std::string & delimiter, bool ignoreEmpty = false, bool trim = false) // Split string by delimiter void stringReplace(std::string & input, const std::string & searchStr, const std::string & replaceStr) // Replace substring in place @@ -1809,7 +1813,7 @@ LogStream tcLogVerbose(const std::string & module = std::string("")) ⚠️depre LogStream tcLogWarning(const std::string & module = std::string("")) ⚠️deprecated // Deprecated alias for logWarning() void tcSetConsoleLogLevel(LogLevel level) ⚠️deprecated // Deprecated alias for setConsoleLogLevel() void tcSetFileLogLevel(LogLevel level) ⚠️deprecated // Deprecated alias for setFileLogLevel() -bool tcSetLogFile(const std::string & path) ⚠️deprecated // Deprecated alias for setLogFile() +bool tcSetLogFile(const fs::path & path) ⚠️deprecated // Deprecated alias for setLogFile() std::string toBase64(const unsigned char * bytes, size_t len) [+2] // Encode raw bytes as a Base64 string std::string toBinary(int value) [+4] // Convert an integer to a binary string bool toBool(const std::string & str) // Parse a string into a bool @@ -1831,29 +1835,30 @@ const std::string & typeName(const std::type_info & ti) [+1] // Readable (deman ### File ```cpp -bool appendToFile(const std::string & path, const std::string & content) // Append string to file -bool createDirectory(const std::string & path) // Create directory (and parents) -bool directoryExists(const std::string & path) // Check if directory exists -bool fileExists(const std::string & path) // Check if file exists -std::string getAbsolutePath(const std::string & path) // Get absolute path -std::string getBaseName(const std::string & path) // Get filename without extension -std::string getDataPath(const std::string & filename) // Get full path relative to data directory -std::string getDataPathRoot() // Get the current data path root (with trailing slash). -std::string getExecutableDir() // Get the directory containing the running executable (with trailing slash). -std::string getExecutablePath() // Get the absolute path of the running executable. -std::string getFileExtension(const std::string & path) // Get file extension without dot -std::string getFileName(const std::string & path) // Get filename from path -int64_t getFileSize(const std::string & path) // Get file size in bytes -std::string getParentDirectory(const std::string & path) // Get parent directory -std::string joinPath(const std::string & dir, const std::string & file) // Join directory and filename -std::vector listDirectory(const std::string & path) // List files in directory -Json loadJson(const std::string & path) // Load a JSON file and return it as a Json object. Relative paths are resolved via getDataPath; returns an empty Json on error. -std::string loadTextFile(const std::string & path) // Load entire text file -Xml loadXml(const std::string & path) // Load an XML file and return it as an Xml object. Relative paths are resolved via getDataPath. -bool removeFile(const std::string & path) // Remove file -bool saveJson(const Json & j, const std::string & path, int indent = 2) // Write a Json object to a file. Relative paths are resolved via getDataPath. indent sets the pretty-print width (negative for compact). Returns true on success. -bool saveTextFile(const std::string & path, const std::string & content) // Save string to text file -void setDataPathRoot(const std::string & path) // Set the root directory used to resolve relative data paths. A relative path is resolved against the executable directory; an absolute path (starting with /) is used as-is. A trailing slash is added automatically. +bool appendToFile(const fs::path & path, const std::string & content) // Append string to file +bool createDirectory(const fs::path & path) // Create directory (and parents) +bool directoryExists(const fs::path & path) // Check if directory exists +bool fileExists(const fs::path & path) // Check if file exists +std::string getAbsolutePath(const fs::path & path) // Get absolute path +std::string getBaseName(const fs::path & path) // Get filename without extension +fs::path getDataPath(const fs::path & filename) // Resolve a relative path against the data directory and return it as fs::path. An absolute input is returned unchanged. +fs::path getDataPathRoot() // Get the current data path root as fs::path. +fs::path getExecutableDir() // Get the directory containing the running executable. +fs::path getExecutablePath() // Get the absolute path of the running executable. +std::string getFileExtension(const fs::path & path) // Get file extension without dot +std::string getFileName(const fs::path & path) // Get filename from path +int64_t getFileSize(const fs::path & path) // Get file size in bytes +std::string getParentDirectory(const fs::path & path) // Get parent directory +std::string joinPath(const fs::path & dir, const fs::path & file) // Join directory and filename +std::vector listDirectory(const fs::path & path) // List files in directory +const char * loadErrorName(LoadError e) // Short label for a LoadError value ("FileNotFound", ...). For log messages +Json loadJson(const fs::path & path) // Load a JSON file and return it as a Json object. Relative paths are resolved via getDataPath; returns an empty Json on error. +std::string loadTextFile(const fs::path & path) // Load entire text file +Xml loadXml(const fs::path & path) // Load an XML file and return it as an Xml object. Relative paths are resolved via getDataPath. +bool removeFile(const fs::path & path) // Remove file +bool saveJson(const Json & j, const fs::path & path, int indent = 2) // Write a Json object to a file. Relative paths are resolved via getDataPath. indent sets the pretty-print width (negative for compact). Returns true on success. +bool saveTextFile(const fs::path & path, const std::string & content) // Save string to text file +void setDataPathRoot(const fs::path & path) // Set the root directory used to resolve relative data paths. A relative root is resolved against the executable directory; an absolute root (fs::path::is_absolute, e.g. C:/ on Windows) is used as-is. void setDataPathToResources() [macos,ios] // Point the data path root at the macOS app bundle's Contents/Resources/data folder for distribution. No-op on non-macOS platforms. ``` @@ -1878,7 +1883,7 @@ void shutdownAudio() // Shut down the global AudioEngine and close the audio de ```cpp std::vector listSystemFonts() [macos,windows,linux,ios] // Enumerate names of all fonts known to the OS -std::string systemFontPath(const std::string & name) [macos,windows,linux,ios] // Resolve a system font name (PostScript / family) to a file path. Returns empty string if not found. macOS uses CoreText; Linux/Windows currently stub. +fs::path systemFontPath(const std::string & name) [macos,windows,linux,ios] // Resolve a system font name (PostScript / family) to a file path. Returns empty string if not found. macOS uses CoreText; Linux/Windows currently stub. ``` ### Animation @@ -2007,6 +2012,7 @@ float atanh(float x) [std] // Inverse hyperbolic tangent Baseline // Direction shorthand for Direction::Baseline (text baseline) Bottom // Direction shorthand for Direction::Bottom Center // Direction shorthand for Direction::Center +std::shared_ptr createWindow(const WindowSettings & settings = {}) [macos,windows,linux] // Create a secondary window (macOS only for now; returns nullptr elsewhere). It runs on its own display link; closing it leaves the app running const char * enumLabel(E value) // Return the display string for one enum value (TC_ENUM_LABELS override, else reflected name). const std::array enumNames() // Return a compile-time array of all valid enumerator names of E. EnumLabelSpan enumReflectedSpan() // Return an EnumLabelSpan synthesized from reflection (valid for contiguous zero-based enums). @@ -2320,7 +2326,7 @@ const Texture & Environment::getIrradianceMap() const // Get irradiance cubemap const Texture & Environment::getPrefilterMap() const // Get prefiltered environment cubemap for specular IBL int Environment::getPrefilterMipLevels() const // Get number of mip levels in the prefilter map bool Environment::isLoaded() const // Check if environment is loaded -bool Environment::loadFromHDR(const std::string & path) [+1] // Load environment from HDR image file +bool Environment::loadFromHDR(const fs::path & path) [+1] // Load environment from HDR image file bool Environment::loadProcedural() // Generate a simple procedural sky environment void Environment::release() // Release GPU resources ``` @@ -2383,7 +2389,7 @@ bool Fbo::save(const fs::path & path) const // Save FBO contents to file void FileReader::close() // Close file bool FileReader::eof() const // Check if at end of file bool FileReader::isOpen() const // Check if file is open -bool FileReader::open(const std::string & path) // Open file for reading +bool FileReader::open(const fs::path & path) // Open file for reading size_t FileReader::read(void * buffer, size_t size) // Read binary data int FileReader::readChar() // Read one character (-1 at EOF) std::string FileReader::readLine() [+1] // Read one line @@ -2398,7 +2404,7 @@ size_t FileReader::tell() // Get current position void FileWriter::close() // Close file void FileWriter::flush() // Flush buffer to disk bool FileWriter::isOpen() const // Check if file is open -bool FileWriter::open(const std::string & path, bool append = false) // Open file for writing +bool FileWriter::open(const fs::path & path, bool append = false) // Open file for writing FileWriter & FileWriter::write(const std::string & text) [+2] // Write data to file FileWriter & FileWriter::writeLine(const std::string & text = std::string("")) // Write line with newline ``` @@ -2446,7 +2452,7 @@ bool Font::isLoaded() const // Check if loaded bool Font::isWrapEnabled() const // Check if line wrapping is enabled bool Font::kinsokuLineEnd(uint32_t cp) const // Return whether a codepoint is forbidden at the end of a line (kinsoku rule). bool Font::kinsokuLineStart(uint32_t cp) const // Return whether a codepoint is forbidden at the start of a line (kinsoku rule). -bool Font::load(const std::string & nameOrPath, int size) // Load font file +LoadResult Font::load(const fs::path & nameOrPath, int size) // Load font file void Font::resetLineHeight() // Reset line height to the font default void Font::setAlign(Direction h, Direction v) [+1] // Set horizontal (and optional vertical) text alignment void Font::setHangingPunctuation(bool enabled) // Let prohibited line-start CJK punctuation hang past the line edge instead of wrapping (default off) @@ -2546,7 +2552,7 @@ sg_sampler IesProfile::getSampler() const // Return the sokol-gfx sampler of th int IesProfile::getTextureWidth() const // Get width of the generated 1D lookup texture sg_view IesProfile::getView() const // Return the sokol-gfx texture view of the IES profile for pipeline binding (advanced interop). bool IesProfile::isLoaded() const // Check if profile is loaded -bool IesProfile::load(const std::string & path) // Load IES profile from file +bool IesProfile::load(const fs::path & path) // Load IES profile from file bool IesProfile::loadFromString(const std::string & data) // Load IES profile from inline string data ``` @@ -2565,8 +2571,8 @@ Texture & Image::getTexture() [+1] // Get internal texture int Image::getWidth() const // Get width void Image::halve() // Replace with 2x2 box-averaged half. Gamma-correct for U8. bool Image::isAllocated() const // Check if allocated -bool Image::load(const fs::path & path, bool mipmaps = false) // Load image from file. `mipmaps=true` builds a mip chain — recommended when the image will be sampled at varying scales (e.g. mapped onto a 3D surface). -bool Image::loadFromMemory(const unsigned char * buffer, int len, bool mipmaps = false) // Load image from memory. `mipmaps=true` builds a mip chain. +LoadResult Image::load(const fs::path & path, bool mipmaps = false) // Load image from file. `mipmaps=true` builds a mip chain — recommended when the image will be sampled at varying scales (e.g. mapped onto a 3D surface). +LoadResult Image::loadFromMemory(const unsigned char * buffer, int len, bool mipmaps = false) // Load image from memory. `mipmaps=true` builds a mip chain. void Image::mirror(bool horizontal, bool vertical) // Flip the image. `horizontal=true` mirrors left-right; `vertical=true` mirrors top-bottom; both true is 180°. void Image::mirrorH() // Mirror horizontally (alias for mirror(true, false)) void Image::mirrorV() // Mirror vertically (alias for mirror(false, true)) @@ -2673,6 +2679,14 @@ void Light::setSpecular(const Color & c) [+1] // Set specular light color void Light::setSpot(const Vec3 & position, const Vec3 & direction, float innerHalfAngle = 0.0, float outerHalfAngle = 0.785399973) [+1] // Set as spot light with cone angles ``` +### LoadResult — Result of a resource load (Image, SoundBuffer, VideoPlayer, Font, ...). Truthy on success; on failure carries a LoadError and a human-readable message. + +```cpp +LoadResult LoadResult::fail(LoadError e, std::string msg = std::string()) // Make a failure result with an error kind and optional message (static) +bool LoadResult::ok() const // true if the load succeeded (error == LoadError::None) +LoadResult LoadResult::success() // Make a success result (static) +``` + ### Location — GPS / WiFi location fix returned by getLocation() ```cpp @@ -2699,7 +2713,7 @@ bool Logger::isFileOpen() const // Check whether a log file is currently open void Logger::log(LogLevel level, const std::string & message) // Emit a log message at the given level void Logger::setConsoleLogLevel(LogLevel level) // Set the minimum console log level void Logger::setFileLogLevel(LogLevel level) // Set the minimum file log level -bool Logger::setLogFile(const std::string & path) // Open a file to receive log output +bool Logger::setLogFile(const fs::path & path) // Open a file to receive log output ``` ### Mat3 — 3x3 matrix for 2D affine / homography transforms (row-major). Includes static factories and a homography solver @@ -3082,9 +3096,9 @@ int Pixels::getWidth() const // Get width void Pixels::halve() // Replace with 2x2 box-averaged half. Gamma-correct for U8. bool Pixels::isAllocated() const // Check if allocated bool Pixels::isFloat() const // Whether the pixel data uses 32-bit floats -bool Pixels::load(const fs::path & path) // Load image from file -bool Pixels::loadFromMemory(const unsigned char * buffer, int len) // Load image from memory -bool Pixels::loadHDR(const fs::path & path) // Load an HDR (.hdr) image into a float pixel buffer +LoadResult Pixels::load(const fs::path & path) // Load image from file +LoadResult Pixels::loadFromMemory(const unsigned char * buffer, int len) // Load image from memory +LoadResult Pixels::loadHDR(const fs::path & path) // Load an HDR (.hdr) image into a float pixel buffer bool Pixels::loadPlatform(const fs::path & path) // Load an image using the platform image decoder void Pixels::mirror(bool horizontal, bool vertical) // Flip in place. Both true is 180°. void Pixels::mirrorH() // Mirror horizontally (alias for mirror(true, false)) @@ -3223,9 +3237,9 @@ bool Reflector::visit(const char * name, float & v) [+7] // Handle one reflecte ```cpp int ScreenRecorder::getFrameCount() const // Number of frames captured so far -const std::string & ScreenRecorder::getPath() const // Output file path of the current recording +fs::path ScreenRecorder::getPath() const // Output file path of the current recording bool ScreenRecorder::isRecording() const // Check if the screen recorder is currently capturing -bool ScreenRecorder::start(const std::string & path, const VideoRecordSettings & settings = {}) [+3] // Start live capture (window, or an Fbo for clean GUI-free output); size is taken automatically. Calling start while recording finalizes the current file first. If the recorded Fbo is destroyed mid-recording, the recording stops and finalizes automatically +bool ScreenRecorder::start(const fs::path & path, const VideoRecordSettings & settings = {}) [+3] // Start live capture (window, or an Fbo for clean GUI-free output); size is taken automatically. Calling start while recording finalizes the current file first. If the recorded Fbo is destroyed mid-recording, the recording stops and finalizes automatically void ScreenRecorder::stop() // Stop live capture and finalize the file VideoWriter & ScreenRecorder::writer() // Access the underlying VideoWriter for advanced introspection ``` @@ -3356,9 +3370,9 @@ bool Sound::isLoop() const // Check if loop mode is enabled bool Sound::isPaused() const // Check if paused bool Sound::isPlaying() const // Check if playing bool Sound::isStreaming() const // True if this Sound was loaded via loadStream() (vs eager load()) -bool Sound::load(const std::string & path) // Load audio file. Format auto-detected by extension: .wav .mp3 .ogg .flac .aac .m4a +LoadResult Sound::load(const fs::path & path) // Load audio file. Format auto-detected by extension: .wav .mp3 .ogg .flac .aac .m4a void Sound::loadFromBuffer(const SoundBuffer & buf) [+1] // Load PCM directly from a pre-generated SoundBuffer (e.g. from ChipSound or a procedural waveform), copying it or adopting the shared_ptr. -bool Sound::loadStream(const std::string & path, int maxPolyphony = 1) [macos,windows,linux,android,ios] // Stream sound from disk (WAV/MP3/FLAC). Best for long files; cuts memory. maxPolyphony = simultaneous play() count. +LoadResult Sound::loadStream(const fs::path & path, int maxPolyphony = 1) [macos,windows,linux,android,ios] // Stream sound from disk (WAV/MP3/FLAC). Best for long files; cuts memory. maxPolyphony = simultaneous play() count. void Sound::loadTestTone(float frequency = 440.0, float duration = 1.0) // Load a generated sine test tone (no file needed). Handy for verifying audio output. void Sound::pause() // Pause playback void Sound::play() // Play audio @@ -3389,18 +3403,18 @@ void SoundBuffer::generateSquareWave(float frequency, float duration, float volu void SoundBuffer::generateTriangleWave(float frequency, float duration, float volume = 0.5, int sr = 44100) // Fill the buffer with a mono triangle wave. int SoundBuffer::getAdtsSampleRateIndex(int sampleRate) // ADTS sample-rate index for the given rate (AAC-in-MOV container helper). float SoundBuffer::getDuration() const // Duration in seconds (numSamples / sampleRate). -bool SoundBuffer::load(const std::string & path) // Decode a file into PCM, auto-detecting format from the extension (.wav .mp3 .ogg .flac .aac .m4a, case-insensitive). Returns false on failure. -bool SoundBuffer::loadAac(const std::string & path) [macos,windows,linux,ios,web] // Decode an AAC / M4A file into PCM (platform-specific; returns false on unsupported platforms). -bool SoundBuffer::loadAacFromMemory(const void * data, size_t dataSize) [macos,windows,linux,ios,web] // Decode AAC data from a memory buffer (platform-specific; returns false on unsupported platforms). -bool SoundBuffer::loadFlac(const std::string & path) // Decode a FLAC file into PCM. -bool SoundBuffer::loadFlacFromMemory(const void * data, size_t dataSize) // Decode FLAC data from a memory buffer. -bool SoundBuffer::loadMp3(const std::string & path) // Decode an MP3 file into PCM. -bool SoundBuffer::loadMp3FromMemory(const void * data, size_t dataSize) // Decode MP3 data from a memory buffer. -bool SoundBuffer::loadOgg(const std::string & path) // Decode an OGG Vorbis file into PCM (via stb_vorbis). -bool SoundBuffer::loadOggFromMemory(const void * data, size_t dataSize) // Decode OGG Vorbis data from a memory buffer. -bool SoundBuffer::loadPcmFromMemory(const void * data, size_t dataSize, int numChannels, int rate, int bitsPerSample = 16, bool bigEndian = false) // Load raw interleaved PCM (16-bit signed or 32-bit float) from memory with explicit format. Returns false for unsupported bit depths. -bool SoundBuffer::loadWav(const std::string & path) // Decode a WAV file into PCM. -bool SoundBuffer::loadWavFromMemory(const void * data, size_t dataSize) // Decode WAV data from a memory buffer. +LoadResult SoundBuffer::load(const fs::path & path) // Decode a file into PCM, auto-detecting format from the extension (.wav .mp3 .ogg .flac .aac .m4a, case-insensitive). Returns false on failure. +LoadResult SoundBuffer::loadAac(const fs::path & path) [macos,windows,linux,ios,web] // Decode an AAC / M4A file into PCM (platform-specific; returns false on unsupported platforms). +LoadResult SoundBuffer::loadAacFromMemory(const void * data, size_t dataSize) [macos,windows,linux,ios,web] // Decode AAC data from a memory buffer (platform-specific; returns false on unsupported platforms). +LoadResult SoundBuffer::loadFlac(const fs::path & path) // Decode a FLAC file into PCM. +LoadResult SoundBuffer::loadFlacFromMemory(const void * data, size_t dataSize) // Decode FLAC data from a memory buffer. +LoadResult SoundBuffer::loadMp3(const fs::path & path) // Decode an MP3 file into PCM. +LoadResult SoundBuffer::loadMp3FromMemory(const void * data, size_t dataSize) // Decode MP3 data from a memory buffer. +LoadResult SoundBuffer::loadOgg(const fs::path & path) // Decode an OGG Vorbis file into PCM (via stb_vorbis). +LoadResult SoundBuffer::loadOggFromMemory(const void * data, size_t dataSize) // Decode OGG Vorbis data from a memory buffer. +LoadResult SoundBuffer::loadPcmFromMemory(const void * data, size_t dataSize, int numChannels, int rate, int bitsPerSample = 16, bool bigEndian = false) // Load raw interleaved PCM (16-bit signed or 32-bit float) from memory with explicit format. Returns false for unsupported bit depths. +LoadResult SoundBuffer::loadWav(const fs::path & path) // Decode a WAV file into PCM. +LoadResult SoundBuffer::loadWavFromMemory(const void * data, size_t dataSize) // Decode WAV data from a memory buffer. void SoundBuffer::mixFrom(const SoundBuffer & other, size_t offsetSamples, float volume = 1.0) // Additively mix another buffer into this one starting at offsetSamples, growing this buffer if needed. ``` @@ -3416,8 +3430,8 @@ Kind SoundSource::kind() const // Source kind (Eager for SoundBuffer, Stream fo ```cpp float SoundStream::getDuration() const // Decoded file duration in seconds. int SoundStream::getMaxPolyphony() const // Number of concurrent decoder slots reserved at loadStream(). -const std::string & SoundStream::getPath() const // Path the stream was opened from. -bool SoundStream::loadStream(const std::string & path, int maxPolyphony = 1) // Open the file, validate format (.wav .mp3 .flac .ogg), and populate channels / sampleRate / duration. maxPolyphony reserves that many concurrent decoder slots. Returns false if the file can't be opened or the format is unsupported. +fs::path SoundStream::getPath() const // Path the stream was opened from. +LoadResult SoundStream::loadStream(const fs::path & path, int maxPolyphony = 1) // Open the file, validate format (.wav .mp3 .flac .ogg), and populate channels / sampleRate / duration. maxPolyphony reserves that many concurrent decoder slots. Returns false if the file can't be opened or the format is unsupported. ``` ### StrokeMesh — Variable-width polyline stroke geometry with caps, joins and miter limit; build it from points or a Path, then update() and draw() @@ -3813,8 +3827,8 @@ void VideoGrabber::update() // Poll for a new frame and upload it to the textur ```cpp void VideoPlayer::close() // Close the video and release resources void VideoPlayer::draw(float x, float y) const [+1] // Draw the current video frame at (x, y), optionally scaled to w x h -bool VideoPlayer::extractFrame(const std::string & path, Pixels & outPixels, float timeSec, float * outDuration = nullptr) [+3] // Extract the exact frame at a given time from a video file. Frame-accurate on every platform -bool VideoPlayer::extractKeyFrame(const std::string & path, Pixels & outPixels, float timeSec, float * outDuration = nullptr) [+3] // Extract the nearest keyframe at or before a given time. Faster than extractFrame but time-approximate +bool VideoPlayer::extractFrame(const fs::path & path, Pixels & outPixels, float timeSec, float * outDuration = nullptr) [+3] // Extract the exact frame at a given time from a video file. Frame-accurate on every platform +bool VideoPlayer::extractKeyFrame(const fs::path & path, Pixels & outPixels, float timeSec, float * outDuration = nullptr) [+3] // Extract the nearest keyframe at or before a given time. Faster than extractFrame but time-approximate int VideoPlayer::getAudioChannels() const [macos,windows,linux,ios] // Number of audio channels (0 if no audio) uint32_t VideoPlayer::getAudioCodec() const [macos,windows,linux,ios] // FourCC of the audio codec in the loaded video (0 if none) std::vector VideoPlayer::getAudioData() const [macos,windows,linux,ios] // Raw decoded audio data for the loaded video @@ -3824,7 +3838,7 @@ int VideoPlayer::getCurrentFrame() const // Get current frame number float VideoPlayer::getDuration() const // Get total duration in seconds float VideoPlayer::getGammaCorrection() const // Get current gamma correction value std::string VideoPlayer::getHwAccelName() const // Get the name of the active decode backend. Returns 'vaapi', 'v4l2m2m', 'cuda', 'videotoolbox', 'mediafoundation', 'software', or 'none' -const std::string & VideoPlayer::getPath() const // Path of the currently loaded video file (resolved via getDataPath); empty string when nothing is loaded +fs::path VideoPlayer::getPath() const // Path of the currently loaded video file (resolved via getDataPath); empty string when nothing is loaded unsigned char * VideoPlayer::getPixels() [+1] // Pointer to the current RGBA pixel buffer (mutable) unsigned char * VideoPlayer::getPixelsUV() // Pointer to the interleaved UV (chroma) plane when decoding NV12; null otherwise unsigned char * VideoPlayer::getPixelsY() // Pointer to the Y (luma) plane when decoding NV12/YUV; null otherwise @@ -3833,7 +3847,7 @@ int VideoPlayer::getTotalFrames() const // Get total number of frames bool VideoPlayer::getUseHwAccel() const // Get HW accel preference (not the actual backend — use isUsingHwAccel() for that) bool VideoPlayer::hasAudio() const // Check if the loaded video has an audio track bool VideoPlayer::isUsingHwAccel() const // Check if hardware decoding is currently active (after load) -bool VideoPlayer::load(const std::string & path) // Load a video file +LoadResult VideoPlayer::load(const fs::path & path) // Load a video file void VideoPlayer::nextFrame() // Advance to the next frame void VideoPlayer::play() // Start or resume playback. With the auto poster (default), a seek made while stopped/paused is bridged with the exact frame at the new position before playback, so it never starts on a stale picture void VideoPlayer::playImpl() // Backend implementation of playImpl for this platform's video player. @@ -3884,7 +3898,7 @@ bool VideoPlayerBase::isPaused() const // Check if video is paused bool VideoPlayerBase::isPlaying() const // Check if video is currently playing (not paused) bool VideoPlayerBase::isReady() const // True while the texture holds a real picture — i.e. drawing shows actual video, not black. With the default auto poster this is true from load() on; false only if the poster failed and no frame has arrived yet bool VideoPlayerBase::isUsingHwAccel() const // Return true if hardware-accelerated decoding is currently active. -bool VideoPlayerBase::load(const std::string & path) // Load a video from the given file path; return true on success. +LoadResult VideoPlayerBase::load(const fs::path & path) // Load a video from the given file path; return true on success. void VideoPlayerBase::markDone() // Mark playback as done, clearing playing unless looping. void VideoPlayerBase::markFrameNew() // Mark that a new frame has arrived (sets frameNew and firstFrameReceived). void VideoPlayerBase::nextFrame() // Advance to the next frame. @@ -3926,18 +3940,34 @@ void VideoWriter::close() // Finalize and flush the video file float VideoWriter::getFps() const // Fixed encoding frame rate int VideoWriter::getFrameCount() const // Number of frames written so far int VideoWriter::getHeight() const // Encoder output height in pixels -const std::string & VideoWriter::getPath() const // Resolved output file path +fs::path VideoWriter::getPath() const // Resolved output file path const VideoRecordSettings & VideoWriter::getSettings() const // Encoder settings the writer was opened with int VideoWriter::getWidth() const // Encoder output width in pixels bool VideoWriter::isOpen() const // Check if the encoder is open and accepting frames -unsigned char * VideoWriter::lockFrame(int & strideOut) // Lock and return the encoder's frame buffer for zero-copy fills; strideOut receives the row stride. Pair with submitFrame -bool VideoWriter::open(const std::string & path, int width, int height, const VideoRecordSettings & settings = {}) // Open the encoder at the given size (path resolved via getDataPath) -bool VideoWriter::submitFrame(double timeSec) // Append the previously locked frame at the given presentation time (seconds) +unsigned char * VideoWriter::lockFrame(int & strideOut) [macos] // Lock and return the encoder's frame buffer for zero-copy fills; strideOut receives the row stride. Pair with submitFrame +bool VideoWriter::open(const fs::path & path, int width, int height, const VideoRecordSettings & settings = {}) // Open the encoder at the given size (path resolved via getDataPath) +bool VideoWriter::submitFrame(double timeSec) [macos] // Append the previously locked frame at the given presentation time (seconds) +``` + +### Window — A secondary application window (macOS only for now). Owns its own Node tree, events, mouse state and render context; ticks at its display's refresh rate. GPU resources are shared with every other window + +```cpp +void Window::close() // Close the native window; the main window and other windows keep running +CoreEvents & Window::events() // This window's own event stream (mousePressed / keyPressed / draw / ...) +std::shared_ptr Window::getApp() const // Get the App attached to this window +int Window::getHeight() const // Window height in logical points (matches its coordinate system) +const std::string & Window::getTitle() const // Last title set for this window (via WindowSettings or setTitle) +int Window::getWidth() const // Window width in logical points (matches its coordinate system) +bool Window::isOpen() const // Whether the native window is still open +void Window::setApp(std::shared_ptr app) // Attach an App to this window — the only way to give a window content. The App's full lifecycle (setup/update/draw/key/mouse/windowResized + RectNode size sync) runs against this window. One App per window +void Window::setClearColor(const Color & c) // Background clear color for this window +void Window::setTitle(const std::string & title) // Set the window title ``` ### WindowSettings — Window configuration passed to the app at startup (size, title, DPI, MSAA, fullscreen, decoration, VSync). Setters chain ```cpp +WindowSettings & WindowSettings::reserveUniformBuffer(int bytes) // Reserve the per-frame GPU uniform buffer in bytes, vector::reserve style; 0 = backend default 4MB ≈ 8k draw calls. Metal/WebGPU/Vulkan only — GL/D3D11 have no such cap (chainable) WindowSettings & WindowSettings::setClipboardSize(int size) // Set clipboard buffer size in bytes (chainable) WindowSettings & WindowSettings::setDecorated(bool enabled) // false = borderless/chromeless window that can still take focus and be closed programmatically (chainable) WindowSettings & WindowSettings::setFullscreen(bool enabled) // Enable/disable fullscreen at startup (chainable) @@ -3957,10 +3987,10 @@ XmlNode Xml::addRoot(const std::string & name) // Append a new root element wit XmlNode Xml::child(const std::string & name) // Find a direct child node of the document by name. XmlDocument & Xml::document() [+1] // Access the underlying pugixml document for advanced operations. bool Xml::empty() const // Return true if the document has no content. -bool Xml::load(const std::string & path) // Load an XML document from a file. Relative paths are resolved via getDataPath. Returns true on success. +bool Xml::load(const fs::path & path) // Load an XML document from a file. Relative paths are resolved via getDataPath. Returns true on success. bool Xml::parse(const std::string & str) // Parse an XML document from a string. Returns true on success. XmlNode Xml::root() [+1] // Get the document's root element node. -bool Xml::save(const std::string & path, const std::string & indent = std::string(" ")) const // Save the document to a file. Relative paths are resolved via getDataPath. indent sets the per-level indentation string. Returns true on success. +bool Xml::save(const fs::path & path, const std::string & indent = std::string(" ")) const // Save the document to a file. Relative paths are resolved via getDataPath. indent sets the per-level indentation string. Returns true on success. std::string Xml::toString(const std::string & indent = std::string(" ")) const // Serialize the document to an XML string. indent sets the per-level indentation string. ``` @@ -3981,6 +4011,7 @@ enum ImageType { Color, Grayscale } // Image type: Color or Grayscale. enum KinsokuLevel { Off, PunctuationOnly, Standard } // Line-breaking (kinsoku) strictness for vertical / Japanese text enum LayoutDirection { Vertical, Horizontal } // Layout axis direction: Vertical or Horizontal. enum LightType { Directional, Point, Spot } // Light type: Directional, Point, or Spot. +enum LoadError { None, FileNotFound, UnsupportedFormat, DecodeFailed, Unknown } // Load failure kind: None, FileNotFound, UnsupportedFormat, DecodeFailed, Unknown. enum LogLevel { Verbose, Notice, Warning, Error, Fatal, Silent } // Log severity, from Verbose (most detailed) to Fatal; Silent disables logging. enum MixMode { Auto, DownmixMono } // Sound channel mixing: Auto (match the output) or DownmixMono. enum MouseButton { Left, Right, Middle, None } // Mouse button: Left, Right, Middle, or None. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 8ff29d8a..829a5a9e 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -16,19 +16,27 @@ | Multi-light shadows | Support shadow maps for multiple lights simultaneously | High | | Area lights | Rectangle / disc / line area light sources | High | | Cascaded shadow maps | CSM for directional lights (large outdoor scenes) | High | -| Unified `LoadResult` API | Replace `bool` return of `Image::load` / `SoundBuffer::load` / `Video::load` / `Font::load` / `Shader::load` with a shared `trussc::LoadResult` carrying `LoadError` enum + message + raw code. Use `explicit operator bool()` so existing `if (x.load(...))` keeps working. Needs an audit of error taxonomy first (file-not-found / invalid-format / permission-denied / decoder-failure / etc.) and a per-domain inventory of what error sources exist (stb_image, AVFoundation, mbedTLS, miniaudio, etc.). | High | -| Real-time audio stream API | Expose the audio callback so user code can synthesize / process samples per buffer (typical 256–512 frames at 96 kHz). oF-style listener pattern: `App` gets an overridable `audioOut(float* out, size_t frames, int channels)` (and `audioIn(const float* in, ...)`); plus a free-function or `SoundStream` class for non-App-bound code. Currently the only way to produce sound is pre-baked `SoundBuffer` + `Sound::play()`, which can't handle dynamic synthesis (sine generators with live parameter changes, granular synthesis, software synths, audio effects, etc.). Output runs at engine native rate (96 kHz, no rateRatio path) — document that. Likely paired with `tc::AudioSettings` (sample rate / buffer size / channel count / max polyphony) passed to a new `AudioEngine::init(settings)` overload, so the engine config is exposed at the same time. | High | -| `AudioEngine::init(AudioSettings)` overload | Currently `AudioEngine::SAMPLE_RATE` / `NUM_CHANNELS` / `MAX_PLAYING_SOUNDS` are `static constexpr`. Wrap them in a `tc::AudioSettings` struct so `AudioEngine::init(settings)` (called at the very start of `setup()` before any `Sound::load()`) can opt into a different sample rate (48 kHz for mobile CPU savings, 192 kHz for hi-res), buffer size, polyphony, etc. Existing zero-config callers keep working — `init()` without args = current defaults. Late callers (after auto-init) get a warning + ignored settings (silent re-init is too disruptive to already-playing sounds). Move the constexpr references in tcVideoPlayer_linux.cpp etc. to a runtime `getSampleRate()` accessor. Often implemented in the same PR as the real-time stream API entry above. | Medium | -| Streaming audio playback | New `Sound::loadStream(path)` (sibling of `Sound::load`) that keeps the underlying `ma_decoder` alive and reads from disk on demand via a worker-thread-fed ring buffer (~16 KB prefetch). Cuts memory cost for long files (multi-minute BGM, podcasts) from "full PCM in RAM" (e.g. ~80 MB for a 4-minute stereo track decoded to float) to a fixed-size prefetch. WAV / MP3 / FLAC ride miniaudio's incremental `ma_decoder_read_pcm_frames`; OGG path mirrors with stb_vorbis incremental reads. AAC stays on-memory (platform decoders buffer internally). Short SFX should keep using on-memory `load()` — streaming is opt-in. Seek requires re-fill of prefetch and ~10 ms blackout, acceptable for music. | Medium | -| Audio device hot-plug handling | Detect device disconnect (USB DAC unplugged etc.) via miniaudio's `ma_device_notification_proc`. On detection, auto-fail-over to the system default device, then fire `AudioEngine::audioDeviceChanged` with the new device's info. Listeners can override the fallback by calling `init(settings)` themselves. Without this, calling `init(settings)` to switch devices after a hot-unplug will hang in `ma_device_uninit` waiting for the dead device's audio thread to join. Likely also wants a `cause` enum on `AudioDeviceChangedArgs` (InitialInit / UserRequest / DeviceDisconnect) so listeners can distinguish event sources. | Medium | +| `LoadResult` error taxonomy enrichment | The API SHAPE shipped in v0.7: `trussc::LoadResult` (`LoadError` enum + message, `explicit operator bool()`) is returned by Image/Pixels/SoundBuffer/Sound/SoundStream/VideoPlayer/tcxHap/Font load APIs, with `fs::exists` pre-checks (FileNotFound), `stbi_failure_reason()`, and native error codes (OSStatus/HRESULT/ma_result) in messages. Note: `Shader` has no path-based load in core (nothing to convert). REMAINING (non-breaking): grow the enum (permission-denied, network, ...) and enrich per-decoder messages (AVFoundation NSError text, GStreamer detail, MF verbose HRESULT mapping) — per-domain audit of error sources can proceed incrementally. | Medium | +| Audio device hot-plug handling | Detect device disconnect (USB DAC unplugged etc.) via miniaudio's `ma_device_notification_proc`. On detection, auto-fail-over to the system default device, then fire `AudioEngine::audioDeviceChanged` with the new device's info (today that event only fires from `init()` — initial + user re-init — not from unplug). Listeners can override the fallback by calling `init(settings)` themselves. Without this, calling `init(settings)` to switch devices after a hot-unplug will hang in `ma_device_uninit` waiting for the dead device's audio thread to join. Likely also wants a `cause` enum on `AudioDeviceChangedArgs` (InitialInit / UserRequest / DeviceDisconnect) so listeners can distinguish event sources. | Medium | | Custom-shader textured quad path (sgl-integrated) | A TrussC-level drawing path that issues `sg_apply_pipeline` + `sg_apply_bindings` + `sg_draw` with a user-supplied shader, while staying ordered with respect to surrounding `sokol_gl` commands. Built on the existing `suspendSwapchainPass()` / `resumeSwapchainPass()` machinery (already used by FBO), plus a partial-flush of the default sgl context so a `drawRect → font/customShader → drawCircle` sequence renders in submission order. Unlocks: R8 / RG8 / non-RGBA texture formats (current sgl shader forces RGBA8 with `(255,255,255,A)` padding), font atlas R8 storage (see below), sensor-data visualization (depth/IR/grayscale with palette mapping), SDF text, mask compositing, future Path glyph rendering. Probably surfaces as `Texture::drawWith(Shader&, ...)` or similar on the TrussC `gpu/` side; sgl_gl_tc fork ideally stays untouched. | High | | Font atlas R8 storage | Drop font atlas from RGBA8 to R8 (4× memory reduction — e.g. ~4 MB → ~1 MB on a large CJK atlas). Currently blocked on the custom-shader path above: the sgl built-in shader returns `(R, 0, 0, 1)` from an R8 sample, which renders as solid red instead of `(color.rgb, glyph.a)`. Once that path lands, swap the atlas pixel format + add a tiny font-specific fragment shader doing `out = vec4(uColor.rgb, sample.r * uColor.a)`. No quality loss (glyphs were already grayscale alpha stored as `(255,255,255,A)`). Sokol R8 is native on all current backends (Metal / D3D11 / GL 3.3 / GLES3 / WebGL2 / WebGPU — GLES2 / WebGL1 already dropped). Pair with this PR cycle's tategaki work to make Japanese / CJK use cases cheap on RAM. | Low (once shader path exists) | -| Font line wrapping (`enableWrap` + `setMaxLineLength`) | Opt-in text wrapping that flows long strings inside a length budget, default off so existing `drawString` behavior is unchanged. Staged rollout, all sharing the same internal "text → break-opportunity list → placement" split so later phases only refine the second half. **Phase 1**: basic wrap for horizontal text — break at ASCII spaces and between CJK ideographs (simplified UAX #14). **Phase 2**: Latin hyphenation — insert a `-` when a word has to be split mid-run, no dictionary, just last-resort. **Phase 3**: CJK `kinsoku` (行頭/行末禁則) opt-in via `setHangingPunctuation(true)` or similar — prevent `、。」』)` etc. from starting a line, either by hanging past the edge (ぶら下げ) or pulling back into the previous line (追い込み). Use a small kinsoku character set, not a full table. **Phase 4**: reuse the same pipeline for vertical writing — `maxLineLength` then means column height, columns break right→left as already implemented, kinsoku tables carry over. Justification / Knuth-Plass / advanced Western typography stays out of scope. Differentiator versus other creative-coding frameworks (oF / Cinder / Processing) where wrap is either absent or western-only. Likely lives on its own branch (`feat/font-wrap`) after the tategaki PR lands so the diffs stay reviewable. | Medium | +| Mouse event bubbling (press/release/drag) — **v0.7.0, breaking** | Unify input propagation with the scroll path, which already bubbles ("return false to allow bubbling to parent", see `RectNode::onMouseScroll`). Today `dispatchMousePress` delivers to the front-most hit node ONLY — if its `fireMousePress` returns false the event silently dies (no ancestor walk, no occluded-sibling fallback), so press/release/drag and scroll follow different rules. Plan: when the hit node does not consume, walk the parent chain (localizing via `localizeMouse` per level, firing node + mod hooks), and the first consumer becomes `grabbedNode` for drag tracking. Keep `RectNode::onMousePress`'s `return true` (consume) default, so bubbling activates only where a handler explicitly returns false — a path that today just swallows the event, making the change near-compatible in practice but still semantically breaking (hence the 0.7.0 window). Kills the current anti-pattern where a child must know its parent to forward input (contradicts the "dependencies point downward only" rule) and unblocks drag-through-children cases (grab a window by its title bar across label children; drag-scroll a list over its buttons). Watch mouse enter/leave consistency along the chain. **Considered and deferred, revisit after bubbling lands:** flipping hit-testing to default-ON ("visible RectNode catches clicks", DOM/Unity-style, opt-out via `disableEvents()`). Only sane as a package with bubbling + consume-by-choice (make `RectNode::onMousePress` return whether a listener consumed instead of unconditional true); default-ON alone under single-target dispatch would make every decorative label/separator child swallow its parent widget's clicks. Perf is a non-issue either way: the hit-test walk already visits all active+visible nodes (Mat4 inverse + children snapshot per node) and `eventsEnabled_` only gates the final rect test. | Medium | +| Coroutine sequencing (`wait` / `tween` / `event` awaiters) | Write time-spanning procedures as procedures: `move(); wait(1); move();` instead of callback chains or hand-rolled state machines in `update()`. Two layers, cheapest first. **Lua (TrussSketch)**: Lua coroutines are native — the host only needs a scheduler that resumes yielded coroutines next frame / after N seconds; `wait(1)` = sugar over `coroutine.yield(1)`. Scratch-style sequencing lands in sketches within days of work. **C++20**: TrussC already requires C++20, so `co_await` is available — a `Task` type + frame-loop-driven awaiters (`co_await wait(1.0)`, `co_await tween(...)`, `co_await event(node.mousePressed)` = "suspend until clicked"). Big win for installation flow control (idle → attract → interact → reset). Ship Lua first, stabilize the C++ API shape against real sketch usage. Purely additive — no existing path changes. | Medium (Lua) / High (C++) | +| ScreenRecorder audio track | Record audio (app output and/or mic) into the video file alongside the frames. **The one item in this batch with real regression surface**: it modifies the working per-platform encode pipelines (AVFoundation / Media Foundation / ...) to mux an audio track with correct A/V sync — so it should ship as its own release, verified on real hardware per platform, not bundled with other work. Pairs naturally with the real-time audio events (`audioOut` already exposes the exact buffers to record). | Medium-High | +| Audio analysis: onset / beat / Mel bands | FFT already exists (`tcFFT.h`); layer the standard music-visualization trio on top: **onset detection** (spectral flux — the moment a drum hit / note attack happens), **beat tracking** (period estimation over the onset train → BPM + phase), **Mel-band energies** (perceptual frequency bands, the usual input for reactive visuals). Makes "visuals that react tightly to music" a one-liner — a workshop staple that currently requires hand-rolling. Additive on the existing FFT chain. | Medium | +| GPU compute (`tc::ComputeShader`) | The vendored sokol_gfx already supports compute passes (`sg_begin_pass({.compute=true})` + storage buffers/images + dispatch; sokol-shdc cross-compiles `@cs` shaders). Wrap it TrussC-style and ship a flagship GPU-particle example (1M particles). **Backend caveat**: compute works on Metal (macOS/iOS), D3D11 (Windows), GL/GLES3.1+ (Linux/Android), and **WebGPU only on web — WebGL2 is excluded**, so compute examples can't run in the current WebGL2-based web player (mark `webSupported: false`, or revisit when the player moves to WebGPU). Desktop/mobile are fully ready today. | Medium-High | +| Input record & replay (`tc::InputRecorder`) | Record timestamped input events (mouse / key / MIDI / OSC) to a file and replay them deterministically through the same event bus MCP injection already uses. Three uses for the price of one: **attract mode** for unattended exhibits (replay a canned interaction), **repro debugging** (capture the "happens sometimes" and take it home), **regression tests** (record an interaction once, assert on the result in CI or via MCP). Additive — replay enters through the existing injection path. | Medium | +| Frame vector export (SVG / PDF) | `beginVectorCapture("frame.svg")` records the frame's draw calls (rects, circles, paths, text-as-paths) as vectors instead of pixels. Serves the pen-plotter (AxiDraw) and print/generative-poster crowd — a real community that today stays on Processing for its PDF export; oF's ofxCairo path is effectively unmaintained. TrussC's thin unified draw API makes the interception layer tractable. Opt-in capture: zero cost when off (verify that). Raster-only features (shaders, textures) are out of scope or rasterized-on-import. | Medium-High | +| Kiosk / installation mode (`anchorbolt start`) | First-class answer to "museum runs an app for 3 months": `anchorbolt start` (see the fleet entry for the anchorbolt tool) supervises the app as a child process — watchdog auto-restart on crash/hang (via local `/health` heartbeat), log rotation, crash-report collection (.ips etc. + log tail attached to the restart notification), scheduled start/stop. **App side stays zero-config**: kiosk passes the log path (`Logger` already has `setLogFile()` + `Event onLog` for extra sinks) and rides the existing MCP HTTP server for `/health`. **Outbound sinks = ONE templated HTTP POST engine, not per-service adapters**: URL + body template + `{{app}}/{{event}}/{{msg}}/{{lines}}` variables; named presets prefill it (Uptime Kuma push, Discord/Slack webhook, VictoriaLogs/Loki ingestion, generic JSON POST). Two modes per sink: event notification (one-shot on error/restart/down) and log stream (batched). The genuinely hard part is the **offline spool**: venue networks drop for hours, so buffer to disk and re-send on recovery — that lives in the engine once, works for every sink. **Periodic thumbnails without the fps hitch**: today's `get_screenshot` does a full-res readback + synchronous PNG encode (tens of ms = a visible hiccup); the monitoring path instead blits the frame into a small FBO (~512px) first, reads back only that, and JPEG-encodes off the main thread — sub-ms on the frame loop, so a 60fps installation never stutters. Full-res PNG stays available on demand for debugging. **MCP tool passthrough**: kiosk relays tool calls to the app's MCP endpoint, so app-specific tools (registered via the existing `tool()` API — e.g. a venue `grab_webcam` snapshot) are reachable remotely with zero new mechanism; kiosk is transport, features stay in the app. **Supervision is two-layered**: kiosk itself owns the real watchdog logic (spawn app as child process, hang detection, restart policy — implemented once, cross-platform); who starts kiosk at boot is left to the user (launchd / systemd / startup entry of their choice) — a `kiosk install` convenience verb that writes those configs is deferred until demand shows up. **Core-side work is only three small additive pieces**: `get_thumbnail(width, quality)` standard MCP tool (small-FBO blit + off-thread JPEG — the one new mechanism), `get_health` standard tool (fps / uptime / frameCount), and a `TRUSSC_LOG_FILE` env var that auto-calls `setLogFile()` at startup so kiosk can inject the log path with zero app code. Everything else lives in anchorbolt — deliberately NOT in trusscli (see the fleet entry). | Medium | +| Fleet dashboard (**anchorbolt**) | The ops tool. Named *anchorbolt* — the bolt that fixes a structure to its foundation: it anchors a running installation to home base. ONE binary, two roles: `anchorbolt serve` (the server, VPS) and `anchorbolt start` (kiosk mode — the venue-side supervisor), plus admin verbs; both ends of the WS protocol live in one codebase (own repo), so version skew is structurally impossible. **Deliberately a separate tool from trusscli**: the dev CLI's charter is building projects and stays free of watch-and-phone-home behavior (the npm-vs-pm2 split — nannying is obnoxious in a builder, but it is the entire value of an ops agent); the audiences barely overlap (workshop beginners never meet anchorbolt, installation pros don't mind a second tool). The server side: once the kiosk JSON (heartbeat / log / thumbnail) has hardened in real use: a small self-hosted service that receives every installation's heartbeats, logs, and periodic thumbnails. **Architecture: monolithic, store-is-the-hub** (decided over a split broker + store: history is a hard requirement so a store always exists, and a separate broker adds a hop + an ops burden without removing any custom code; MCP passthrough is RPC and fits a persistent connection, not pub/sub). Each kiosk keeps ONE outbound WebSocket to the server (NAT-friendly, auto-registers with app-id + token on connect — no manual IP entry, venue IPs are meaningless behind NAT); the server appends to storage AND fans out to live viewers via in-process pub/sub, and exposes a WS/SSE subscribe endpoint so external consumers (extra viewers, a live AI session) can attach broker-style. MQTT can be added later as an optional ingest transport (server subscribes to a broker) without changing the design. **Storage: DB-free by design** — logs are append-only JSONL files rotated daily (`apps//logs/.jsonl`), thumbnails are timestamp-named JPEGs (the directory IS the index), registry is a JSON per app. At exhibit scale (a few MB/day/app) server-side file scan answers "ERROR ± 50 lines last night" at ripgrep speed; append-only files are crash-proof, migration-free, and readable with `less` when everything else is broken. SQLite (itself stb-class as a dependency — single amalgamation file, in-process) enters only later and only as an INDEX (FTS5 over long ranges, transactional metadata like incident notes / tokens), with files staying the source of truth. The headline is the **live thumbnail wall** — "what does every venue look like right now" at a glance, which generic monitoring (Uptime Kuma etc.) can't do by construction, and which replaces the physical camera people end up pointing at their own installations. The second headline: **the fleet server is itself an MCP server** (`search_logs`, `tail_logs(follow)`, `get_screenshot_history`, `restart_app`, plus passthrough to each app's own MCP tools) — so an AI assistant can investigate "the Osaka piece crashed last night" end-to-end: search the logs, pull thumbnails around the error, form a hypothesis, even live-debug the running app through the same node-tree/ImGui tools used in local development. **Requires auth + per-venue scope control from day one** (token, read-only vs full-control) — the passthrough surface can mutate app state. **Auth model**: two token classes — per-venue *agent* tokens (publish-only, zero read access, so a leaked venue token's blast radius is that venue's fake logs at worst; the token IS the identity, bound to app-id at mint time) and *operator* tokens with roles (viewer / operator / admin); tokens are 32-byte randoms stored hashed in the file registry (no JWT — instant revocation matters more than statelessness here). Mutating tools go through a **server-side approval queue**: the call blocks (with TTL) while a notification links the human to the dashboard's approve/deny page — same shape as Claude Code's permission prompt; approval interaction NEVER lives in chat apps (that path leads to per-service bot/OAuth swamps). Notifications stay **one-way webhook POSTs** (Discord / Slack / ntfy / generic presets over the kiosk sink engine), aggregated at fleet (flap dedup, "3 venues down = 1 message"); email/SMTP and interactive bots are permanently out of scope. **Deployment: per-project servers, UI-first admin** — token minting / member management happens in the dashboard (add-installation → pairing code → venue types `--pair 483201`); CLI admin verbs are optional sugar, maybe never built. Admin lockout recovery = shell access (`reset-admin` on the server, Grafana-style). Share URLs / groups ride the same token table (a share link is a viewer-scoped token in URL form + optional password + expiry) — v1.5. **Agent (AI) access**: fleet's `/mcp` is a plain HTTP MCP server; register it in Claude Code / claude.ai with an operator token; app tools are exposed via two proxy tools (`app_list_tools(app_id)` / `app_call(app_id, tool, args)`) to avoid tool-name explosion; a scheduled cloud agent can do morning patrols over the same endpoint. **v2 remote control**: input injection (mouse/key tools already exist in core) driven from a live-view panel — JPEG frames over the existing WS into an `` (both ends are ours, no MJPEG/HLS/WebRTC formality; upgrade path = H.264 fMP4 over WS reusing the ScreenRecorder hardware encoders when bandwidth matters; JPEG XS is LAN-mezzanine territory, wrong tool for WAN), plus a **remote ImGui panel as an HTML mirror**: `imgui_get_widgets` already enumerates widgets by label → render native HTML controls → send back `imgui_click` / `imgui_input` — structure over pixels, phone-friendly, zero app changes. Dashboard shell is HTML+JS (share-URL viewers are clients on phones; logs need browser-native text UX) — WASM has no earning role here. **Stack**: headless TrussC app (windowless + `setFps(0)` event-driven, both already proven by trusscli itself) + tcxCrow for HTTP/WS; TLS terminates at a reverse proxy (Caddy, or Cloudflare Tunnel for zero open ports). **Build order**: Phase 0 = thin vertical slice (kiosk spawns app + streams thumbnails → wall in browser, single shared token) to validate the WS pipeline and capture cost; Phase 1 = logs + agent tokens + watchdog; Phase 2 = sinks + MCP endpoint + approval queue; Phase 3 = share URLs / groups / remote control. Not a Kuma/VictoriaLogs replacement (use those meanwhile); this is vertical TrussC-only value. | Medium-High | +| Spring / smoothDamp utilities | `smoothDamp(current, target, velocity, smoothTime)` (critically damped, Unity-style) + a tiny spring type. "Follow the mouse with nice easing" is hand-rolled by every user today; two functions in tcMath end that. Zero risk, high daily-use value. | Low | ### Medium Priority | Feature | Description | Difficulty | |---------|-------------|------------| +| tcxLua build memory (luagen sharded output) | sol2's per-usertype template instantiation makes the single generated `trussc_generated.cpp` TU peak at multiple GB during compilation — OOM on RasPi 5 (8GB) and any low-memory machine (trusscli's memory-aware `-j` helps but one TU alone is the floor). Splitting **sol.hpp itself doesn't help** (parse is cheap; instantiation happens at the binding call sites), so make `luagen.js` emit N sharded TUs (`trussc_generated_00.cpp` …) each registering a subset of types plus a small hub that calls all `registerPartN(lua)` — peak memory drops ~1/N and the shards parallelize. sol2 usertype registration is runtime-only, so shards have no cross-TU coupling. Verify with bindcheck + sweep_examples. Optional companion: compile generated TUs at `-O1`. | Medium | +| Android soft keyboard + file dialog | Both need a **core-shipped default Java layer** (the APK pipeline already compiles `android/java/*.java` from the project and addons via javac+d8 — add a core-default java dir to that glob so every APK gets the helpers automatically). (1) **File dialog**: `tcFileDialog_android.cpp` is a stub today; implement via a small transparent helper Activity that fires `Intent.ACTION_OPEN_DOCUMENT`, receives `onActivityResult` (NativeActivity can't), resolves the `content://` URI through ContentResolver into the app cache dir, and returns the path over JNI — the Android counterpart of the iOS document picker. (2) **Soft keyboard**: `ANativeActivity_showSoftInput` is broken in the NDK (upstream sokol's own comment) and soft-IME text never arrives as key events; ship an invisible EditText (GameActivity-style) whose InputConnection forwards committed text over JNI into TrussC CHAR/key events — the Android counterpart of iOS's hidden UITextField bridge. Verified gaps on device (Pixel 8a) during the sokol_app_tc P5 port; both are TrussC platform-layer work, independent of the driver. | Medium | | VBO detail control | Dynamic vertex buffers | Medium | | Auto-growing per-frame uniform buffer | The Metal/WebGPU/Vulkan uniform ring is FIXED at `sg_setup` (4MB default ≈ 8k draw calls/frame, each apply 256-byte aligned); overflowing it in a release build silently corrupts uniforms (flipped / fully black frames — found via the suzuki-rain high-density bug). `WindowSettings::reserveUniformBuffer` (2026-07) covers known peaks up front, vector::reserve-style. The full fix is growing on overflow: Metal legally allows swapping the bound buffer mid-encoder (`setVertexBuffer` again), so on overflow a freshly allocated larger buffer can take over at offset 0 with ZERO dropped draws (already-recorded bindings keep reading the old buffer, which the command buffer retains) — worst case is one allocation hitch (~ms) on the grow frame, which a sufficient reservation avoids. Requires patching vendored sokol_gfx (`_sg_mtl_apply_uniforms` + commit path to re-create the inflight pair). Note: `sg_frame_stats` proved UNRELIABLE for pre-overflow detection — measured `size_apply_uniforms`/`num_apply_uniforms` did not match the ring's actual offset progression (crash at 6MB while stats estimated ~0.5MB); investigate that gap as part of this work. | Medium | | macOS deprecated API migration | Replace `tracksWithMediaType:` / `copyCGImageAtTime:` with async equivalents (deprecated in macOS 15.0) | Medium | @@ -38,6 +46,14 @@ | Dear ImGui 1.92.6 → latest | Currently on 1.92.6 WIP. 1.92.8 (2026-05-12) introduced a notable breaking change: `AddRect()` / `AddPolyline()` / `PathStroke()` swapped their `flags` and `thickness` arg positions. TrussC core does not touch these, but user code that does will need updates. Inline redirection keeps source compatible unless `IMGUI_DISABLE_OBSOLETE_FUNCTIONS` is on. Plan a single bump to the latest 1.92.x. | Low | +--- + +## Addon Candidates + +| Addon | Description | +|:------|:------------| +| tcxTimeline | Keyframe timeline editor for any `TC_REFLECT`ed property — the building blocks all exist in TrussC already (property enumeration via reflection, tcxImGui for the editor UI, JSON serialization for saving tracks). Spiritual successor to ofxTimeline / Duration (James George, developed with YCAM InterLab), which is effectively unmaintained — its users (installation / show-control / VJ work) have nowhere current to go. Also the natural showcase for the reflection system. | + --- ## oF Compatibility Gap diff --git a/docs/dev/multi-window-win-audit.md b/docs/dev/multi-window-win-audit.md new file mode 100644 index 00000000..149d6d0a --- /dev/null +++ b/docs/dev/multi-window-win-audit.md @@ -0,0 +1,287 @@ +# Multi-window Windows driver — cannibalization audit + +Audit of `origin/feat/multi-window-win` as **reference source** for a new Windows +secondary-window driver to be written into `core/include/sokol/sokol_app_tc.h`. +Not a merge — pieces are lifted/adapted/replaced individually. + +All file:line references below are into the branch file +`core/platform/win/tcWindowWin.cpp` (708 lines) unless noted. Read via +`git show origin/feat/multi-window-win:`. + +## Scope of the branch + +`git diff $(git merge-base HEAD origin/feat/multi-window-win)..origin/feat/multi-window-win --stat`: + +``` + core/include/tc/app/tcWindow.h | 25 +- (comments + TC_PLATFORMS("macos,windows") + stub #if guard) + core/platform/win/tcWindowWin.cpp | 708 ++++ (the whole driver — new file) + docs/reference/api-reference.toml | 12 +- (doc strings macOS -> macOS/Windows) +``` + +Key facts up front: + +- **Does NOT touch `sokol_app.h`, `sokol_impl.cpp`, or any sokol win TU.** It is a + standalone C++ file that plugs into the existing TrussC `Window`/`WindowContext` + abstraction and borrows sokol_app's already-created D3D11 device via + `sg_d3d11_device()` / `sg_d3d11_device_context()`. The new design integrates + into `sokol_app_tc.h` itself (a C header), so all of this C++/TrussC glue must be + re-expressed against the new per-window frame model. +- **No per-window dt story.** `tickWindow()` calls the *global* `beginFrame()` + (line 286) and global `present()` (295). There is no per-window EMA frame timer. + The new `sokol_app_tc.h` already has a per-window EMA timer (header line ~206), + so dt must come from there, not from this branch. +- **Only two "not-done" markers** (`git grep -i todo/fixme/hack` is clean): + - line 273: "Same lifecycle caveats as Fbo contexts on sgl resize" (advisory). + - **line 436: keycode table is a known gap** — see Event handling below. + +--- + +## 1. Window creation — `createWindow()` (637-704), `ensureWindowClass()` (552-564) + +- **Window class** (555-562): `WNDCLASSEXW`, class name `L"TrussCSecondaryWindow"` + (45), style `CS_HREDRAW | CS_VREDRAW | CS_OWNDC`, `IDC_ARROW` cursor, registered + once via a `static bool` guard (553). Straightforward. +- **Styles** (654): `WS_OVERLAPPEDWINDOW` if `settings.decorated` else `WS_POPUP`. +- **DPI handling** (669-677): creates at the requested logical size interpreted as + 96 DPI (`CW_USEDEFAULT` position), then reads the real per-monitor DPI with + `GetDpiForWindow`, computes `scale`, and resizes the CLIENT area to + `logical * scale` using `AdjustWindowRectExForDpi` + `SetWindowPos`. Assumes + sokol_app has already set per-monitor-v2 awareness (comment 656-657). Clean and + correct; the two-step create-then-resize is the standard per-monitor-v2 dance. +- `HWND` create at 658-661 stashes `nw` via `lpCreateParams`; retrieved in + `WM_NCCREATE` (460-463) into `GWLP_USERDATA`. + +**Rating: LIFT (logic) / ADAPT (packaging).** The Win32 sequence is correct and +directly reusable, but it lives inside a C++ `Window`/`shared_ptr` factory. In the +C header it becomes a `_sapp_tc` window-slot init function. Class registration, +style selection, and the DPI create-then-resize block port almost verbatim. + +--- + +## 2. Swapchain — `createSwapchain()` (181-219), `createRenderTargets()` (108-156), `resizeSwapchain()` (166-179), `acquireSecondarySwapchain()` (74-91) + +- **Factory acquisition** (186-192): `device->QueryInterface(IDXGIDevice)` → + `GetAdapter` → `adapter->GetParent(IDXGIFactory2)`. Reaches the *same* factory + that made sokol_app's device — correct approach, avoids a second `CreateDXGIFactory`. +- **Swapchain desc** (194-204): `DXGI_SWAP_CHAIN_DESC1`, format + `DXGI_FORMAT_R10G10B10A2_UNORM` (10-bit, matches the patched sokol_app main + window, const at 41), `SampleDesc.Count = 1` (flip model is always single-sample), + `BufferUsage = RENDER_TARGET_OUTPUT`, **`BufferCount = 2`**, + **`SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD`**, `Scaling = STRETCH`, + `AlphaMode = IGNORE`. `CreateSwapChainForHwnd` (206). +- **Alt-Enter suppression** (207): `factory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER)` + right after create — the correct flip-model gotcha handling. +- **MSAA approach** (133-143): flip-model swapchains can't be multisampled, so when + `sampleCount > 1` it creates a *separate* MSAA color texture + (`D3D11_STANDARD_MULTISAMPLE_PATTERN`) and hands sokol_gfx both a `render_view` + (MSAA) and `resolve_view` (backbuffer) via `sg_swapchain.d3d11` (83-88). Correct + and matches how sokol resolves. +- **Depth-stencil** (146-152): `D24_UNORM_S8_UINT`, drawable-sized, sample count + matched to color. Recreated on resize. +- **Backbuffer RTV** (114-121): `GetBuffer(0)` → `CreateRenderTargetView`. Comment + notes flip model keeps buffer 0 as the current back buffer every frame so the + view is reusable (persistent, not per-frame) — this is why + `acquireSecondarySwapchain` (74-91) is a trivial "hand back current views + size" + with no per-frame drawable acquisition (contrast Metal). Good insight to preserve. +- **Resize** (166-179): releases all views, then the critical flip-model gotcha + (170-175): **before `ResizeBuffers` you must release EVERY reference to the back + buffers, including the RTV still bound on sokol's shared immediate context** — + so it does `dc->OMSetRenderTargets(0, nullptr, nullptr); dc->Flush();` first. + Then `ResizeBuffers(0, w, h, kSwapFormat, 0)` and recreate targets. This + ordering is the single most valuable encoded gotcha in the file. + +**Rating:** +- Factory acquisition, desc, flip-discard/BufferCount=2, MakeWindowAssociation, + MSAA-side-texture + resolve wiring, depth-stencil, ResizeBuffers unbind/flush + ordering: **LIFT** — all correct D3D11, portable to C almost as-is. +- `BufferCount = 2` and swapchain-flags: **ADAPT.** The new design uses **DXGI + frame-latency waitable objects**, which requires + `DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT` on create, usually + `BufferCount = 2` or 3 with `SetMaximumFrameLatency(1)`, and passing that same + flag to every `ResizeBuffers` call (a common bug source: flag dropped on resize + → waitable handle invalidated). This branch passes `0` flags — must change. +- `acquireSecondarySwapchain` hook: **REPLACE** — it targets TrussC's + `WindowContext::acquireSwapchain` callback (C++), which does not exist in the + C-header world. The new driver returns/fills `sg_swapchain` directly at frame time. + +--- + +## 3. Tick model — `tickWindow()` (224-312), `SetTimer` (696-701), `monitorRefreshHz()` (94-105) + +- **Driver: `WM_TIMER`.** `SetTimer(hwnd, kTickTimerId, interval, nullptr)` at + 699-701 with `interval = 1000 / monitorRefreshHz(hwnd)` (integer ms). Fires + `WM_TIMER` (469-470) → `tickWindow`. Delivered on the main thread by sokol_app's + own message pump; the comment (696-698) leans on Windows auto-coalescing timers + to depth 1. +- **THIS IS THE PART BEING REPLACED.** `WM_TIMER` resolution is ~15.6 ms + (`USER_TIMER_MINIMUM` 10 ms, and the timer floor is the system tick), so it + effectively caps at ~64 Hz and jitters badly — it cannot hit 120/144 Hz displays + and is not vsync-locked. Integer `1000/hz` (699) also quantizes (e.g. 144 Hz → + 6 ms → 166 Hz nominal, then clamped by the timer floor). The whole point of the + redesign is to drive each window off a **DXGI frame-latency waitable object** + (`GetFrameLatencyWaitableObject` → `WaitForSingleObjectEx`) so the tick is + vsync-paced and jitter-free. +- **Integration with the main loop** (295, 303-304): renders the tree, then + `Present(0, 0)` with **SyncInterval 0** so the present never blocks the main + thread on this window's vblank (comment 300-303: a blocking wait would halve the + main window's rate). This "don't block on secondary present" principle is exactly + what the waitable-object model formalizes. +- **`monitorRefreshHz`** (94-105): `MonitorFromWindow` → `GetMonitorInfoW` → + `EnumDisplaySettingsW(ENUM_CURRENT_SETTINGS).dmDisplayFrequency`, fallback 60. + Still useful as a *fallback* dt estimate, but not as the pacing source. + +**Rating: REPLACE (the pacing) / keep the shape.** The per-window-tick-on-main-thread +*structure* (each window renders synchronously, present non-blocking) is exactly +what the new design keeps and what `sokol_app_tc.h` already assumes. But the +`SetTimer`/`WM_TIMER` mechanism is thrown out and replaced by the frame-latency +waitable object polled from the main loop. `monitorRefreshHz` survives only as a +fallback constant. + +--- + +## 4. Event handling — `wndProc()` (458-550) + input helpers (317-453) + +Messages handled: + +- Mouse buttons L/R/M down+up (473-478) → `mouseButtonEvent` (330-365): tracks a + `buttonMask`, `SetCapture`/`ReleaseCapture` on first-press/last-release (346, 355) + so drags survive leaving the client area. Maps to TrussC `MouseEventArgs` + + fires `events().mousePressed`, `app_->mousePressed`, and + `dispatchMousePressToTree`. +- `WM_MOUSEMOVE` (480-482) → `mouseMoveEvent` (367-409): arms `TrackMouseEvent` + (TME_LEAVE) once (372-377), computes delta, emits drag vs move depending on + `buttonMask`. +- `WM_MOUSELEAVE` (484-493): disarms tracking, parks cursor at (-1,-1) so the next + tick's `updateHoverState` clears hover. +- `WM_MOUSEWHEEL` (495-498) → `mouseScrollEvent` (411-428): converts screen→client, + `wheel = WHEEL_DELTA_WPARAM / WHEEL_DELTA`. +- `WM_KEYDOWN/SYSKEYDOWN/KEYUP/SYSKEYUP` (500-509) → `keyEvent` (430-453). SYS + variants `break` to `DefWindowProc` so Alt/F10 system handling survives (503, 508). + Repeat flag from `lParam bit 30` (502). +- `WM_SIZE` (511-526): non-minimized → `resizeSwapchain` + re-read DPI + + `syncRootSize` (fires `windowResized`). +- `WM_DPICHANGED` (528-537): adopts the suggested rect (per-monitor v2); next tick + picks up the new scale. +- `WM_CLOSE` (539-544): calls `owner->close()` (which deletes `nw` — comment warns + touch nothing after). +- `WM_DESTROY` (546-547): no-op. + +**Modifier keys** (`fillMods`, 317-322): `GetKeyState` for Shift/Ctrl/Alt/LWIN|RWIN. +**DPI→logical conversion** (`logicalPos`, 325-328): `px / (dpi/96)`. + +- **KEYCODE GAP (line 430-437, explicit).** `keyEvent` does `int key = vk;` — it + passes the raw **Win32 virtual-key code straight through as the TrussC/sokol + keycode**. The comment admits this only works because letters/digits happen to + share ASCII == VK == SAPP_KEYCODE; **the full `SAPP_KEYCODE` translation table is + deferred as "Phase 2 polish"**. So arrows, F-keys, punctuation, keypad, etc. are + wrong. This is the direct analogue of the gap macOS had. The new driver should + port sokol_app.h's existing `_sapp_win32_key()` VK→`SAPP_KEYCODE` table rather + than copy this shortcut. No text/char (`WM_CHAR`) handling either. + +**Rating:** +- Mouse (buttons/capture/move/drag/leave/wheel), DPI conversion, SYS-key + pass-through, WM_SIZE/WM_DPICHANGED handling: **ADAPT** — correct behavior, but + every handler writes into TrussC `WindowContext`/`CoreEvents`/Node-tree dispatch + (`internal::currentWindowCtx`, `win.events()...`, `win.dispatchMousePressToTree`). + In the C header these become `sapp_event` fills dispatched through the event + callback. The *message set* and the capture/track/leave logic transfer; the + *payload target* is rewritten. +- Keycode mapping: **REPLACE** — use sokol_app.h's VK→SAPP_KEYCODE table; add + `WM_CHAR` for text. + +--- + +## 5. Occlusion / minimize — `tickWindow()` (229-242, 304-311) + +- **Minimize** (231): `if (IsIconic(hwnd)) return;` — timer keeps firing, resumes + on restore. Fully idle while minimized. +- **Occlusion** (235-242, 304-311): after `Present(0,0)`, if it returns + `DXGI_STATUS_OCCLUDED` (305) sets `nw->occluded`; subsequent ticks cheaply poll + with `Present(0, DXGI_PRESENT_TEST)` (236) and skip all rendering until it + returns `S_OK`. Logs once each way. This is the D3D11 analogue of the mac + occlusion-skip and is genuinely good — a fully covered window costs one + present-test per tick, never stalls others. + +**Rating: LIFT (logic).** `IsIconic` skip and the `DXGI_PRESENT_TEST` occlusion +poll are the correct idioms and port directly. Note under the waitable-object +model you must still not *block* on the waitable for an occluded/minimized window +(or you'd stall) — keep the "test cheaply, skip render" path. + +--- + +## 6. How it plugs into TrussC — the C++ seam that must be re-expressed + +- Renders through TrussC's `Window` / `internal::WindowContext`: + `internal::currentWindowCtx` swap (269-270, 299), a per-window `sgl_context` + created lazily on first tick (274-282) with matching color/depth/sample formats, + `sgl_set_context` + `sgl_defaults` (283-284), global `beginFrame()` (286), + `win.events().update/draw/afterFrame` + `win.tickTree()` + `win.drawTreeNow()` + (288-293), then global `present()` (295) which does `sg_end_pass`+`sg_commit`. +- Swapchain handed to sokol via TrussC's `WindowContext::acquireSwapchain` + function-pointer hook (688-689) → `acquireSecondarySwapchain` (74-91). +- Teardown (`close()`, 583-614): `KillTimer`, clear `GWLP_USERDATA`, + `DestroyWindow`, release views + swapchain, `sgl_destroy_context`, then App + `exit()/cleanup()` and unregister from `attachedApps`. + +**Rating: REPLACE / ADAPT.** All of this is TrussC-C++-abstraction plumbing that +does not exist in `sokol_app_tc.h`. The new driver owns the window slot in the C +header and calls back out. What transfers conceptually: the per-window sgl context +lifecycle, format agreement across swapchain/sgl/MSAA/depth, and the teardown +ordering (kill tick source → clear userdata → destroy window → release GPU → free +slot). The `beginFrame()`/`present()` being *global* (single frame counter, single +pass bracket) is a limitation to fix in the new model — each window needs its own +frame timing (the header's EMA timer) and its own pass. + +--- + +## 7. D3D11 gotchas encoded in the file (the real value) + +1. **Flip-model `ResizeBuffers` ordering** (170-175): unbind RTV from the shared + immediate context (`OMSetRenderTargets(0,nullptr,nullptr)`) + `Flush()` before + `ResizeBuffers`, or it fails and the next present crashes. **Highest-value comment.** +2. **Flip-model + MSAA** (133-143, 83-88): swapchain is single-sample; MSAA is a + side texture that sokol resolves into the backbuffer via `resolve_view`. +3. **Alt-Enter suppression** (207): `MakeWindowAssociation(DXGI_MWA_NO_ALT_ENTER)`. +4. **Persistent backbuffer RTV** (112-113): flip model keeps buffer 0 current, so + the RTV is reusable frame-to-frame (no per-frame acquire). +5. **Non-blocking secondary present** (300-304): `Present(0,0)` SyncInterval 0 so a + secondary never gates the main window's frame rate. +6. **Shared device/context**: uses `sg_d3d11_device()` / + `sg_d3d11_device_context()` — one device, one immediate context, single-threaded + (174 comment). Any per-window work runs on that shared context; ordering matters. + +All six are LIFT-grade knowledge to carry into the new driver. + +--- + +## Rating summary + +| Piece | Rating | Note | +|---|---|---| +| Window class + style + per-monitor-v2 create/resize | LIFT logic / ADAPT packaging | port into C window-slot init | +| DXGI factory acquisition from sokol device | LIFT | verbatim | +| Swapchain desc (flip-discard, 10-bit, MakeWindowAssociation) | LIFT + **ADAPT** | must add FRAME_LATENCY_WAITABLE flag + carry flag through ResizeBuffers | +| MSAA side-texture + resolve wiring | LIFT | correct | +| Depth-stencil creation | LIFT | correct | +| ResizeBuffers unbind/flush ordering | **LIFT (keep verbatim)** | top gotcha | +| `acquireSecondarySwapchain` hook | REPLACE | fill `sg_swapchain` directly, no C++ callback | +| **WM_TIMER / SetTimer pacing** | **REPLACE** | → DXGI frame-latency waitable object | +| `monitorRefreshHz` | keep as fallback | not the pacing source | +| Mouse handlers (capture/track/leave/wheel/drag) | ADAPT | retarget payload to `sapp_event` | +| **Keycode `int key = vk`** | **REPLACE** | use sokol_app VK→SAPP_KEYCODE table; add WM_CHAR | +| Occlusion (PRESENT_TEST) + IsIconic skip | LIFT logic | keep non-blocking skip under waitable model | +| WindowContext/sgl/beginFrame/present plumbing | REPLACE/ADAPT | re-express against C header + per-window timer | +| Teardown ordering | ADAPT | same ordering, C-header resources | + +--- + +## Cross-checks + +- **Touches `sokol_app.h` / sokol win TU?** No. Standalone file, borrows the device + via `sg_d3d11_device()`. The new work lands *inside* `sokol_app_tc.h`. +- **Per-window dt story?** None. Global `beginFrame()`/`present()`, single frame + counter. The new `sokol_app_tc.h` already carries a per-window EMA frame timer + (header ~line 206) — that becomes the dt source; this branch offers nothing here. +- **Known-broken / deferred:** the keycode table (line 436, "Phase 2 polish") — the + only functional gap. No text input (`WM_CHAR`). No TODO/FIXME/hack strings + otherwise; the code is otherwise complete and self-describing. diff --git a/docs/dev/sapp-android-impl-spec.md b/docs/dev/sapp-android-impl-spec.md new file mode 100644 index 00000000..7737b909 --- /dev/null +++ b/docs/dev/sapp-android-impl-spec.md @@ -0,0 +1,794 @@ +# sokol_app.h Android (GLES3 / NativeActivity) Backend — Behavioral Specification + +Implementation contract for giving `sokol_app_tc.h` an Android main-window / +app-lifecycle backend (project "sokol_app graduation", **Android leg — P5, the last +driver port before `sokol_app.h` is deleted**). All line references are into +`core/include/sokol/sokol_app.h` (14602 lines, this worktree). Focus: `_SAPP_ANDROID` ++ **`SOKOL_GLES3`** — the only configuration TrussC ships on Android (no Metal/Vulkan; +see §11). There is no alternate 3D-API path in the Android backend (unlike iOS's dead +EAGL branch), so nothing here is "dead-but-kept" — the whole `_SAPP_ANDROID` body ports. + +This is the **sixth** sibling of `sapp-mac-impl-spec.md` (event-driven `[NSApp run]`), +`sapp-win32-impl-spec.md` (owned PeekMessage loop), `sapp-x11-impl-spec.md` (owned +XPending loop), `sapp-web-impl-spec.md` (no owned loop, browser rAF) and +`sapp-ios-impl-spec.md` (`UIApplicationMain` owns the loop). **Read the iOS spec first** +— it is the closest structural relative (single window, touch-only, GLES-family, +lifecycle SUSPENDED/RESUMED, posix/monotonic-family clock, port-time it preceded this +one). Android differs from iOS in FIVE structural ways, each load-bearing: + +1. **Entry point is inverted (`ANativeActivity_onCreate`, NOT `main`).** Android is the + ONE platform where `SOKOL_NO_ENTRY` is **forbidden** (compile error at 2352–2354). + sokol_app.h *exports* `ANativeActivity_onCreate()` (10967), which calls + `sokol_main(0, NULL)` to obtain the desc. `sapp_run()` is not supported. TrussC's + `main()`/`runApp` is reached *through* `sokol_main`, not the other way around (§1). +2. **A dedicated app thread, not the UI thread.** The entire backend (EGL, frame loop, + `init_cb`/`frame_cb`/`cleanup_cb`, input) runs on a **pthread spawned in + `onCreate`**, talking to Android's UI thread over a **message pipe + `ALooper` + + mutex/cond** (§2). TrussC's "main thread == app thread" assumption holds only because + TrussC code runs on *that* thread — but it is NOT Android's Java/UI thread. This is + the single biggest hazard in the port. +3. **EGL surface has an independent lifecycle.** The window/surface is created, + destroyed and recreated while the app object lives (background/foreground, screen + off). The full state is an **APP_CMD state machine** driven by NativeActivity + callbacks (§3). `init_cb` fires on the first frame *after* a surface exists; + `frame_cb` pauses whenever there is no surface or no focus. +4. **Touch + BACK key only.** No mouse/cursor/pointer-lock/clipboard-from-OS/drag&drop/ + window-title/icon/fullscreen-toggle surface (§10). Input = multi-touch via + `AInputQueue` + a single hardware key (BACK) that TrussC deliberately swallows. +5. **Orientation & immersive mode are done in TrussC via JNI, NOT via sokol.** Unlike + iOS (which pokes sokol's `_sapp_ios_immersive_mode` extern + a UIViewController + subclass), Android's `tcPlatform_android.cpp` reaches the `ANativeActivity` directly + (`ANativeActivity_setWindowFlags`, JNI `setRequestedOrientation`). So there is **no + sokol-side orientation/immersive patch to preserve** on Android (§8/§10). + +**Port target.** `sokol_app_tc.h` (11876 lines) is organised as **N independent, +self-contained per-platform copies** of the renamed (`_sapp` → `_sapp_tc`, +`_sapp_*` → `_sapp_tc_*`) sokol_app implementation. The top-level chain is: + +``` +#if defined(__APPLE__) // 198 (mac @202, iOS @1726 — DONE) +#elif defined(_WIN32) // 3670 +#elif defined(__linux__) && !defined(__ANDROID__) && !defined(__EMSCRIPTEN__) // 5650 +#elif defined(__EMSCRIPTEN__) // 8917 +#else /* other platforms */ // 11818 (stub) +#endif // 11850 +``` + +The Android backend slots in as a **new `#elif defined(__ANDROID__)` branch** anywhere +before the `#else` stub (the linux guard already excludes `__ANDROID__`, so ordering is +not a correctness issue; place it next to linux for readability). It is a **plain C++ +translation unit** — **no Objective-C++ `#error` guard** (contrast mac/iOS, which +enforce `.mm`). Each existing branch already carries the full public-API block with +`_SAPP_ANDROID` branches compiled-but-dead inside it (verify: `_sapp_tc.android.display` +at sokol_app_tc.h:3080/11245, `_sapp_tc.android.context` at 3091/11256, +`_sapp_tc_android_show_keyboard` at 3103/11268, `sapp_android_get_native_activity` at +3593–3597). The web/iOS ports lifted those blocks verbatim and the Android port reuses +them the same way — the port's job is to add the **Android-specific** struct + helpers + +`ANativeActivity_onCreate` and wire the already-present public branches to real +`_sapp_tc_android_*` functions. + +--- + +## 0. Global state touched (the shared contract) + +The single global is `static _sapp_t _sapp;` (~3298), which embeds `_sapp_android_t +android;` at **3311–3312** (under `_SAPP_ANDROID`). In the port this becomes +`static _sapp_tc_t _sapp_tc;` with `_sapp_tc.android`. + +### The Android state structs (3027–3074) +```c +typedef enum { // 3028–3037 — messages UI-thread → app-thread + _SOKOL_ANDROID_MSG_CREATE, _SOKOL_ANDROID_MSG_RESUME, _SOKOL_ANDROID_MSG_PAUSE, + _SOKOL_ANDROID_MSG_FOCUS, _SOKOL_ANDROID_MSG_NO_FOCUS, + _SOKOL_ANDROID_MSG_SET_NATIVE_WINDOW, _SOKOL_ANDROID_MSG_SET_INPUT_QUEUE, + _SOKOL_ANDROID_MSG_DESTROY, +} _sapp_android_msg_t; + +typedef struct { // 3039–3045 — the cross-thread pipe + lock + pthread_t thread; pthread_mutex_t mutex; pthread_cond_t cond; + int read_from_main_fd; int write_from_main_fd; // "main" = Android UI thread here +} _sapp_android_pt_t; + +typedef struct { ANativeWindow* window; AInputQueue* input; } _sapp_android_resources_t; // 3047–3050 + +typedef struct { // 3052–3072 + ANativeActivity* activity; + _sapp_android_pt_t pt; + _sapp_android_resources_t pending; // set by UI thread, consumed by app thread + _sapp_android_resources_t current; // owned by app thread + ALooper* looper; + bool is_thread_started, is_thread_stopping, is_thread_stopped; + bool has_created, has_resumed, has_focus; // the "should we render?" latches + EGLConfig config; EGLDisplay display; EGLContext context; EGLSurface surface; + #if __ANDROID_API__ >= 29 + AChoreographer* choreographer; // TrussC vsync-paced frame loop (§4) + bool frame_callback_in_flight; + #endif +} _sapp_android_t; +``` +Note the `pending`/`current` double-buffering of window+input: the UI thread writes +`pending` and posts a MSG; the app thread swaps `current = pending` under the mutex +(§3). `has_resumed && has_focus && surface!=EGL_NO_SURFACE` is the render gate +(`_sapp_android_should_update`, 10720). + +### Cross-platform `_sapp` fields the Android path reads/writes +`desc`, `valid`, `first_frame`, `init_called`, `cleanup_called`, +`window_width/height`, `framebuffer_width/height`, `dpi_scale`, `timing`, `frame_count`, +`event`, `gl.framebuffer` (10422), `skip_present` (10531), `onscreen_keyboard_shown` +(read-only via `sapp_keyboard_shown`; **never written on Android** — see §8 gap). +**NOT touched:** `fullscreen`, `mouse.*`, `clipboard.*`, `drop.*`, `keycodes[]`, +`custom_cursor_bound[]`, `window_title`, `quit_requested`/`quit_ordered` (all inert, §10). + +### desc defaults — GLES **3.1** context requested (3537–3543) +`_sapp_desc_defaults` under `SOKOL_GLES3`: `gl.major_version = 3`, and +`#if defined(_SAPP_ANDROID) || defined(_SAPP_LINUX) → minor_version = 1` (else 0). So +`_sapp_android_init_egl` (10372–10375) requests an **ES 3.1** context via +`EGL_CONTEXT_MAJOR/MINOR_VERSION = 3/1`. Manifest declares +`glEsVersion="0x00030000"` (ES3 minimum) — §11. + +### No ObjC / no ARC macros +Plain C++/NDK. Teardown is explicit EGL/pthread cleanup + `exit(0)` (§3); there is no +`_SAPP_OBJC_RELEASE`/`_SAPP_CLEAR_ARC_STRUCT` machinery in this backend. + +--- + +## 1. Entry / run flow — `ANativeActivity_onCreate` is the entry, `sokol_main` pulls the desc + +### Platform detection (2346–2354) +`#elif defined(__ANDROID__)` → `#define _SAPP_ANDROID (1)`; +`#if !defined(SOKOL_GLES3) #error(... must be SOKOL_GLES3)` (2349–2351); +**`#if defined(SOKOL_NO_ENTRY) #error("SOKOL_NO_ENTRY is not supported on Android")` +(2352–2354)** — the defining constraint of this platform. The port's `__ANDROID__` +branch must keep this guard; it is the ONLY tc.h branch where `SOKOL_NO_ENTRY` is absent. + +### Includes (2520–2531) +``, ``, ``, ``, +`` (**`// [TrussC] AConfiguration for display density`**, 2525), +``, `#if __ANDROID_API__>=29 ` (2527–2529), +``, ``. + +### The exported entry: `ANativeActivity_onCreate` (10967–11036) +Android's `NativeActivity` framework (manifest `android.app.lib_name`, §11) `dlopen`s +the app's `.so` and calls the exported `ANativeActivity_onCreate`. Sequence: +```c +JNIEXPORT void ANativeActivity_onCreate(ANativeActivity* activity, void* saved, size_t) { + _sapp_clear(&_sapp, sizeof(_sapp)); // 10977 + _sapp.android.activity = activity; // 10978 — set BEFORE sokol_main (issue #708) + sapp_desc desc = sokol_main(0, NULL); // 10979 — user desc (calls TrussC main(), §below) + _sapp_init_state(&desc); // 10980 — clears _sapp again! + _sapp.android.activity = activity; // 10981 — so re-set activity AFTER init_state + pipe(pipe_fd); … read/write_from_main_fd // 10983–10989 — the UI→app pipe + pthread_mutex_init / cond_init // 10991–10992 + pthread_create(&…thread, DETACHED, _sapp_android_loop, 0); // 10994–10998 — spawn app thread + // wait until app thread signals is_thread_started // 11001–11005 + _sapp_android_msg(_SOKOL_ANDROID_MSG_CREATE); wait has_created // 11008–11013 (EGL init) + activity->callbacks->onStart/onResume/onPause/onStop/onDestroy/ + onNativeWindowCreated/onNativeWindowDestroyed/ + onInputQueueCreated/onInputQueueDestroyed/onLowMemory = … // 11016–11031 + /* NOT A BUG: do NOT call sapp_discard_state() */ // 11035 +} +``` +`onCreate` runs **on the Android UI thread** and **returns** (it does NOT own a loop — +contrast iOS `UIApplicationMain`, which blocks). The loop lives on the spawned app +thread (§2). The double `activity=` (10978, 10981) is required because `_sapp_init_state` +zeroes `_sapp`; keep both. `onSaveInstanceState`/`onWindowFocusChanged`/`onConfigChanged` +handlers also exist (10849, 10856, 10923) but the last two window callbacks are commented +out in the registration block (11024–11025, 11029–11030 — resize/redraw/contentRect/ +configChanged are NOT wired; rotation is handled per-frame instead, §5). + +### The TrussC entry chain (verified, the crux of the port) +Android is the inverse of every other platform. `sokol_main` is **declared** by +sokol_app.h but **defined by TrussC**, in `core/platform/android/sokol_impl.cpp`: +```c +extern int main(); +namespace trussc { namespace internal { extern sapp_desc g_androidDesc; } } +sapp_desc sokol_main(int argc, char* argv[]) { + (void)argc; (void)argv; + main(); // runs the user's main() → runApp() stores desc + return trussc::internal::g_androidDesc; +} +``` +And `TrussC.h` (2642–2653) specialises `runApp` for `__ANDROID__`: +```c +#ifdef __ANDROID__ + inline sapp_desc g_androidDesc = {}; // 2644 (namespace internal) + template int runApp(const WindowSettings& s = {}) { + internal::g_androidDesc = buildAppDescriptor(s); // 2649 — store, DON'T run + return 0; // returns to sokol_main; NO sapp_run() call + } +#else … sapp_run(&desc) … // 2656–2658 (every other platform) +#endif +``` +So the full boot is: +**NativeActivity `dlopen` → `ANativeActivity_onCreate` (sokol_app_tc.h) → `sokol_main` +(TrussC `sokol_impl.cpp`) → user `int main()` → `runApp()` (stores `g_androidDesc`, +returns) → back in `sokol_main`, returns `g_androidDesc` → `onCreate` inits state, spawns +app thread, returns to UIKit-equivalent.** `TrussC.h:15` still `#define SOKOL_NO_ENTRY`, +but on Android that macro is **not passed to the sokol impl TU** — `sokol_impl.cpp` does +not define it (it can't; 2352), and `sokol_main`+`ANativeActivity_onCreate` provide the +entry. **The port must keep `ANativeActivity_onCreate` exported from sokol_app_tc.h's +`__ANDROID__` branch** (see §11 migration) — this is the one platform where the tc.h +branch supplies the process entry point rather than a `sapp_run`. + +--- + +## 2. Threading model — the app runs on a spawned pthread, NOT the UI thread + +**This is the section that contradicts "everything is on the main thread".** There are +two threads: + +- **Android UI thread** ("main" in sokol's fd names): runs `ANativeActivity_onCreate` + and every `activity->callbacks->on*` (onResume/onPause/onWindowFocusChanged/ + onNativeWindowCreated/…). These handlers do almost nothing except **post a message** + down the pipe and, for window/input/destroy, **block on a condvar** until the app + thread acknowledges (`_sapp_android_msg_set_native_window` 10877, `_set_input_queue` + 10900, `on_destroy` 10947 all `pthread_cond_wait` until `current==pending` / + `is_thread_stopped`). +- **App / loop thread** (`_sapp_android_loop`, 10753): the sokol "main thread". Owns the + `ALooper`, the EGL context/surface, runs `_sapp_frame` (`init_cb`/`frame_cb`/user + code), processes input, and handles the APP_CMD messages in `_sapp_android_main_cb` + (10629). **All TrussC user callbacks run here.** + +### The message pipe + ALooper (10753–10829, 10832–10836) +`_sapp_android_msg(msg)` (10832) = `write(write_from_main_fd, &msg, sizeof msg)` from the +UI thread. On the app thread, `ALooper_addFd(read_from_main_fd, …, _sapp_android_main_cb)` +(10758–10763) drives `_sapp_android_main_cb` (10629) which `read()`s the msg and switches +under `pthread_mutex_lock(&pt.mutex)` (10642), then `pthread_cond_broadcast` to release +the (possibly blocked) UI thread (10715). Input arrives on a *second* fd: the +`AInputQueue` is `AInputQueue_attachLooper(…, _sapp_android_input_cb)` (10695–10700) so +touch/key dispatch also lands on the app thread (10607). + +### Which callback on which thread +| Callback | Thread | +|---|---| +| `ANativeActivity_onCreate`, all `on*` NativeActivity callbacks | **UI thread** | +| `sokol_main` → TrussC `main()`/`runApp` | **UI thread** (inside onCreate, before app thread does anything TrussC-visible) | +| `_sapp_android_loop`, `_sapp_android_frame`, `_sapp_frame` (init/frame/cleanup cb), touch/key events, APP_CMD handling | **App thread** | + +### Hazards for TrussC +- **`sokol_main` (hence TrussC `main()`/`runApp`/`buildAppDescriptor`) runs on the UI + thread**, but every other TrussC callback runs on the app thread. Anything with + thread-affinity captured at `runApp` time (thread-local, `runOnMainThread` liveness + token — see MEMORY thread-safety notes) is captured on the *UI* thread, then the app + runs on a *different* thread. Verify TrussC's main-thread identity is established from + `setup()`/first frame (app thread), not from `runApp`. +- **JNI on the native activity**: `tcPlatform_android.cpp` calls JNI + (`setRequestedOrientation`, §8) via `activity->clazz`. JNI calls must be on a + JNI-attached thread. `sapp_android_get_native_activity()` is callable from any thread + (no `_sapp.valid` assert, 14582–14584), but JNI usage from the app thread must + `AttachCurrentThread` — confirm `tcPlatform_android.cpp` does its own attach (it + uses `activity->env` / `vm->AttachCurrentThread`; the port doesn't change this but the + spec flags it as a live cross-thread concern). +- The mutex/cond/pipe are **process-global lifetime**; `on_destroy` tears them down + after the app thread confirms stop, then `exit(0)` (§3). + +--- + +## 3. EGL surface lifecycle + APP_CMD state machine + +The window surface is not permanent. `_sapp_android_main_cb` (10629–10718) is the state +machine; every case runs on the app thread under the pt mutex. + +### The messages +- **`MSG_CREATE` (10644–10653):** `_sapp_android_init_egl()` (display+context, NO + surface yet, 10322–10386), `_sapp.valid = true`, `has_created = true`. Sent once from + `onCreate` (11009). Context lives for the whole process; surface comes later. +- **`MSG_RESUME` (10654–10658):** `has_resumed = true`; fire + `SAPP_EVENTTYPE_RESUMED` via `_sapp_android_app_event` (10437, gated by + `_sapp_events_enabled`). +- **`MSG_PAUSE` (10659–10663):** `has_resumed = false`; fire `SAPP_EVENTTYPE_SUSPENDED`. +- **`MSG_FOCUS` / `MSG_NO_FOCUS` (10664–10671):** toggle `has_focus`. No event fired. +- **`MSG_SET_NATIVE_WINDOW` (10672–10687):** the **surface** transition. If + `current.window != pending.window`: destroy the old EGL surface + (`_sapp_android_cleanup_egl_surface`, 10426) if any, then if the new window is non-NULL + `_sapp_android_init_egl_surface(pending.window)` (10404 — `eglCreateWindowSurface` + + `eglMakeCurrent` + read `gl.framebuffer`) and `_sapp_android_update_dimensions(…, true)` + (§5). On surface-init failure → `_sapp_android_shutdown()`. Then `current.window = + pending.window`. +- **`MSG_SET_INPUT_QUEUE` (10688–10704):** detach the old `AInputQueue` from the looper, + attach the new one with `_sapp_android_input_cb` (§6). +- **`MSG_DESTROY` (10705–10710):** `_sapp_android_cleanup()` (calls `cleanup_cb` if a + surface still exists, then destroys the EGL context, 10505–10514), `_sapp.valid=false`, + `is_thread_stopping=true` (breaks the loop). + +### NativeActivity callbacks → messages (10838–10932) +`onStart`→(log only), `onResume`→RESUME, `onPause`→PAUSE, `onStop`→(log only), +`onWindowFocusChanged`→FOCUS/NO_FOCUS, `onNativeWindowCreated`→SET_NATIVE_WINDOW(window), +`onNativeWindowDestroyed`→SET_NATIVE_WINDOW(NULL), `onInputQueueCreated/Destroyed`→ +SET_INPUT_QUEUE(queue/NULL), `onLowMemory`→(log only), `onConfigurationChanged`→(log +only; NOT registered), `onSaveInstanceState`→returns NULL/0. + +### The render gate + when init_cb fires +`_sapp_android_should_update()` (10720–10724) = `has_resumed && has_focus && surface != +EGL_NO_SURFACE`. The frame loop only calls `_sapp_android_frame` when this is true. The +**first** `_sapp_frame()` (10529) — which fires `init_cb` (TrussC `setup()`) on the very +first frame — therefore happens only once a surface exists AND the activity is +resumed+focused, i.e. after MSG_CREATE + MSG_SET_NATIVE_WINDOW + MSG_RESUME + +MSG_FOCUS. Background/foreground cycles destroy+recreate the surface and pause+resume the +frame loop, but `init_cb` fires **once** (guarded inside `_sapp_frame`); `cleanup_cb` +fires from `_sapp_android_cleanup` only while a surface is still bound (10508). Typical +minimise/restore = SET_NATIVE_WINDOW(NULL)+PAUSE+NO_FOCUS → surface destroyed, frames +stop → FOCUS+RESUME+SET_NATIVE_WINDOW(window) → surface recreated, frames resume, RESIZED +fires (10500). + +### `on_destroy` + `exit(0)` (10934–10965) +UI thread posts MSG_DESTROY, blocks until `is_thread_stopped`, destroys mutex/cond, +`close()`s both fds, then **`exit(0)`** (10964) — a deliberate hard reset so static +globals are cleared for a clean relaunch. Port note: this bypasses normal C++ static +dtors — consistent with TrussC's "leak GPU singletons at exit" gotcha (MEMORY); do not +add teardown that relies on running after `exit(0)`. + +### SUSPENDED/RESUMED are fired but TrussC drops them +`TrussC.h`'s `_event_cb` switch (2253–2503) handles KEY/MOUSE/TOUCHES/RESIZED/ +FILES_DROPPED/CLIPBOARD_PASTED/QUIT_REQUESTED but has **no `case SAPP_EVENTTYPE_SUSPENDED` +/ `RESUMED`** — identical gap to iOS (§3 of the iOS spec). The events still fire at the +sokol layer (harmless); the port must keep firing them but need not surface them. + +--- + +## 4. Frame loop — Choreographer (TrussC patch) with a poll-loop fallback + +`_sapp_android_loop` (10753–10829) is the owned loop on the app thread. There are **two +frame-driving paths**; the Choreographer path is a **TrussC patch** (upstream sokol_app +has no Choreographer integration — verify: log items `ANDROID_CHOREOGRAPHER_ENABLED` / +`ANDROID_CHOREOGRAPHER_UNAVAILABLE` at 1803–1804, `AChoreographer* choreographer` + +`frame_callback_in_flight` in the struct at 3068–3071, all `#if __ANDROID_API__ >= 29`). + +### Path A — Choreographer (API ≥ 29, the normal case) 10765–10797, 10726–10741 +At loop start `choreographer = AChoreographer_getInstance()` (10766); log ENABLED/ +UNAVAILABLE. Each outer iteration: if `!frame_callback_in_flight && should_update()`, +`AChoreographer_postFrameCallback64(choreographer, _sapp_android_frame_callback, NULL)` +and set `frame_callback_in_flight=true` (10789–10792), then `ALooper_pollOnce(-1, …)` +blocks until the next event (10795). The vsync callback `_sapp_android_frame_callback` +(10727–10740, `#if __ANDROID_API__>=29`) clears the in-flight flag, bails if +`is_thread_stopping`, and if `should_update()` **re-posts the next callback** and calls +`_sapp_android_frame((double)frame_time_nanos / 1e9)` (10738) — the choreographer +timestamp feeds `_sapp_timing_update` as the external clock. This is vsync-paced. + +### Path B — poll-loop fallback (API < 29, or `choreographer==NULL`) 10799–10809 +If no choreographer: `if (should_update()) _sapp_android_frame(0.0)` (0.0 → sokol uses +its own `CLOCK_MONOTONIC` clock, §below), then drain events with +`ALooper_pollOnce(block_until_event ? -1 : 0, …)` in a `while (process_events && +!is_thread_stopping)` (10804–10809). `block_until_event = !should_update()` — when +nothing to render (backgrounded), block indefinitely on the looper; otherwise poll +non-blocking and spin frames. Both paths break when `is_thread_stopping`. + +### `_sapp_android_frame(double external_now)` (10523–10533) +```c +SOKOL_ASSERT(display/context/surface valid); +_sapp_timing_update(&_sapp.timing, external_now); // choreographer ts, or 0→monotonic +_sapp_android_update_dimensions(_sapp.android.current.window, false); // §5, every frame +_sapp_frame(); // init_cb (first) then frame_cb +// Modified by tettou771 for TrussC: skip present support 10530 +if (_sapp.skip_present) { _sapp.skip_present = false; return; } // 10531 — TrussC patch +eglSwapBuffers(_sapp.android.display, _sapp.android.surface); // 10532 +``` +The **`skip_present` early-return (10530–10531)** is TrussC mod §skip_present +(TRUSSC_MODIFICATIONS.md line 43 lists "Android EGL" among the backends patched) — lets +`sapp_skip_present()` (14597–14599) suppress the next `eglSwapBuffers`. Preserve it. + +### Clock — CLOCK_MONOTONIC (posix branch) +Android uses the shared posix timestamp path (2586–2596 init, 2635–2639 now): +`_SAPP_CLOCK_MONOTONIC` = `CLOCK_MONOTONIC`, `clock_gettime` in ns. Live (unlike web, +which asserts false). Fed by `_sapp_timing_update` when `external_now==0`; when the +choreographer supplies `frame_time_nanos`, that is used as the external now instead. +`sapp_frame_duration()` uses the platform-agnostic EMA-filtered `_sapp.timing`; there is +no Android-specific `sapp_frame_duration` branch (contrast iOS's CADisplayLink timer). + +--- + +## 5. Dimensions, DPI, rotation (`_sapp_android_update_dimensions`, 10444–10503) + +Runs **every frame** from `_sapp_android_frame`, and once with `force_update=true` on +surface (re)creation (10680). This is how rotation and resize are picked up — there is no +dedicated resize/rotate callback (the NativeActivity resize/config callbacks are left +unregistered, 11024–11030; the manifest declares `configChanges="orientation|screenSize| +screenLayout|keyboardHidden"` so the activity is NOT recreated on rotation — §11). + +```c +win_w = ANativeWindow_getWidth(window); win_h = ANativeWindow_getHeight(window); // 10450 +win_changed = (win_w != window_width) || (win_h != window_height); +window_width = win_w; window_height = win_h; +if (win_changed || force_update) { + if (!desc.high_dpi) { // 10457 — half-res buffer + ANativeWindow_setBuffersGeometry(window, win_w/2, win_h/2, format); // 10468 + // NOTE (10463): setting geometry == window size causes display artefacts, + // so only call it when buffer geometry differs from window size. + } +} +eglQuerySurface(EGL_WIDTH/HEIGHT) → fb_w, fb_h; // 10474–10476 +framebuffer_width = fb_w; framebuffer_height = fb_h; +``` + +### DPI via `AConfiguration_getDensity` (10482–10497) — TrussC patch §8 +Upstream computes `dpi_scale = fb_w/win_w`. TrussC replaces it: +```c +if (_sapp.android.activity) { + AConfiguration* config = AConfiguration_new(); + AConfiguration_fromAssetManager(config, activity->assetManager); + int32_t density = AConfiguration_getDensity(config); + AConfiguration_delete(config); + if (density > 0) _sapp.dpi_scale = (float)density / 160.0f; // mdpi baseline + else _sapp.dpi_scale = (float)fb_w / (float)win_w; // fallback +} else _sapp.dpi_scale = (float)fb_w / (float)win_w; // fallback +``` +`density/160` matches macOS/Windows semantics (160dpi = scale 1.0). Preserve the +fb/win-ratio fallback for both the no-activity and density≤0 cases. `sapp_dpi_scale()` +(14060) returns this; `tcPlatform_android.cpp:22 dpiScale()` forwards it. + +### RESIZED (10498–10502) +`if ((win_changed || fb_changed || force_update) && !_sapp.first_frame) +_sapp_android_app_event(SAPP_EVENTTYPE_RESIZED)`. Suppressed on `first_frame`; gated by +`_sapp_events_enabled`. TrussC's `_event_cb` handles RESIZED at TrussC.h:2463. + +--- + +## 6. Touch input (`_sapp_android_touch_event`, 10535–10587) + +Dispatched from `_sapp_android_input_cb` (10607–10627) on the app thread: for each event +pulled from the `AInputQueue`, `AInputQueue_preDispatchEvent` (IME pre-dispatch) is +honoured, then `_sapp_android_touch_event(e) || _sapp_android_key_event(e)` decides +`handled`, and `AInputQueue_finishEvent(…, handled)` acks it. + +```c +if (AInputEvent_getType(e) != AINPUT_EVENT_TYPE_MOTION) return false; // 10536 +if (!_sapp_events_enabled()) return false; +action = AMotionEvent_getAction(e) & AMOTION_EVENT_ACTION_MASK; +// DOWN/POINTER_DOWN → TOUCHES_BEGAN; MOVE → TOUCHES_MOVED; +// UP/POINTER_UP → TOUCHES_ENDED; CANCEL → TOUCHES_CANCELLED // 10545–10562 +_sapp_init_event(type); +num_touches = min(AMotionEvent_getPointerCount(e), SAPP_MAX_TOUCHPOINTS); // 10568–10571 +for i in num_touches: + dst->identifier = AMotionEvent_getPointerId(e, i); // 10574 stable id + dst->pos_x = (AMotionEvent_getX(e,i) / window_width) * framebuffer_width; // 10575 + dst->pos_y = (AMotionEvent_getY(e,i) / window_height) * framebuffer_height; // 10576 → fb space + dst->android_tooltype = (sapp_android_tooltype)AMotionEvent_getToolType(e,i); // 10577 + dst->changed = (POINTER_DOWN/UP) ? (i == pointer_index) : true; // 10578–10583 +_sapp_call_event(&_sapp.event); // 10585 +``` +- **All active pointers** are emitted each event; `changed` marks which pointer this + event is about (for POINTER_DOWN/UP only the indexed one changed; DOWN/MOVE/UP/CANCEL + mark all changed). Same `identifier`+`changed` contract as iOS — TrussC's touch + tracking in `_event_cb` (TrussC.h:2392–2440, maps to `TouchEventArgs`, `cancelled` + from CANCELLED) relies on it. **Preserve.** +- `android_tooltype` (finger/stylus/mouse/eraser; enum `sapp_android_tooltype` 1550–1555, + field `touchpoint.android_tooltype` 1570) is **upstream sokol** (not a TrussC patch) — + carry it through. +- Positions are already framebuffer-space (multiplied by fb/window ratio), unlike iOS + which multiplies by `dpi_scale`. Same net effect (fb pixels). + +--- + +## 7. Key input — BACK key consumed, no shutdown (`_sapp_android_key_event`, 10589–10605) + +There is **no general keycode table** on Android in this backend — the only key handled +is BACK, and TrussC deliberately swallows it: +```c +if (AInputEvent_getType(e) != AINPUT_EVENT_TYPE_KEY) return false; +if (AKeyEvent_getKeyCode(e) == AKEYCODE_BACK) { + /* [TrussC tettou771] Patched: BACK key no longer triggers shutdown. (10594–10601) + Upstream calls _sapp_android_shutdown() here, which destroys the EGL surface and + invokes cleanup_cb. Under screen pinning the OS blocks ANativeActivity_finish(), + so the process keeps running but the EGL context is gone — app appears frozen. + Consume without shutdown. Long-term fix: SAPP_EVENTTYPE_QUIT_REQUESTED for Android. */ + return true; // consume, do NOT shut down +} +return false; // all other keys unhandled → OS default +``` +This is TRUSSC_MODIFICATIONS.md **§8a (Android BACK Key No Shutdown)** — kiosk / +lock-task / screen-pinning deployments depend on it (the user's Sony XP / signage apps). +**Preserve verbatim.** Upstream's version calls `_sapp_android_shutdown()` (10516) here; +the port must NOT reintroduce that. No CHAR events are produced from hardware keys; text +comes from the soft keyboard indirectly (§8) but this backend does not bridge IME text to +`SAPP_EVENTTYPE_CHAR` (contrast iOS's UITextField bridge) — key/char input is effectively +BACK-consume only. + +--- + +## 8. On-screen keyboard (`_sapp_android_show_keyboard`, 10743–10751) + +```c +void _sapp_android_show_keyboard(bool shown) { + SOKOL_ASSERT(_sapp.valid); + if (shown) ANativeActivity_showSoftInput(activity, ANATIVEACTIVITY_SHOW_SOFT_INPUT_FORCED); + else ANativeActivity_hideSoftInput(activity, ANATIVEACTIVITY_HIDE_SOFT_INPUT_NOT_ALWAYS); +} +``` +Comment (10745) notes the NDK soft-input API is flaky. `sapp_show_keyboard(bool)` +(14086–14094) routes `#elif defined(_SAPP_ANDROID) → _sapp_android_show_keyboard` (14089). +Consumer: `addons/tcxImGui/src/sokol_imgui.h` toggles it from `io.WantTextInput` (same +one consumer as iOS). + +> **⚠ Gap (finding):** `sapp_keyboard_shown()` (14096–14098) returns +> `_sapp.onscreen_keyboard_shown`, but **nothing on the Android path ever sets that +> flag** (there is no IME text-change delegate as on iOS). So `sapp_keyboard_shown()` +> always returns `false` on Android, and tcxImGui's `if (WantTextInput && +> !sapp_keyboard_shown()) sapp_show_keyboard(true)` will re-issue `show` every frame while +> a text field is focused. Harmless (the OS ignores redundant shows) but worth noting; +> the port does not need to fix it but must not regress the flakier behaviour. + +### Orientation / immersive mode are NOT sokol's job on Android +Unlike iOS (§8 of the iOS spec: `_sapp_ios_immersive_mode` extern + +`sapp_ios_set_supported_orientations` + UIViewController subclass), Android does all of +this in `tcPlatform_android.cpp` **directly against the NativeActivity**, not through +sokol: +- `setImmersiveMode(bool)` (tcPlatform_android.cpp:29–38): `ANativeActivity_setWindowFlags` + `FLAG_FULLSCREEN (0x0400)` on/off. +- `setKeepScreenOn` (125–131): `FLAG_KEEP_SCREEN_ON (0x0080)`. +- `setOrientation(Orientation)` (143–184): JNI `Activity.setRequestedOrientation(int)` + via `activity->clazz` / `GetMethodID`. +- `getDeviceOrientation` (410), sensor/window queries — all via JNI. + +The only sokol entry these use is `sapp_android_get_native_activity()` (§9). **There is +no sokol-side orientation/immersive patch to preserve** — a real divergence from iOS the +port must not "helpfully" add. + +--- + +## 9. Public API: EGL getters, native activity, environment/swapchain (GLES3) + +### `sapp_egl_get_display` / `sapp_egl_get_context` (14064–14084) +`#if defined(_SAPP_ANDROID) return _sapp.android.display / .context`. Asserts +`_sapp.valid`. (Also a `_SAPP_LINUX && _SAPP_EGL` branch.) These feed sokol_gfx's GLES3 +context binding. + +### `sapp_android_get_native_activity()` (14582–14590) — the one TrussC-consumed getter +```c +// _sapp.valid NOT asserted — callable from within sokol_main() (issue #708) 14583–14584 +#if defined(_SAPP_ANDROID) return (void*)_sapp.android.activity; #else return 0; #endif +``` +**Sole consumer:** `tcPlatform_android.cpp` (dpiScale, setImmersiveMode, setKeepScreenOn, +setOrientation, getDeviceOrientation, `getActivityClass`, JNI env — lines 30, 125, 144, +221, 461, 477). No other Android platform TU touches sokol +(`tcFileDialog_android.cpp`, `tcFbo_android.cpp`, `tcVideoRecorder_android.cpp`, +`tcSound_android.cpp` etc. use JNI/AAudio directly, not sapp). **Keep this getter with +its no-valid-assert contract** — it must be callable from `sokol_main` (§1) and from the +app thread. + +### GLES3 environment / swapchain +Android has no Metal `sapp_get_environment`/`sapp_get_swapchain` device fields to fill; +the GLES3 path uses `glGetIntegerv(GL_FRAMEBUFFER_BINDING, &_sapp.gl.framebuffer)` read in +`_sapp_android_init_egl_surface` (10422) and consumed by `sglue_swapchain()`/sokol_glue's +GL branch (`_sapp.gl.framebuffer` is the default FBO name). `sapp_width()/height()` return +`framebuffer_width/height`; `sapp_color_format`/`depth_format` fall to the GLES3 defaults +(RGBA8 / DEPTH_STENCIL from the EGL config: RGB8 + `EGL_DEPTH_SIZE 16`, +`EGL_STENCIL_SIZE 0`, 10341–10342). No RGB10A2 / MSAA-texture machinery (contrast iOS). + +--- + +## 10. Features NOT applicable on Android (explicitly not-ported) + +| Feature | Android reality | +|---|---| +| **Mouse / cursor / pointer-lock** | No pointer model. `sapp_show_mouse`/`lock_mouse`/`set_mouse_cursor` have no `_SAPP_ANDROID` branch → inert. (A mouse *device* surfaces only as a touch with `android_tooltype == MOUSE`.) | +| **Clipboard** | No `_SAPP_ANDROID` branch in `sapp_set/get_clipboard_string`. Returns `""`. No `CLIPBOARD_PASTED`. | +| **Drag & drop** | No Android drop path. `enable_dragndrop` inert. File access is via `tcFileDialog_android.cpp` (JNI intents), not sokol. | +| **Window title / icon** | `sapp_set_window_title` / `sapp_set_icon` are `_SAPP_MACOS`-only → no-op. Label/icon come from the APK manifest + res (§11). | +| **Fullscreen toggle** | `sapp_toggle_fullscreen` (14104–14114) has no Android branch → no-op; `_sapp.fullscreen` unused. Immersive mode (JNI, §8) is the analogue and lives in TrussC, not sokol. | +| **Quit** | No `sapp_request_quit`→OS route. Upstream had BACK→shutdown; TrussC removed it (§7). App exit is OS-driven (task-swipe → onDestroy → `exit(0)`, §3). `SAPP_EVENTTYPE_QUIT_REQUESTED` is the documented long-term fix but is NOT implemented for Android. | +| **Orientation / immersive** | Done in TrussC via JNI/`setWindowFlags`, **not** sokol (§8). No sokol patch. | +| **`skip_present`** | Supported (TrussC patch, §4, 10530–10531). | +| **Secondary windows** | Single `ANativeWindow` bound to the activity. `sapp_create_window()` / plural-window getters → invalid handle + log (like web/iOS). Map window #0 to `_sapp.android.current.window`. | +| **Alternate 3D API** | GLES3 only. No Metal/Vulkan/EAGL branch exists in the Android backend — nothing dead to skip (contrast iOS). | +| **SUSPENDED / RESUMED dispatch** | Fired by sokol (§3) but dropped by TrussC's `_event_cb` (no case). Keep firing. | + +--- + +## 11. TrussC-side wiring (verified against the repo) + +**THE MIGRATION IN ONE LINE:** `core/platform/android/sokol_impl.cpp` currently does +`#define SOKOL_IMPL` + `#include "sokol_app.h"` (full upstream impl, incl. its +`ANativeActivity_onCreate`). The Android leg = give `sokol_app_tc.h` an `#elif +defined(__ANDROID__)` backend that **exports `ANativeActivity_onCreate`**, then switch +this TU to the decls-only + `SOKOL_APP_TC_IMPL` shim used by every desktop TU. + +### The migration shape (mirror mac/win/linux desktop TUs) +Desktop `sokol_impl.*` (verified mac/win/linux) use: +```c +#include "sokol_app.h" // declarations only (NO SOKOL_IMPL) +#define SOKOL_IMPL +#define SOKOL_APP_TC_IMPL +#include "sokol_app_tc.h" // the real _sapp_tc implementation +``` +Android's `sokol_impl.cpp` must adopt the same pattern **but must NOT define +`SOKOL_NO_ENTRY`** (2352 errors on it; desktop TUs define it to suppress sokol's `main`). +The Android tc.h branch supplies `ANativeActivity_onCreate` instead of a `main`. The +existing `sokol_main` definition + `extern int main()` bridge in `sokol_impl.cpp` (§1) +**stays** — sokol_app_tc.h only *declares* `sokol_main`. +- Current header comment already documents the constraint: *"SOKOL_NO_ENTRY is NOT + defined on Android. sokol_app.h provides ANativeActivity_onCreate() … sapp_run() is NOT + supported."* Keep and update it to say `sokol_app_tc.h`. + +### `TrussC.h` +- `#define SOKOL_NO_ENTRY` (15) — but not passed to the Android sokol impl TU (§1). +- `runApp` `__ANDROID__` specialisation stores `g_androidDesc` and returns without + `sapp_run` (2642–2653); `sokol_main` returns it. +- `_event_cb` (2253–2503) handles TOUCHES (2392–2440) + RESIZED (2463); **no + SUSPENDED/RESUMED** (§3). + +### Build system +- `core/CMakeLists.txt`: `elseif(ANDROID)` → `TC_PLATFORM_ANDROID`, backend `GLES3` + (56–60); `SOKOL_GLES3` defined PUBLIC (193, 308, 318 — `elseif(TC_PLATFORM_ANDROID)`). + No Metal/Vulkan anywhere on Android. +- `core/cmake/trussc_app.cmake`: `if(ANDROID)` builds a `.so`, then packages an APK + (736+); manifest from `core/resources/android/AndroidManifest.xml.in` (or project/addon + override, 764–771); collects `android/java` + `android/res` from the app and every + addon (780–811). Hot-reload forced off on Android (156–161). +- **API level 26 hardcode (verified):** `AndroidManifest.xml.in:7` + `android:minSdkVersion="26" targetSdkVersion="35"`; `tools/src/ProjectGenerator.cpp:414` + `androidPreset["cacheVariables"]["ANDROID_PLATFORM"] = "android-26"`. So the compile + `__ANDROID_API__` is 26 by default → **the `>= 29` Choreographer path (§4) and the + `frame_callback` are compiled out on a stock TrussC build**; the poll-loop fallback is + what actually runs unless the project bumps `ANDROID_PLATFORM`. (MEMORY: + `project_android_api_level` — bumping to 31 blocks on AMidi etc.; the density and BACK + patches work at 26.) The port must keep both the `#if __ANDROID_API__ >= 29` guards and + the fallback intact so a future API bump lights up the choreographer path. +- **Manifest**: `android.app.NativeActivity` with `android.app.lib_name` = the app's + `.so`; `configChanges="orientation|screenSize|screenLayout|keyboardHidden"` (activity + survives rotation — §5); `glEsVersion=0x00030000`. + +### CI +`.github/workflows/build.yml` has a **`build-android` job (239)** — NDK **r29** (267), +caches `core/build-android`. So the port is **compile-tested in CI** (unlike iOS). CI +does not run on a device; runtime behaviour is verified on the user's Android hardware +(§12d). + +**Used and must be preserved on Android:** `ANativeActivity_onCreate` export → app-thread +loop + message pipe; the `sokol_main`→`main()`→`runApp`→`g_androidDesc` entry chain; EGL +init/surface lifecycle + APP_CMD state machine; Choreographer frame loop **and** poll +fallback; per-frame `update_dimensions` with the AConfiguration DPI patch (§5) and +`setBuffersGeometry` half-res; `skip_present` early-return; touch events (identifier + +changed + tooltype); **BACK-key no-shutdown patch (§7)**; `sapp_android_get_native_activity` +no-valid-assert getter; `sapp_show_keyboard` → soft input; SUSPENDED/RESUMED firing. + +**Not applicable / gaps on Android:** §10 table; `sapp_keyboard_shown()` always false +(§8); SUSPENDED/RESUMED dropped by TrussC (§3); Choreographer path compiled out at API 26 +(§11 build); orientation/immersive live in TrussC-JNI, not sokol (§8). + +--- + +## 12. PORT CHECKLIST (`sokol_app_tc.h` `#elif defined(__ANDROID__)` section) + +### (a) Lift verbatim (line ranges in sokol_app.h → rename `_sapp`→`_sapp_tc`, `_sapp_*`→`_sapp_tc_*`) +1. **Platform detect + `#error` guards** — 2346–2354 (GLES3 required; **SOKOL_NO_ENTRY + forbidden** — keep this). +2. **Includes** — 2520–2531 (pthread/unistd/time, native_activity, **configuration.h + [TrussC]**, looper, `#if __ANDROID_API__>=29 choreographer.h`, EGL, GLES3). +3. **Log items** — 1795–1804 (NativeActivity events + `ANDROID_CHOREOGRAPHER_ENABLED/ + UNAVAILABLE` TrussC items). +4. **State structs** — 3027–3074 (msg enum, pt, resources, `_sapp_android_t` incl. the + `#if __ANDROID_API__>=29 choreographer` fields) + embed 3311–3312. +5. **EGL** — `init_egl` 10322–10386, `cleanup_egl` 10388–10402, `init_egl_surface` + 10404–10424 (keep `gl.framebuffer` read 10422), `cleanup_egl_surface` 10426–10435. +6. **`app_event`** 10437–10442. +7. **`update_dimensions`** 10444–10503 — keep the **AConfiguration DPI patch (10482–10497)** + and the `setBuffersGeometry` half-res / artefact note. +8. **`cleanup` / `shutdown`** 10505–10521. +9. **`_sapp_android_frame`** 10523–10533 — keep the **`skip_present` early-return + (10530–10531)**. +10. **Touch** 10535–10587 (identifier, tooltype, changed) + **key/BACK-no-shutdown patch + 10589–10605** (do NOT reintroduce `_sapp_android_shutdown`). +11. **Input/main callbacks** — `input_cb` 10607–10627, `main_cb` (APP_CMD state machine) + 10629–10718, `should_update` 10720–10724. +12. **Choreographer callback** 10726–10741 (`#if __ANDROID_API__ >= 29`). +13. **`show_keyboard`** 10743–10751. +14. **Loop** 10753–10829 — keep BOTH the choreographer path (10765–10797) and the poll + fallback (10799–10809), and the "heap corruption on ALooper_removeFd" comment + (10817–10819, leave the removal commented out). +15. **UI-thread bridge + NativeActivity callbacks** 10832–10932. +16. **`on_destroy` + `exit(0)`** 10934–10965. +17. **`ANativeActivity_onCreate`** 10967–11036 — the exported entry (double `activity=`, + thread spawn, MSG_CREATE handshake, callback registration; keep the "NOT A BUG: do NOT + call sapp_discard_state()" comment). +18. **Public API Android branches** — already present in the tc.h public block; wire them: + `sapp_egl_get_display/context` (sokol_app.h 14064–14084 → tc.h 3079–3092/11244–11257), + `sapp_show_keyboard` (14086–14094 → tc.h 3102–3103/11268), `sapp_keyboard_shown` + (14096–14098), `sapp_android_get_native_activity` (14582–14590 → tc.h 3593–3597), + `sapp_skip_present` (14596–14599). desc GLES3 minor=1 (3537–3543 → tc.h 2225–2227/ + 9341–9343). + +### (b) Adapt to the tc window model (single window = window #0) +1. **`sapp_create_window()` + plural-window APIs → invalid handle + log** (like web/iOS). + The activity owns exactly one `ANativeWindow`. +2. **`sapp_window_*` getters** → window #0 returns real values + (`_sapp_tc.android.current.window`, framebuffer size, dpi_scale); other index → 0 + log. +3. **The "window handle" is `_sapp_tc.android.current.window`** (`ANativeWindow*`); + there is no HWND/UIWindow analogue beyond `sapp_android_get_native_activity()`. +4. **DPI is single, display-wide** (`AConfiguration_getDensity/160`, §5). No per-window DPI. +5. **Plain C++ — NO ObjC `#error` guard** (contrast mac/iOS). +6. **The owned loop lives on the spawned app thread** (`_sapp_android_loop`), driven by + Choreographer or `ALooper_pollOnce`. The process entry is `ANativeActivity_onCreate`, + NOT `main`/`sapp_run` — this branch is the ONLY one that exports an activity entry + point and does NOT provide a `sapp_run`-driven `main`. + +### (c) Known hazards +1. **App thread ≠ Android UI thread ≠ where `sokol_main`/`runApp` ran** (§2). Don't assume + TrussC "main thread" identity is fixed at `runApp` (that's the UI thread); it must be + established on the app thread (setup/first frame). JNI from the app thread needs an + attach. +2. **`SOKOL_NO_ENTRY` must be absent** from the Android impl TU, and the tc.h Android + branch must **export `ANativeActivity_onCreate`** and keep the `#error` on + SOKOL_NO_ENTRY. If the port accidentally guards onCreate behind `!SOKOL_NO_ENTRY` (as + sokol does for desktop `main`), the app has no entry point and won't launch. +3. **BACK-key no-shutdown (§7)** — kiosk/screen-pinning depends on it. Do NOT let the + verbatim lift silently reintroduce upstream's `_sapp_android_shutdown()` on BACK. +4. **Choreographer path is compiled out at API 26** (§11) — verify the poll fallback still + renders; do not delete it as "dead" (a future `ANDROID_PLATFORM` bump enables the + choreographer path). +5. **`exit(0)` in `on_destroy`** — no C++ static dtors run after it; consistent with + TrussC's leak-GPU-singletons policy. Don't add teardown that must run post-exit. +6. **Surface can vanish mid-lifecycle** — every `_sapp_android_frame` asserts a valid + surface; the render gate `should_update()` is what prevents drawing without one. Keep + the gate; don't render on RESUME before SET_NATIVE_WINDOW re-creates the surface. +7. **DPI density patch + fb/win fallback** (§5) — both branches; density can be 0. +8. **`sapp_keyboard_shown()` never true** (§8) — a pre-existing quirk, not a port bug. + +### (d) Device-verification checklist (Android IS compile-tested in CI; verify runtime on the user's device) +CI's `build-android` only compiles. On a real Android device, before calling the port done: +- [ ] App launches (NativeActivity → onCreate → app thread → EGL surface → `setup()`), + renders, holds framerate; correct colors/orientation. +- [ ] **Touch**: multi-touch works; began/moved/ended/cancelled reach app; `identifier` + stable across a drag; coordinates correct at the device's density; `android_tooltype` + distinguishes finger/stylus if available. +- [ ] **BACK key**: pressing BACK does **not** kill or freeze the app (event consumed, EGL + surface intact) — the whole point of §7. +- [ ] **Rotation / config change**: rotate → RESIZED fires, framebuffer resizes, activity + is NOT recreated (manifest `configChanges`), no artefacts. +- [ ] **Home / recents / resume**: background then foreground → SET_NATIVE_WINDOW(NULL)+ + PAUSE stops frames and destroys the surface; resume recreates the surface and frames + resume; no crash, no black frame, RESIZED fires on return. +- [ ] **Immersive mode**: `setImmersiveMode(true)` hides status/nav bars (JNI, §8); + `false` restores. +- [ ] **Orientation lock**: `setOrientation(...)` (JNI `setRequestedOrientation`) actually + constrains rotation. +- [ ] **Soft keyboard**: tcxImGui text field raises the keyboard via `sapp_show_keyboard`. +- [ ] **Screen pinning / kiosk (lock-task)**: enter screen pinning, press BACK/HOME → + app survives (no frozen last-frame), proving the BACK patch under the OS's blocked + `ANativeActivity_finish`. +- [ ] **DPI**: `sapp_dpi_scale()` ≈ device density/160 (e.g. xxhdpi ≈ 3.0), consistent + with desktop semantics. +- [ ] **Task-swipe exit**: swipe the app away → onDestroy → clean `exit(0)`, relaunch works. +- [ ] **(If ANDROID_PLATFORM bumped ≥29)** Choreographer path engages (log + `ANDROID_CHOREOGRAPHER_ENABLED`), vsync-paced frames. + +--- + +## Appendix: function → line index (Android GLES3-relevant, sokol_app.h) + +| symbol | line | +|---|---| +| Android platform detect (`_SAPP_ANDROID` @2348; GLES3 `#error` @2350; **NO_ENTRY `#error` @2352**) | 2346–2354 | +| Android includes (`configuration.h` [TrussC] @2525; choreographer `#if>=29` @2527) | 2520–2531 | +| Log items (Choreographer ENABLED/UNAVAILABLE @1803–1804) | 1795–1804 | +| posix/CLOCK_MONOTONIC timestamp init / now | 2586–2596 / 2635–2639 | +| `_sapp_android_msg_t` enum / pt / resources / `_sapp_android_t` (choreographer @3068) | 3028 / 3039 / 3047 / 3052 | +| `_sapp_android_t android;` embed | 3311 | +| desc defaults GLES3 minor=1 (Android/Linux) | 3537–3543 | +| `_sapp_android_init_egl` (ES3.1 ctx @10372) | 10322 | +| `_sapp_android_cleanup_egl` | 10388 | +| `_sapp_android_init_egl_surface` (gl.framebuffer @10422) | 10404 | +| `_sapp_android_cleanup_egl_surface` | 10426 | +| `_sapp_android_app_event` | 10437 | +| `_sapp_android_update_dimensions` (setBuffersGeometry @10468; **DPI patch @10482–10497**; RESIZED @10500) | 10444 | +| `_sapp_android_cleanup` / `_shutdown` | 10505 / 10516 | +| `_sapp_android_frame` (**skip_present @10530–10531**; eglSwapBuffers @10532) | 10523 | +| `_sapp_android_touch_event` (tooltype @10577; changed @10578) | 10535 | +| `_sapp_android_key_event` (**BACK no-shutdown @10593–10603**) | 10589 | +| `_sapp_android_input_cb` (AInputQueue drain) | 10607 | +| `_sapp_android_main_cb` (**APP_CMD state machine**) | 10629 | +| `_sapp_android_should_update` (render gate) | 10720 | +| `_sapp_android_frame_callback` (Choreographer, `#if>=29`) | 10726 | +| `_sapp_android_show_keyboard` | 10743 | +| `_sapp_android_loop` (choreographer path @10784; poll fallback @10799) | 10753 | +| `_sapp_android_msg` (UI→app write) | 10832 | +| NativeActivity callbacks (on_start/resume/pause/stop/focus/window/input/lowmem) | 10838–10932 | +| `_sapp_android_on_destroy` (**exit(0) @10964**) | 10934 | +| **`ANativeActivity_onCreate`** (sokol_main @10979; thread spawn @10997; callbacks @11016) | 10967 | +| `#endif /* _SAPP_ANDROID */` | 11038 | +| `sapp_egl_get_display` / `_context` (Android branch) | 14064 / 14075 | +| `sapp_show_keyboard` (Android @14089) / `sapp_keyboard_shown` | 14086 / 14096 | +| `sapp_toggle_fullscreen` (no Android branch → no-op) | 14104 | +| `sapp_dpi_scale` | 14060 | +| `sapp_android_get_native_activity` (no valid-assert) | 14582 | +| `sapp_skip_present` (TrussC) | 14596 | +| **tc.h port target:** top-level branch chain (apple @198, win @3670, linux @5650, emsc @8917, stub @11818); android public branches already present @3079–3103 / 11244–11268 / 3593–3597 | sokol_app_tc.h | diff --git a/docs/dev/sapp-inventory.md b/docs/dev/sapp-inventory.md new file mode 100644 index 00000000..ae7d2f93 --- /dev/null +++ b/docs/dev/sapp-inventory.md @@ -0,0 +1,239 @@ +# sokol_app (sapp_) usage inventory + +Purpose: complete inventory of everything TrussC consumes from `sokol_app.h`, +to prepare a drop-in replacement header (`sokol_app_tc.h`). Scope: `core/`, +`addons/`, a quick sweep of `examples/`/`apps/`/`tools/`, plus the existing +TrussC patches already applied to `sokol_app.h` (these must carry over). + +Method: `grep -rohE 'sapp_[A-Za-z0-9_]*' --exclude-dir=sokol | sort | uniq -c` +per file, read call sites for context. Counts are per-identifier occurrence +(not unique call sites — a line using the same identifier twice counts twice). + +Headline numbers: **core/ (excluding `sokol/`) uses 27 distinct `sapp_*` +identifiers** across 10 files, overwhelmingly concentrated in `TrussC.h` +(52 of ~75 occurrences). Addons add 3 more identifiers (all via +`tcxImGui/src/sokol_imgui.h`, which is the single largest consumer outside +core). Only 1 identifier (`sapp_request_quit`) leaks into example/app user +code, and only in the trivial "Esc to quit" idiom (9 sites). Nothing in +`tools/` (trusscli) touches sapp at all. + +--- + +## Table 1: API usage + +### Lifecycle / frame + +| sapp API | call sites | files | what it provides | per-window / app-global | platforms | +|---|---|---|---|---|---| +| `sapp_run` | 3 | TrussC.h (`runApp`), tcHotReloadHost.h (`runHotReloadApp`) x2 | Starts the sokol_app event loop from a built `sapp_desc` | app-global (one loop) | all desktop (not called on Android — see notes) | +| `sapp_desc` (type) | 8 | TrussC.h, tcHotReloadHost.h | Descriptor struct: width/height/title/high_dpi/sample_count/fullscreen/swap_interval, `init_cb`/`frame_cb`/`cleanup_cb`/`event_cb`, `logger.func`, `enable_dragndrop`+`max_dropped_files`+`max_dropped_file_path_length`, `enable_clipboard`+`clipboard_size` | app-global | all | +| `sapp_quit` | 2 | TrussC.h (`exitApp`), tcHotReloadHost.h (host init failure) | Immediate, non-cancellable exit | app-global | all | +| `sapp_request_quit` | 2 (core) + 9 (examples) | TrussC.h (`requestExitApp`), tcStandardTools.h; examples/\*/tcApp.cpp (9 files: coordinateConversionExample, 3DPrimitivesExample, vectorMathExample, loopModeExample, hitTestExample, nodeExample, graphicsExample, roundedRectExample, colorExample) | Cancellable exit; triggers `SAPP_EVENTTYPE_QUIT_REQUESTED` | app-global | all except Android (patched out — see Table 2 item "Android BACK key") | +| `sapp_cancel_quit` | 1 | TrussC.h (`_event_cb`, `SAPP_EVENTTYPE_QUIT_REQUESTED` handler) | Cancels a pending quit if `exitRequested` listener sets `args.cancel` | app-global | all | +| `sapp_frame_duration` | 3 | TrussC.h (x2, update-loop dt + first-frame estimate), tcxImGui.h/sokol_imgui.h (`simgui_desc.delta_time`) | Smoothed per-frame delta time from sokol's internal timer | app-global | all | +| `sapp_frame_count` | 5 | tcTexture.h x2 (`loadData` once-per-frame guard), tcFont.h x2 (dpi + `flushPendingDestroys` once-per-frame guard), tcGlobal.cpp (`getDrawCount()`) | Monotonic frame counter | app-global | all | +| `sapp_skip_present` | 1 | TrussC.h (`_frame_cb`, after `endSwapchainPass`) | Skips the next present/swap call (event-driven low-power redraw without visible flicker) | app-global | all (TrussC-added function; see Table 2) | +| `sapp_event` (type) | 3 | TrussC.h (`_event_cb` signature x1), tcCoreEvents.h (`rawEvent` listener signature x2) | Raw input event struct forwarded to `events().rawEvent` for addons (tcxImGui) | app-global | all | +| `sapp_event_type` enum consts (`SAPP_EVENTTYPE_*`) | ~14 distinct + counts | TrussC.h `_event_cb` switch | Dispatch table: KEY_DOWN/UP, MOUSE_DOWN/UP/MOVE/SCROLL, TOUCHES_BEGAN/MOVED/ENDED/CANCELLED, RESIZED, FILES_DROPPED, CLIPBOARD_PASTED, QUIT_REQUESTED | app-global (delivered per active window in multi-window future) | all (touch: Android/iOS/web only) | + +### Window geometry / DPI + +| sapp API | call sites | files | what it provides | per-window / app-global | platforms | +|---|---|---|---|---|---| +| `sapp_width` | 11 | TrussC.h (getWidth/getViewport x4, resize handler, aspect ratio, screenshot helpers), tcVideoRecorder.h x2, tcHotReloadHost.h, tcxLut/tcLut.h, tcxImGui.h/sokol_imgui.h, tcPlatform_linux.cpp, tcPlatform_android.cpp, tcVideoPlayer_linux.cpp | Framebuffer width in pixels | **app-global only for the main window** — `getWidth()` branches on `ctx.isMain` and uses `ctx.fbWidth` for secondary windows (see Notes) | all | +| `sapp_height` | 9 | same set minus one | Framebuffer height in pixels | same caveat as above | all | +| `sapp_dpi_scale` | 11 | TrussC.h (getDisplayScaleFactor x1 + 6 more sites), tcFont.h (glyph physical-size scaling), tcHotReloadHost.h, tcxLut/tcLut.h, tcxImGui.h/sokol_imgui.h, tcPlatform_android.cpp (`getDisplayScaleFactor`), tcPlatform_win.cpp (indirectly via setWindowSize), tcVideoPlayer_linux.cpp | Display scale factor (Retina/HiDPI); on Android patched to use real `AConfiguration` density instead of fb/win ratio (Table 2) | same `isMain` caveat | all | +| `sapp_sample_count` | 2 | tcMeshPointPipeline.h, tcMeshPbrPipeline.h | MSAA sample count for the default swapchain, used to pick/build the right pipeline when not inside an FBO pass | app-global | all | + +### Events / input + +| sapp API | call sites | files | what it provides | per-window / app-global | platforms | +|---|---|---|---|---|---| +| `sapp_event` fields (`type`, `modifiers`, `key_code`, `key_repeat`, `mouse_x/y`, `mouse_button`, `scroll_x/y`, `num_touches`, `touches[]`) | many (read, not counted separately) | TrussC.h `_event_cb` | Source data for TrussC's `KeyEventArgs`/`MouseEventArgs`/`ScrollEventArgs`/`TouchEventArgs` and legacy `App::handle*` callbacks | app-global input → written into `WindowContext` (mouseX/Y, keysPressed) | all | +| `SAPP_MODIFIER_SHIFT/CTRL/ALT/SUPER` | 4 | TrussC.h `_event_cb` | Modifier bitmask decode | app-global | all | +| `SAPP_KEYCODE_*` (30 distinct constants) | 35 | TrussC.h: `constexpr int KEY_*` alias table ~L1827-1866 (SPACE/ESCAPE/ENTER/TAB/BACKSPACE/DELETE, arrows, F1-F12) + `isShiftPressed`/`isControlPressed`/`isAltPressed`/`isSuperPressed` modifier-check helpers ~L1567-1572 (LEFT/RIGHT_SHIFT/CONTROL/ALT/SUPER) | Named key constants re-exported as TrussC's own `KEY_*` constants, so user code never needs to spell `SAPP_KEYCODE_*` | app-global | all | +| `sapp_get_num_dropped_files` | 1 | TrussC.h (`SAPP_EVENTTYPE_FILES_DROPPED` handler) | Count of files in a drag-and-drop event | app-global | desktop (macOS/Windows/Linux) + web | +| `sapp_get_dropped_file_path` | 1 | TrussC.h (same handler, loop) | Path of the i-th dropped file | app-global | desktop + web | +| `sapp_consume_event` | 4 | addons/tcxImGui/src/sokol_imgui.h (`simgui_handle_event`, key down/up when ImGui wants keyboard) | Marks the current event as consumed so sokol_app / TrussC's own dispatch doesn't double-handle it | app-global | all | +| `sapp_keyboard_shown` / `sapp_show_keyboard` | 1 each | addons/tcxImGui/src/sokol_imgui.h (`simgui_new_frame`, toggles on-screen keyboard based on `io->WantTextInput`) | On-screen (mobile) keyboard visibility control | app-global | Android/iOS (no-op elsewhere) | +| `sapp_ios_set_supported_orientations` | 1 | tcPlatform_ios.mm | Runtime orientation mask control | per-app (iOS has one screen) | iOS (TrussC-patched API — Table 2) | + +### Mouse cursor + +| sapp API | call sites | files | what it provides | per-window / app-global | platforms | +|---|---|---|---|---|---| +| `sapp_show_mouse` | 2 | TrussC.h (`showCursor`/`hideCursor`) | Show/hide system cursor | app-global | all | +| `sapp_set_mouse_cursor` / `sapp_get_mouse_cursor` | 1 / 1 (core) + 2 (sokol_imgui.h ImGui-driven cursor sync) | TrussC.h (`setCursor`/`getCursor`), sokol_imgui.h `simgui_new_frame` (maps `ImGuiMouseCursor` → `sapp_mouse_cursor` each frame unless `disable_set_mouse_cursor`) | Set/read current cursor shape (system cursor enum or custom slot) | app-global | all | +| `sapp_mouse_cursor` (type) + `SAPP_MOUSECURSOR_*` (11 system + 16 `CUSTOM_0..15` slots) | 40 + many enum refs | TrussC.h `enum class Cursor` (1:1 mirror of the sokol enum) | Cursor shape enum, including 16 custom-image slots | app-global | all | +| `sapp_bind_mouse_cursor_image` / `sapp_unbind_mouse_cursor_image` | 1 / 1 | TrussC.h (`bindCursorImage`/`unbindCursorImage`) | Upload/release a custom RGBA cursor image + hotspot into a `CUSTOM_n` slot | app-global | all (custom cursor images not exercised much beyond this wrapper — no example uses it) | +| `sapp_image_desc` (type) | 1 | TrussC.h (`bindCursorImage`) | Struct for `{width, height, pixels, cursor_hotspot_x/y}` passed to bind | app-global | all | + +### Clipboard + +| sapp API | call sites | files | what it provides | per-window / app-global | platforms | +|---|---|---|---|---|---| +| `sapp_set_clipboard_string` | 1 (core) + 1 (sokol_imgui.h) | TrussC.h (`setClipboardString`), sokol_imgui.h (ImGui `SetClipboardTextFn`) | Write OS clipboard | app-global | all | +| `sapp_get_clipboard_string` | 1 (core) + 1 (sokol_imgui.h) | TrussC.h (`getClipboardString`), sokol_imgui.h (ImGui `GetClipboardTextFn`) | Read OS clipboard | app-global | all | +| `SAPP_EVENTTYPE_CLIPBOARD_PASTED` | 1 | TrussC.h `_event_cb` | Notifies when web clipboard content becomes available (web needs the paste event; desktop can read clipboard synchronously) | app-global | primarily web | +| `desc.enable_clipboard` / `desc.clipboard_size` | (part of `sapp_desc` count above) | TrussC.h `buildAppDescriptor`, tcHotReloadHost.h | Enables clipboard subsystem + buffer size (mirrored into `WindowContext::clipboardSize` for overflow warnings) | app-global | all | + +### Swapchain / native handles + +| sapp API | call sites | files | what it provides | per-window / app-global | platforms | +|---|---|---|---|---|---| +| `sapp_get_swapchain` | 1 (core) + 1 (win) + 1 (mac) | tcGlobal.cpp (fallback when `ctx.acquireSwapchain` is null), tcPlatform_win.cpp (`captureWindow` — reads `sc.d3d11.render_view`), tcPlatform_mac.mm (`captureWindowAsync` fallback — reads `.metal.current_drawable`) | Current-frame `sg_swapchain` handle (backend-specific texture/view/drawable) | **app-global / main-window only** — secondary windows supply their own via `ctx.acquireSwapchain` hook, bypassing sapp entirely | all (fields vary: d3d11/.metal/.gl/.wgpu) | +| `sglue_swapchain()` (sokol_glue, wraps `sapp_get_environment()` + `sapp_get_swapchain()`) | 1 | tcGlobal.cpp (`beginSwapchainPass`, main-window default) | Bridges sapp's swapchain/environment into an `sg_pass.swapchain` | app-global (main window) | all | +| `sapp_macos_get_window` | 3 | tcPlatform_mac.mm (window-level ops: always-on-top, position, etc.) | Native `NSWindow*` for the sokol-owned main window | per-window (main only) | macOS | +| `sapp_win32_get_hwnd` | 6 | tcPlatform_win.cpp | Native `HWND` for the sokol-owned main window | per-window (main only) | Windows | +| `sapp_x11_get_window` | 2 | tcPlatform_linux.cpp | Native `Window` (X11) for the sokol-owned main window | per-window (main only) | Linux | +| `sapp_ios_get_window` | 1 | tcFileDialog_ios.mm | Native `UIWindow*` (for presenting a document picker) | per-window | iOS | +| `sapp_android_get_native_activity` | 6 | tcPlatform_android.cpp (x4), tcSerial_android.cpp (x2), tcxCurl_android.cpp (x2 — wait, counted separately below) | `ANativeActivity*` — JNI bridge for file dialogs, serial permissions, network state | app-global (Android is single-activity) | Android | + +### Window control / fullscreen + +| sapp API | call sites | files | what it provides | per-window / app-global | platforms | +|---|---|---|---|---|---| +| `sapp_set_window_title` | 1 | TrussC.h (`setWindowTitle`) | Set OS window title | main window only | all (no-op/limited on web) | +| `sapp_is_fullscreen` | 2 | TrussC.h (`setFullscreen`, `isFullscreen`) | Query fullscreen state | main window only | all | +| `sapp_toggle_fullscreen` | 2 | TrussC.h (`setFullscreen`, `toggleFullscreen`) | Toggle fullscreen (sokol has no "set" — only toggle) | main window only | all | + +Note: `setWindowSize`/`setWindowSizeLogical` do **not** go through sapp at +all — sokol_app has no runtime desktop resize API, so TrussC calls native +platform code directly (`SetWindowPos`/`NSWindow setFrame`/`XResizeWindow`) +using the native handle obtained via `sapp_*_get_*window` above. `sapp_dpi_scale()` +is used only to convert between pixel-perfect and logical size requests. + +### Misc / android-serial / curl (indirect, via native activity handle) + +| sapp API | call sites | files | what it provides | per-window / app-global | platforms | +|---|---|---|---|---|---| +| `sapp_android_get_native_activity` (addon) | 2 | addons/tcxCurl/src/tcxCurl_android.cpp | Same JNI bridge, used to query Android network connectivity for libcurl | app-global | Android | + +--- + +## Table 2: TrussC's existing sokol_app.h patches + +(Source: `core/include/sokol/TRUSSC_MODIFICATIONS.md`, cross-checked against +`grep -n "tettou771\|\[TrussC" core/include/sokol/sokol_app.h`.) + +| patch | location (anchor) | why it exists | must carry over? | +|---|---|---|---| +| **Skip Present** (adds `sapp_skip_present()`) | Declaration ~L2258; `skip_present` field in `_sapp_t` ~L3276; checked in `_sapp_wgpu_frame` ~L4262, `_sapp_vk_frame` ~L5022, `_sapp_d3d11_present` ~L8751, `_sapp_wgl_swap_buffers` ~L9059, Android EGL swap ~L10530, Linux GLX swap ~L12566, Linux EGL swap ~L13829, public impl `sapp_skip_present()` ~L14596 | Fixes D3D11 flickering in event-driven (non-continuous) rendering by letting TrussC skip the next present call after a frame that shouldn't visibly swap | **Yes — used by `TrussC.h::_frame_cb`** (1 call site in core); needed on all backends it patches, but D3D11 is the motivating case | +| **Emscripten: keyboard events on canvas, not window** | Register ~L7963–7972 (`emscripten_set_keydown/keyup/keypress_callback` target changed from `EMSCRIPTEN_EVENT_TARGET_WINDOW` to `_sapp.html5_canvas_selector`); unregister ~L7998 | Lets other page elements (e.g. Monaco editor embedded alongside a TrussSketch canvas) receive keyboard input independently; canvas needs a `tabindex` to be focusable | Yes — required for TrussSketch web embedding use case | +| **10-bit color output (RGB10A2)** | Enum `SAPP_PIXELFORMAT_RGB10A2` ~L1865; macOS Metal layer + MSAA tex ~L5095/L5217; iOS Metal layer + MSAA tex ~L6407/L6488; D3D11 swapchain format + resize ~L8592/L8717/L8745; `sapp_color_format()` report ~L14041; plus 1-line mapping in `sokol_glue.h` (`SAPP_PIXELFORMAT_RGB10A2 → SG_PIXELFORMAT_RGB10A2`) | Reduces color banding on 10-bit displays at no bandwidth cost (same 32-bit/px as BGRA8); Metal + D3D11 only, WebGL unchanged | Yes — visual quality feature, no known regressions | +| **iOS custom view controller + runtime orientation control** | `_sapp_ios_view_ctrl` @interface/@impl ~L6917–6918 area; `sapp_ios_set_supported_orientations()` decl ~L14508 region | Upstream `UIViewController` doesn't support changing supported orientations at runtime; TrussC apps need this (e.g. rotate-to-landscape features) | Yes — `tcPlatform_ios.mm` calls `sapp_ios_set_supported_orientations()` directly (1 call site) | +| **iOS immersive mode** | Non-static `_sapp_ios_immersive_mode` global + `prefersStatusBarHidden`/`prefersHomeIndicatorAutoHidden` overrides on `_sapp_ios_view_ctrl` | Hide status bar / home indicator for fullscreen iOS experiences; `tcPlatform_ios.mm` reads/writes the extern global directly (not a function call) | Yes — used by `tcPlatform_ios.mm` (`getImmersiveMode`/set) | +| **iOS: use view bounds instead of `UIScreen.mainScreen.bounds`** | `_sapp_ios_update_dimensions()` ~L6658 | Avoids a timing bug where screen bounds don't yet match actual view layout; falls back to screen bounds if view isn't laid out yet | Yes — correctness fix for iOS sizing | +| **iOS: read back actual Metal drawable dimensions** | `_sapp_ios_mtl_update_framebuffer_dimensions()` ~L6512 | After setting drawable size, re-reads `layer.drawableSize` so `sapp_width()`/`sapp_height()` always matches what Metal will actually render, preventing scissor-rect-exceeds-render-pass errors | Yes — correctness fix | +| **Android: BACK key no longer triggers shutdown** | `_sapp_android_key_event()` ~L10594 | Upstream calls `_sapp_android_shutdown()` on BACK, destroying the EGL surface + firing `cleanup_cb`; under screen-pinning (lock-task) `ANativeActivity_finish()` is blocked by the OS, so the process survives but EGL is gone → frozen app. Event is consumed without shutdown instead. Documented long-term fix: implement `SAPP_EVENTTYPE_QUIT_REQUESTED` for Android (already implemented for macOS/Windows/Linux) | Yes — kiosk/lock-task deployments depend on this | +| **Android: real display density for `sapp_dpi_scale()`** | `_sapp_android_update_dimensions()` ~L10482 (`#include ` + `AConfiguration_getDensity()` / 160.0f) | Upstream computes dpi_scale from framebuffer/window pixel ratio, which is unreliable; using actual density makes Android's `sapp_dpi_scale()` consistent with macOS/Windows semantics | Yes — `tcFont.h` and others rely on `sapp_dpi_scale()` being meaningful cross-platform | +| **CGBitmapInfo enum-cast (silence C++20 warning)** | `_sapp_macos_set_dock_tile()` CGImage creation ~L5833 | C++20 flags `-Wdeprecated-enum-enum-conversion` on the original `CGImageAlphaInfo | CGImageByteOrderInfo` OR; explicit cast to the destination `CGBitmapInfo` (`uint32_t`) type, no behavior change | Yes, trivially (or simply avoid the warning in the rewritten code) | +| **(Removed) SetForegroundWindow (Win32)** | N/A — historical | Was in sokol_app.h; moved to `trussc::bringWindowToFront()` in `tcPlatform.h`/platform files, made cross-platform | N/A — already lives outside sokol_app; nothing to port from sokol_app.h itself | + +Also documented but **not sokol_app.h** (carries over to the `sokol_gl_tc.h` +fork, out of scope for this inventory but noted since it's in the same +"sokol modifications" family): `sg_append_buffer` multi-draw-per-frame, +auto-grow CPU/GPU buffers, `sgl_tc_*` public API additions, float vertex +colors (UBYTE4N→FLOAT4). These live in `util/sokol_gl_tc.h`, not +`sokol_app.h`, and are unaffected by a sokol_app replacement. + +--- + +## Table 3: Unused sokol_app features + +| feature | evidence it's unused | port now / defer / never | +|---|---|---| +| Mouse lock / pointer lock (`sapp_lock_mouse`, `sapp_mouse_locked`) | Zero hits anywhere in core/addons/examples (`grep sapp_lock_mouse\|sapp_mouse_locked` → empty) | Defer — no current feature needs it, but cheap to keep if the replacement supports it structurally | +| Window icon API (`sapp_set_icon`, `sapp_icon_desc`) | Zero hits outside `sokol_app.h` itself; TrussC's app icons are handled by CMake/platform resource embedding (`.ico`/`Info.plist`/`resources/`), not sapp | Never (for the sapp path) — keep using native resource-embedded icons | +| Vulkan backend (`sapp_vk_*`, `SOKOL_VULKAN`) | No `SOKOL_VULKAN` define anywhere in `core/CMakeLists.txt`; backends selected are Metal (mac/iOS), D3D11 (Windows), GLCORE (Linux desktop/Raspbian via GLES3), GLES3 (Android, Raspbian, web default), optional WGPU (web only) | Never (unless a future Vulkan backend is added — not currently planned) | +| WGPU backend (`sapp_wgpu_*`) | Only reachable via `TC_GRAPHICS_BACKEND STREQUAL "WGPU"` opt-in on Emscripten (`core/CMakeLists.txt` L188-193); default web build uses GLES3 | Defer — keep as an optional path if the replacement wants to support it, but it's not exercised by default CI/build | +| GL desktop path on macOS/iOS (`sapp_gl_*`, `sapp_macos_gl_*`) | macOS/iOS always build `SOKOL_METAL` (CMakeLists.txt), never `SOKOL_GLCORE`/`SOKOL_GLES3` on Apple platforms | Never for Apple; GLCORE path IS used on Linux desktop/Raspbian, GLES3 on Android — so the GL backend overall is NOT dead, just dead specifically on Apple | +| Custom mouse cursor images beyond the wrapper (`sapp_bind/unbind_mouse_cursor_image`) | `TrussC.h` exposes `bindCursorImage`/`unbindCursorImage` wrappers (1 call site each) but no example/app in the repo calls them | Port now (thin, already wired) but low priority to verify beyond compiling — no exerciser exists | +| Gamepad input | sokol_app has no gamepad API at all (not a TrussC omission — upstream doesn't expose this) | N/A — not a sokol_app feature to begin with | +| `sapp_html5_*` fetch / drag&drop / ask-leave-site internals | Only the public surface (`enable_dragndrop`, `SAPP_EVENTTYPE_FILES_DROPPED`, `SAPP_EVENTTYPE_CLIPBOARD_PASTED`) is touched by TrussC; the `sapp_html5_fetch_*`/`sapp_js_*` symbols found in the earlier broad grep are sokol_app's own internal implementation, not called by TrussC code | N/A (internal to sokol_app, not a TrussC dependency) | +| iOS/Android/Web-specific internals (`sapp_ios_mtl_*`, `sapp_android_egl_*`, `sapp_emsc_*`, `sapp_x11_*`, `sapp_win32_*`, `sapp_wgl_*`, `sapp_glx_*`, `sapp_egl_*`, `sapp_dxgi_*`, `sapp_d3d11_*` private helpers) | These are sokol_app's own backend implementation details (all inside `core/include/sokol/sokol_app.h`, prefixed `_sapp_` in the source even though the broad grep strips the leading underscore) — TrussC never calls them directly except through the small public surface documented in Table 1 | N/A — these must exist in the replacement's own backend implementations, but they are not a "TrussC consumes X" dependency; they're implementation, not API surface | +| Fullscreen: TrussC never calls a "set fullscreen(bool)" sapp function directly | sokol_app only exposes `sapp_toggle_fullscreen()` (no direct setter); TrussC's `setFullscreen(bool)` wrapper compares against `sapp_is_fullscreen()` first, then toggles | Port now — trivial, already wrapped this way | +| Runtime desktop window resize | No `sapp_set_window_size` — doesn't exist upstream. TrussC's `setWindowSizeLogical` bypasses sapp entirely via native handles (`sapp_macos_get_window`, `sapp_win32_get_hwnd`, `sapp_x11_get_window`) | Port now — but note the native-handle escape hatch (`sapp_*_get_*window`) is exactly how TrussC already works around this sokol_app gap; the replacement should keep exposing an equivalent native handle getter | + +--- + +## Notes + +- **Entry point / `TC_RUN_APP` — there is no macro; it's a template function.** + `runApp(settings)` (in `TrussC.h`, ~L2622–2644) builds a `sapp_desc` + via `buildAppDescriptor()` and calls `sapp_run(&desc)`. All the + `App` lifecycle glue (`internal::appSetupFunc`, `appUpdateFunc`, + `appDrawFunc`, `appCleanupFunc`, plus the per-input-type forwarding lambdas) + is wired inside `buildAppDescriptor` before the descriptor is returned. + `main.cpp` in every example just calls `return trussc::runApp(settings);`. + **On Android**, `sapp_run` is never called — `runApp()` just stores + the built descriptor into `internal::g_androidDesc`, and + `core/platform/android/sokol_impl.cpp`'s `sokol_main(argc, argv)` calls the + app's `main()` (which populates that global via `runApp`) then returns the + stored descriptor, because `ANativeActivity_onCreate` (provided by + sokol_app.h without `SOKOL_NO_ENTRY` on Android) is the real entry point and + calls `sokol_main()` to get it. + +- **There are two independent places that build a `sapp_desc` and call `sapp_run`**: + `TrussC.h::buildAppDescriptor`/`runApp` (normal path) and + `tcHotReloadHost.h::runHotReloadApp` (hot-reload path, `internal::` namespace). + They duplicate almost the entire descriptor-building logic (width/height/ + pixel-perfect conversion, high_dpi, sample_count, fullscreen, swap_interval, + the four core callbacks, drag&drop + clipboard enable/size). A sokol_app + replacement should ideally let both paths share one builder function instead + of hand-duplicating the field list twice (currently they're just two copies + kept in sync by hand). + +- **`sokol_glue.h` is the sapp↔sgfx bridge.** `sglue_swapchain()` (used once, + in `tcGlobal.cpp::beginSwapchainPass`) internally calls + `sapp_get_environment()` + `sapp_get_swapchain()` and converts + `sapp_pixel_format` → `sg_pixel_format` (including the TrussC-added + `SAPP_PIXELFORMAT_RGB10A2` → `SG_PIXELFORMAT_RGB10A2` mapping, the file's + only patch). This is the *only* place `sapp_get_environment` is reached from + TrussC code (transitively) — TrussC itself never calls it directly. + +- **Multi-window architecture already treats sapp as "main-window-only."** + `core/include/tc/app/tcWindowContext.h`'s `WindowContext` has an `isMain` + flag: when true, `getWidth()`/`getHeight()`/`getDisplayScaleFactor()` + (TrussC.h ~L317-328) read `sapp_width()`/`sapp_height()`/`sapp_dpi_scale()` + directly; when false (a Phase-1 secondary native window), they read + `ctx.fbWidth`/`ctx.fbHeight`/`ctx.dpiScale`, which the native window layer + keeps up to date itself, and the frame's `sg_swapchain` comes from + `ctx.acquireSwapchain(ctx.acquireSwapchainUser)` instead of + `sapp_get_swapchain()`. In other words: **the multi-window seam already + exists and sapp's job is scoped to exactly one (the main) window** — a + sokol_app replacement only needs to serve that single main-window role; + secondary windows are handled entirely outside sapp already (see + `core/platform/mac/tcWindowMac.mm`, which has zero `sapp_` references). + +- **Emscripten/web build differs only in backend selection, not in the sapp + API surface consumed.** `core/CMakeLists.txt` picks `SOKOL_GLES3` by default + or `SOKOL_WGPU` if `TC_GRAPHICS_BACKEND=WGPU`; the emscripten keyboard-on-canvas + patch (Table 2) is the only web-specific sokol_app.h modification. No + TrussC/addon code calls `sapp_emsc_*`/`sapp_js_*`/`sapp_html5_*` directly — + those are all sokol_app's own internal Emscripten implementation. + +- **`tcxImGui/src/sokol_imgui.h`'s sapp dependency is fully self-contained + and small.** It needs exactly: `sapp_event`/`sapp_keycode` (types, for + `simgui_handle_event`/`simgui_map_keycode`), `sapp_consume_event` (x4, key + down/up when ImGui wants keyboard focus), `sapp_width`/`sapp_height`/ + `sapp_frame_duration`/`sapp_dpi_scale` (feeding `simgui_new_frame`'s + `simgui_frame_desc_t`), `sapp_set_clipboard_string`/`sapp_get_clipboard_string` + (ImGui clipboard callbacks), `sapp_keyboard_shown`/`sapp_show_keyboard` + (mobile on-screen keyboard sync with `io->WantTextInput`), and + `sapp_get_mouse_cursor`/`sapp_set_mouse_cursor` (ImGui-driven cursor shape + sync, gated by `disable_set_mouse_cursor`). `tcxImGui.h` (the TrussC-side + wrapper) itself only touches `sapp_event` (raw-event listener signature) and + `sapp_width`/`sapp_height`/`sapp_frame_duration`/`sapp_dpi_scale` (building + the per-frame `simgui_frame_desc_t`) — i.e. it needs the exact same small + surface as sokol_imgui.h itself, nothing extra. + +- **User-facing code essentially never touches sapp directly.** Of ~30 + example projects, only 9 call `sapp_request_quit()` (the "Esc quits" idiom), + and none call anything else. This means the replacement header's public API + compatibility surface for *user* code is trivially small — the real + compatibility burden is entirely inside `TrussC.h`/`tcHotReloadHost.h` + (core lifecycle + input dispatch) and `tcxImGui`'s vendored `sokol_imgui.h`. + +- **`tools/` (trusscli) has zero sapp references** — it's a project-generator/ + build tool, not a sokol_app consumer at all. diff --git a/docs/dev/sapp-ios-impl-spec.md b/docs/dev/sapp-ios-impl-spec.md new file mode 100644 index 00000000..19b2fb2e --- /dev/null +++ b/docs/dev/sapp-ios-impl-spec.md @@ -0,0 +1,717 @@ +# sokol_app.h iOS (Metal) Backend — Behavioral Specification + +Implementation contract for giving `sokol_app_tc.h` an iOS main-window / +app-lifecycle backend (project "sokol_app graduation", iOS leg). All line +references are into `core/include/sokol/sokol_app.h` (14602 lines, this worktree). +Focus: `_SAPP_IOS` + **`SOKOL_METAL`** — the only configuration TrussC ships on +iOS (see §11). The `SOKOL_GLES3`/EAGL path that also lives in the iOS backend is +**DEAD for TrussC** and is marked not-ported wherever it appears. + +This is the fifth sibling of `sapp-mac-impl-spec.md` (event-driven `[NSApp run]`), +`sapp-win32-impl-spec.md` (owned PeekMessage loop), `sapp-x11-impl-spec.md` (owned +XPending loop) and `sapp-web-impl-spec.md` (no owned loop, browser rAF). **iOS is +closest to macOS** — same Apple/Metal machinery (CAMetalLayer, CADisplayLink, +RGB10A2 10-bit patch, MSAA/depth textures, mach timestamp clock, `_SAPP_OBJC_RELEASE` +teardown). It differs from mac in four structural ways: + +1. **UIKit, not AppKit.** `UIApplicationMain` + a `UIWindowSceneDelegate` own the + loop (never returns), instead of `[NSApplication sharedApplication] … [NSApp run]`. +2. **Single window, like web.** There is exactly one `UIWindow` bound to the + connecting `UIWindowScene`. A second window is not modeled — `sapp_create_window()` + and the plural-window APIs must return an invalid handle + log, exactly as the web + port does. +3. **Touch, not mouse.** No mouse / cursor / pointer-lock / clipboard-from-OS / + drag&drop / window-title / favicon surface at all (§10). The whole input model is + multi-touch + an on-screen keyboard driven through a hidden `UITextField`. +4. **App lifecycle events.** `SAPP_EVENTTYPE_SUSPENDED` / `RESUMED` are fired on + scene resign/activate (mac never fires these). + +**Port target.** `sokol_app_tc.h` currently has a macOS impl section guarded by +`#if defined(__APPLE__) && TARGET_OS_OSX` (sokol_app_tc.h:199) followed by +`#elif defined(_WIN32)` (1723). The iOS backend slots in as a new +`#elif defined(__APPLE__) && TARGET_OS_IPHONE` branch between them. Like the macOS +branch it MUST be compiled as Objective-C++ (`.mm`) — enforce with the same +`#error "… must be compiled as Objective-C"` guard (cf. sokol_app_tc.h:201–203). + +--- + +## 0. Global state touched (the shared contract) + +The single global is `static _sapp_t _sapp;` (~3298) which embeds `_sapp_ios_t ios;` +at **3301** (under `_SAPP_IOS`). The iOS struct is at **2850–2877**: + +```c +typedef struct { + UIWindow* window; + _sapp_ios_view* view; // UIView (Metal) subclass; hosts touch + display link + UITextField* textfield; // hidden field driving the on-screen keyboard (§7) + _sapp_textfield_dlg* textfield_dlg; + NSUInteger supported_orientations; // UIInterfaceOrientationMask (default: All) — TrussC patch (§8) + UIViewController* view_ctrl; // actually a _sapp_ios_view_ctrl instance — TrussC patch (§8) + struct { // SOKOL_METAL only + id device; + CAMetalLayer* layer; + CADisplayLink* display_link; + id depth_tex; + id msaa_tex; // nil unless sample_count > 1 + struct { CFTimeInterval timestamp; CFTimeInterval frame_duration_sec; } timing; + } mtl; + // #else EAGLContext* eagl_ctx; // GLES3 path — DEAD for TrussC + bool suspended; // scene resign/activate latch (§3) +} _sapp_ios_t; +``` + +The declared type of `view_ctrl` in the struct is `UIViewController*` (2857) but it +is **always constructed as `_sapp_ios_view_ctrl`** (a TrussC UIViewController subclass, +2837–2839, §8). `supported_orientations` and the subclass are both TrussC additions; +upstream sokol stores no orientation mask and uses a plain `UIViewController`. + +Cross-platform `_sapp` fields the iOS path reads/writes: `desc`, `valid`, +`first_frame`, `init_called`, `cleanup_called`, `window_width/height`, +`framebuffer_width/height`, `dpi_scale`, `sample_count`, `swap_interval`, `timing`, +`frame_count`, `event`, `onscreen_keyboard_shown`. Notably **NOT** touched on iOS: +`fullscreen`, `mouse.*`, `clipboard.*`, `drop.*`, `keycodes[]`, `custom_cursor_bound[]`, +`window_title`, `quit_requested`/`quit_ordered`, `skip_present` (all inert here, §10). + +`_sapp_init_state(desc)` runs first inside `_sapp_ios_run` (§1) and applies +`_sapp_desc_defaults` (sample_count→1, swap_interval→1, window_title→"sokol", etc.), +sets `first_frame=true`, `dpi_scale=1.0`. `desc.width/height` are ignored on iOS — +the window is always screen-sized (§2/§5). + +### iOS-specific desc: `sapp_ios_desc` (2031–2033) +```c +typedef struct sapp_ios_desc { bool keyboard_resizes_canvas; } sapp_ios_desc; +``` +Embedded as `desc.ios` (2068). **TrussC's `buildAppDescriptor` never sets it**, so +`keyboard_resizes_canvas` stays `false` — the on-screen keyboard does NOT shrink the +render view by default (§7). + +### `_SAPP_OBJC_RELEASE` / ARC +All ObjC teardown uses `_SAPP_OBJC_RELEASE(obj)` (the shared Apple release macro, +Apple decls region 2554–2630) — same as mac. `_SAPP_CLEAR_ARC_STRUCT` (2554–2563) is +ARC-compatible. iOS builds are typically ARC (`.mm`); "safe to `[release]` a nil +object" is relied on in `_sapp_ios_discard_state` (6576). + +--- + +## 1. Entry / run flow — UIApplicationMain owns the loop + +### Platform detection (2314–2329) +`__APPLE__` → include ``; then +`#if defined(TARGET_OS_IPHONE) && !TARGET_OS_IPHONE` → `_SAPP_MACOS`, **else** +`#define _SAPP_IOS (1)` (2322). API-select `#error` unless `SOKOL_METAL || SOKOL_GLES3` +(2323–2324). `TARGET_OS_TV` → `_SAPP_TVOS` (2326–2327) — TrussC never builds tvOS, +but the tvOS code (press events, `multipleTouchEnabled` guards) is compiled in and +must survive the lift; mark it dead-but-kept. + +### Apple / UIKit includes (2435–2447) +`#elif defined(_SAPP_IOS)` → `#import `; under `SOKOL_METAL`: +``, ``, `` +(2437–2440). The `_SAPP_ANY_GL` alternative pulls `` + `` +(2441–2444) — **DEAD for TrussC**. Plus ``, `` +(2446–2447, shared with mac). + +### `_sapp_ios_run(const sapp_desc* desc)` (6588–6593) +```c +_sapp_init_state(desc); +static int argc = 1; +static char* argv[] = { (char*)"sokol_app" }; +UIApplicationMain(argc, argv, nil, NSStringFromClass([_sapp_scene_delegate class])); +``` +- `UIApplicationMain` **never returns** (owns the run loop, like `[NSApp run]`). The + 4th arg (principal class) is `nil`; the 5th (delegate class) names + `_sapp_scene_delegate`, which conforms to **both** `UIApplicationDelegate` and + `UIWindowSceneDelegate` (2829). So the app delegate *is* the scene delegate class. +- **No `sg_setup`, no window, no Metal device is created here** — all of that happens + later, inside `scene:willConnectToSession:` when UIKit connects the scene (§2). This + is the crucial timing difference from mac (where `applicationDidFinishLaunching:` + does everything under `[NSApp run]`). On iOS, `_sapp_ios_run` returns control to + UIKit and the real setup is scene-driven. + +### Entry point / SOKOL_NO_ENTRY (6595–6602) +```c +#if !defined(SOKOL_NO_ENTRY) +int main(int argc, char* argv[]) { sapp_desc desc = sokol_main(argc, argv); _sapp_ios_run(&desc); return 0; } +#endif +``` +**TrussC defines `SOKOL_NO_ENTRY`** (TrussC.h:15 and platform/ios/sokol_impl.mm:6), so +this `main` is compiled out. The live entry chain is: +**user `int main()` → `runApp()` (TrussC.h:2634–2637) → `sapp_run(&desc)` +(13941, `#if SOKOL_NO_ENTRY`) → `_sapp_ios_run(desc)` (13946) → `UIApplicationMain`.** +So on iOS the user's own `main()` blocks forever inside `UIApplicationMain` — unlike +web where `sapp_run` returns immediately. The port must keep this NO_ENTRY dispatch. + +### Scene adoption WITHOUT an Info.plist scene manifest (LOAD-BEARING, 6727–6735) +```objc +- (UISceneConfiguration*) application:(UIApplication*)application + configurationForConnectingSceneSession:(UISceneSession*)connectingSceneSession + options:(UISceneConnectionOptions*)options { + UISceneConfiguration* config = [[UISceneConfiguration alloc] + initWithName:@"SokolSceneConfiguration" sessionRole:connectingSceneSession.role]; + config.delegateClass = [_sapp_scene_delegate class]; + return config; +} +``` +`_sapp_scene_delegate` returns a `UISceneConfiguration` **programmatically** from the +app-delegate method. This is what makes iOS adopt the `UIWindowScene` lifecycle +**even though `core/resources/Info-iOS.plist.in` declares NO +`UIApplicationSceneManifest`** (verified: the plist has `LSRequiresIPhoneOS`, +`UISupportedInterfaceOrientations`, usage strings — but no scene manifest). If a +future refactor adds a manifest, it must name this delegate class or the mechanism +double-registers. **Keep the programmatic config path.** + +`application:didFinishLaunchingWithOptions:` (6759–6761) just returns `YES`. + +--- + +## 2. Window + Metal setup (`scene:willConnectToSession:`, 6737–6757) + +This is the iOS analogue of mac's `applicationDidFinishLaunching:`. Exact sequence: + +1. `windowScene = (UIWindowScene*)scene; screen_rect = windowScene.screen.bounds;` +2. `_sapp.ios.window = [[UIWindow alloc] initWithWindowScene:windowScene];` +3. `window_width/height = round(screen_rect.size)` (points) — **always screen-sized**; + `desc.width/height` play no role. +4. **DPI (6743–6747):** `if (desc.high_dpi) dpi_scale = windowScene.screen.nativeScale; + else 1.0`. (`nativeScale`, not `scale` — the true backing ratio.) +5. `framebuffer_width/height = round(window_width/height * dpi_scale)` (6748–6749). +6. **Renderer bring-up:** `#if SOKOL_METAL _sapp_ios_mtl_init(windowScene)` (6751) — + **`#else _sapp_ios_gles3_init` is DEAD** (6753). +7. `[_sapp.ios.window makeKeyAndVisible];` (6755). +8. `_sapp.valid = true;` (6756). + +### `_sapp_ios_mtl_init(UIWindowScene*)` (6474–6501) +- `mtl.device = MTLCreateSystemDefaultDevice()` (6475). +- `view = [[_sapp_ios_view alloc] initWithFrame:windowScene.screen.bounds]`; + `userInteractionEnabled = YES`; `multipleTouchEnabled = YES` (guarded `!_SAPP_TVOS`, + 6479–6481). +- `supported_orientations = UIInterfaceOrientationMaskAll` (6482, and again 6493 — + redundant but harmless). +- **CAMetalLayer (6484–6491):** `layer = [CAMetalLayer layer]`; `layer.device = device`; + `layer.opaque = true`; **`layer.framebufferOnly = false`** (6487, TrussC patch, issue + #56 — enables `captureWindow()` GPU readback); **`layer.pixelFormat = + MTLPixelFormatRGB10A2Unorm`** (6488, TrussC 10-bit patch); `layer.frame = + view.layer.frame`; then `[view.layer addSublayer:layer]`. NOTE: the Metal layer is a + **sublayer of the view's backing layer**, not the view's own layer (differs from mac, + where the view IS layer-backed by the CAMetalLayer). +- **View controller (6494–6497):** `view_ctrl = [[_sapp_ios_view_ctrl alloc] init]` + (TrussC subclass, §8); `modalPresentationStyle = UIModalPresentationFullScreen`; + `view_ctrl.view = view`; `window.rootViewController = view_ctrl`. +- `_sapp_ios_mtl_start_display_link()` (6499); `_sapp_ios_mtl_timing_init()` (6500). + +### Metal swapchain textures (`_sapp_ios_mtl_create/swapchain_*`, 6373–6432) +- `_sapp_ios_mtl_create_texture` (6373–6399): `MTLTextureType2DMultisample` if + `sample_count>1` else `2D`; `usage = RenderTarget`; `resourceOptions = + StorageModePrivate`; mip=1, arrayLength=1, depth=1. Labels only under `SOKOL_DEBUG`. +- `_swapchain_create` (6401–6412): depth = `MTLPixelFormatDepth32Float_Stencil8` + (panic `METAL_CREATE_SWAPCHAIN_DEPTH_TEXTURE_FAILED`); MSAA (only if `sample_count>1`) + = **`MTLPixelFormatRGB10A2Unorm`** (6407, TrussC patch; panic + `..._MSAA_TEXTURE_FAILED`). +- `_swapchain_destroy` (6414–6421) releases both; `_swapchain_resize` (6423–6426) = + destroy+create. +- `_swapchain_next` (6428–6432): `[layer nextDrawable]`, **asserts non-nil**, returns + the drawable. Called lazily at swapchain-query time (§9), not at frame start. + +### Display link + swap interval (`_sapp_ios_mtl_start/stop_display_link`, 6456–6472) +`display_link = [CADisplayLink displayLinkWithTarget:_sapp.ios.view +selector:@selector(displayLinkFired:)]`. `preferred_fps = max_fps / swap_interval` +(6460, `max_fps = windowScene.screen.maximumFramesPerSecond`, 6367–6369); +`preferredFrameRateRange = {p,p,p}` (6461–6462); added to the current run loop in +`NSRunLoopCommonModes` (6463). Stop invalidates + nils it (the run loop held the only +strong ref, 6466–6472). This is the frame driver — `displayLinkFired:` (6878) calls +`_sapp_ios_frame()` every vsync. + +--- + +## 3. App lifecycle / quit semantics (SUSPENDED · RESUMED · terminate) + +Unlike mac (window-close-driven quit, `quit_requested`/`quit_ordered`), **iOS has no +user-driven quit path.** There is no red button, no Cmd+Q, no `sapp_request_quit` +route to the OS. `quit_requested`/`quit_ordered` and `sapp_request_quit()` etc. are +inert on iOS (the frame loop never inspects them). Lifecycle is entirely +scene/UIKit-driven: + +### Suspend / resume (`_sapp_scene_delegate`, 6763–6785) +- **`sceneWillResignActive:` (6763–6773):** if not already suspended → `suspended=true`; + **pause the display link** (`display_link.paused = YES`, Metal only); fire + `SAPP_EVENTTYPE_SUSPENDED`. +- **`sceneDidBecomeActive:` (6775–6785):** if suspended → `suspended=false`; **unpause + the display link**; fire `SAPP_EVENTTYPE_RESUMED`. + +Both go through `_sapp_ios_app_event(type)` (6604–6609) → gated by +`_sapp_events_enabled()` (event_cb set AND `init_called`). **The display-link pause is +load-bearing:** it stops the frame callback (and therefore GPU work) while +backgrounded, which iOS requires — rendering to a Metal drawable while suspended is a +watchdog kill. The port MUST pause/resume the CADisplayLink on these transitions. + +> **⚠ TrussC wiring gap (finding):** TrussC.h's `_event_cb` switch (2248–2489) has NO +> `case SAPP_EVENTTYPE_SUSPENDED:` / `RESUMED:`. sokol fires them but TrussC drops them +> — they are **not surfaced to app code today**. The port doesn't need to change this, +> but the events must keep firing (the display-link pause is internal to the backend +> and independent of dispatch). If TrussC later wants app-visible suspend/resume, the +> plumbing already exists at the sokol layer. + +### Terminate (`applicationWillTerminate:`, 6791–6796) +```objc +_sapp_call_cleanup(); // user cleanup_cb, once (3450) +_sapp_ios_discard_state(); // release ObjC objects incl. Metal device/layer/textures (6575) +_sapp_discard_state(); // free cross-platform buffers, zero _sapp +``` +Same ordering guarantee as mac: **user `cleanup_cb` runs before any Metal teardown** +(TrussC does its own `sg_shutdown` in cleanup_cb). Comment 6787–6790 warns this method +**"will rarely ever be called — iOS apps are usually killed via SIGKILL"**, so the port +must not rely on clean teardown; treat it as best-effort. `_sapp_ios_discard_state` +(6575–6586) releases `textfield_dlg`, `textfield`, then `_sapp_ios_mtl_discard_state` +(6503–6509: stop display link, destroy swapchain, release layer/view_ctrl/device), +then `view`, then `window`. + +--- + +## 4. Frame timing + +Same two-layer model as mac: the platform-agnostic EMA filter (`_sapp.timing`) plus a +CADisplayLink-timestamp timer (`_sapp.ios.mtl.timing`). **iOS uses the real mach clock** +(unlike web, whose `_sapp_timestamp_now` asserts false) — the Apple timestamp path at +2606–2608 / 2621–2625 (`mach_absolute_time`) is live. + +### `_sapp_ios_frame(void)` (6678–6690) — the per-vsync driver +```c +_sapp_timing_update(&_sapp.timing, 0.0); // external_now=0 → uses mach clock (feeds fallback) +#if SOKOL_METAL +_sapp_ios_mtl_timing_update(); // CADisplayLink-timestamp timer +#endif +#if _SAPP_ANY_GL glGetIntegerv(GL_FRAMEBUFFER_BINDING, …) #endif // DEAD (GLES3 only) +@autoreleasepool { + _sapp_ios_update_dimensions(); // §5 — every frame + _sapp_frame(); // init_cb (first frame) then frame_cb +} +``` +Called from `_sapp_ios_view displayLinkFired:` (6878–6881) on Metal. The +`@autoreleasepool` per frame matters (touch/Metal churn ObjC temporaries). + +### CADisplayLink timing (`_sapp_ios_mtl_timing_*`, 6434–6454) +- `_timing_init` (6434–6437): `timestamp=0`, `frame_duration_sec = 1.0/max_fps`. +- `_timing_update` (6439–6449): `cur = display_link.timestamp`; **first frame skipped** + (`timing.timestamp>0` guard); else `dt = cur - prev` run through + `_sapp_timing_clamp(&_sapp.timing, dt)` (min/max clamp only, NOT the EMA, 2666) → + `frame_duration_sec`. Store `timestamp=cur`. +- `_timing_frame_duration` (6451–6454): returns `frame_duration_sec` (asserts >0). + +`sapp_frame_duration()` (13988) → `#elif _SAPP_IOS && SOKOL_METAL` → +`_sapp_ios_mtl_timing_frame_duration()` (13991–13992). +`sapp_frame_duration_unfiltered()` → raw `_sapp.timing.dt`. `sapp_frame_count()` +(13984) is the shared counter, incremented in `_sapp_frame()` after `frame_cb`. + +**Note vs mac:** iOS has NO occlusion/fallback-timer machinery (no `NSTimer` +fallback, no `_transition_to_occluded`). When backgrounded the display link is simply +paused (§3); there is no obscured-but-rendering state, so `frame_duration_sec` is +always the display-link value while running. + +--- + +## 5. Dimensions, DPI, rotation (`_sapp_ios_update_dimensions`, 6657–6676) + +Runs **every frame** from `_sapp_ios_frame`. This is how rotation is picked up (no +dedicated rotate event — the view bounds change and RESIZED fires). + +```c +CGRect view_rect = _sapp.ios.view.bounds; // TrussC patch (6661) +if (view_rect.size.width < 1 || height < 1) // not laid out yet → + view_rect = _sapp.ios.window.windowScene.screen.bounds; // fall back to screen +_sapp.window_width = round(view_rect.size.width); +_sapp.window_height = round(view_rect.size.height); +bool dim_changed = _sapp_ios_mtl_update_framebuffer_dimensions(view_rect); // Metal +if (dim_changed && !_sapp.first_frame) _sapp_ios_app_event(SAPP_EVENTTYPE_RESIZED); +``` + +- **TrussC patch (6658–6665):** uses `view.bounds` instead of + `UIScreen.mainScreen.bounds` (consistent with mac's `[view bounds]`), avoiding a + timing bug where screen bounds don't yet match the view's actual laid-out size; + falls back to screen bounds only before first layout. +- RESIZED is suppressed on `first_frame` (same as mac's startup-silent update), and + anyway `_sapp_events_enabled()` gates it (no events pre-init). + +### `_sapp_ios_mtl_update_framebuffer_dimensions(CGRect)` (6511–6532) — TrussC patch #7 +```c +_sapp.framebuffer_width = round(screen_rect.size.width * dpi_scale); +_sapp.framebuffer_height = round(screen_rect.size.height * dpi_scale); +CGSize cur = layer.drawableSize; +bool dim_changed = (framebuffer_width != round(cur.width)) || (framebuffer_height != round(cur.height)); +if (dim_changed) { + layer.drawableSize = { framebuffer_width, framebuffer_height }; + layer.frame = screen_rect; + _sapp_ios_mtl_swapchain_resize(framebuffer_width, framebuffer_height); +} +// ALWAYS read back the ACTUAL drawable size (Metal may clamp/round): +CGSize actual = layer.drawableSize; +_sapp.framebuffer_width = round(actual.width); +_sapp.framebuffer_height = round(actual.height); +return dim_changed; +``` +The readback (6526–6530) is the load-bearing TrussC fix: `sapp_width()`/`sapp_height()` +must equal what Metal will actually render, or a scissor/viewport can exceed the render +pass bounds and Metal validation aborts. **Replicate the set-then-readback exactly.** +`_sapp_roundf_gzero` (3423) = round, clamped ≥0. + +--- + +## 6. Touch input (`_sapp_ios_touch_event`, 6636–6655) + +Real multi-touch (no mouse emulation). Wired from `_sapp_ios_view` (6903–6914): +`touchesBegan/Moved/Ended/Cancelled:withEvent:` → `_sapp_ios_touch_event(TYPE, touches, +event)`. + +```c +_sapp_init_event(type); +// enumerate ALL active touches (event.allTouches), not just the changed set: +for (UITouch* t in event.allTouches) { + if (num_touches+1 < SAPP_MAX_TOUCHPOINTS) { + CGPoint p = [t locationInView:_sapp.ios.view]; + sapp_touchpoint* cp = &event.touches[num_touches++]; + cp->identifier = (uintptr_t)t; // pointer identity as stable id + cp->pos_x = p.x * dpi_scale; // framebuffer-space + cp->pos_y = p.y * dpi_scale; + cp->changed = [touches containsObject:t]; // was THIS the changed touch? + } +} +if (num_touches > 0) _sapp_call_event(&event); +``` +- Event type map: BEGAN→`TOUCHES_BEGAN`, MOVED→`TOUCHES_MOVED`, ENDED→`TOUCHES_ENDED`, + CANCELLED→`TOUCHES_CANCELLED`. +- **All active touches are emitted every event**, with `changed` distinguishing which + points this call is about — the app reconstructs deltas from `identifier`. Positions + are CSS→wait, iOS points × `dpi_scale` = framebuffer pixels. +- TrussC consumes these in `_event_cb` (TrussC.h:2373–2440): maps to `TouchEventArgs`, + tracks began/moved/ended, `cancelled` flag from CANCELLED. **Keep the `changed` flag + and `identifier` semantics intact** — TrussC's touch tracking relies on them. + +### tvOS press events (6611–6634, 6892–6902) — DEAD-but-kept +`_sapp_tvos_press_event` maps Apple-TV remote `UIPress` arrow/select/menu/playpause to +`SAPP_KEYCODE_*` and fires KEY_DOWN/KEY_UP. The `_sapp_ios_view` wires +`pressesBegan/Ended/Cancelled:`. TrussC never targets tvOS, but the code compiles on +iOS (harmless) — lift as-is, mark not-exercised. + +--- + +## 7. On-screen keyboard (hidden UITextField, 6692–6725, 6799–6874) + +There is no hardware keyboard model. Text entry is driven by a hidden `UITextField` +made first-responder to raise the software keyboard. + +### `_sapp_ios_show_keyboard(bool)` (6692–6725) +Lazily (first call) creates `textfield` (`CGRectMake(10,10,100,50)`, `hidden=YES`, +`text=@"x"`, no autocap/autocorrect/spellcheck) and `textfield_dlg`, adds the field as +a subview of `view_ctrl.view`, and registers three `NSNotificationCenter` observers +(`UIKeyboardDidShow`, `WillHide`, `DidChangeFrame`; guarded `!_SAPP_TVOS`, 6707–6717). +Then `shown ? [textfield becomeFirstResponder] : [textfield resignFirstResponder]`. + +`sapp_show_keyboard(bool)` (14086–14088) → `_sapp_ios_show_keyboard`. +`sapp_keyboard_shown()` (14096+) returns `_sapp.onscreen_keyboard_shown`. + +### `_sapp_textfield_dlg` (6799–6874) +- `keyboardWasShown:` (6800–6811): sets `onscreen_keyboard_shown=true`; **iff + `desc.ios.keyboard_resizes_canvas`** shrinks `view.frame` by the keyboard height. + Since TrussC leaves that flag false, the view is NOT resized — the keyboard overlays. +- `keyboardWillBeHidden:` (6813–6818): `onscreen_keyboard_shown=false`; restore frame + if resizing. +- `keyboardDidChangeFrame:` (6819–6830): re-apply shrink on rotation while open. +- **`textField:shouldChangeCharactersInRange:replacementString:` (6831–6873)** — the + actual text→event bridge. For each char: if `c >= 32` (and not a surrogate) fire + `SAPP_EVENTTYPE_CHAR` with `char_code=c`; if `c <= 32` map 10→ENTER, 32→SPACE and + fire KEY_DOWN+KEY_UP. Empty replacement string = **backspace** → KEY_DOWN+KEY_UP with + `SAPP_KEYCODE_BACKSPACE`. Returns `NO` (the field never actually accumulates text — + it stays `@"x"`; it's purely an event source). This yields CHAR + a *tiny* keycode + subset (Enter/Space/Backspace only) — there is NO full keycode table on iOS. + +### TrussC consumer +`addons/tcxImGui/src/sokol_imgui.h` (2661–2665) toggles the keyboard from ImGui: +`if (io->WantTextInput && !sapp_keyboard_shown()) sapp_show_keyboard(true);` and the +inverse. **Keep `sapp_show_keyboard` / `sapp_keyboard_shown` working** — this is the +one real consumer of the iOS keyboard on TrussC. + +--- + +## 8. Orientation + immersive mode (TrussC patches — `_sapp_ios_view_ctrl`, 6917–6930) + +Upstream sokol uses a plain `UIViewController` with no runtime orientation control. +TrussC replaces it with a subclass and adds two runtime hooks. **These are the highest- +risk items to preserve** because `tcPlatform_ios.mm` couples to them directly. + +```objc +bool _sapp_ios_immersive_mode = false; // 6919 — NON-STATIC extern global (TrussC patch) +@implementation _sapp_ios_view_ctrl +- (UIInterfaceOrientationMask)supportedInterfaceOrientations { return _sapp.ios.supported_orientations; } +- (BOOL)prefersStatusBarHidden { return _sapp_ios_immersive_mode; } +- (BOOL)prefersHomeIndicatorAutoHidden { return _sapp_ios_immersive_mode; } +@end +``` + +### (1) Runtime orientation — `sapp_ios_set_supported_orientations` (decl 2235; impl 14508–14520) +```c +_sapp.ios.supported_orientations = (NSUInteger)mask; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 160000 + if (@available(iOS 16.0, *)) [_sapp.ios.view_ctrl setNeedsUpdateOfSupportedInterfaceOrientations]; +#endif +``` +The subclass's `supportedInterfaceOrientations` returns the stored mask, so changing it ++ the iOS16 nudge re-queries the OS. **Consumer:** `tcPlatform_ios.mm:453–455` +`setOrientation(Orientation) → sapp_ios_set_supported_orientations((uint32_t)mask)`. + +### (2) Immersive mode — non-static extern global `_sapp_ios_immersive_mode` +The view controller's `prefersStatusBarHidden` / `prefersHomeIndicatorAutoHidden` +return this global. **`tcPlatform_ios.mm` reads/writes the extern directly** (NOT +through a function): 421 `extern bool _sapp_ios_immersive_mode;`, then +`setImmersiveMode(bool)` (423–433) sets it and calls +`setNeedsStatusBarAppearanceUpdate` + `setNeedsUpdateOfHomeIndicatorAutoHidden` on the +root VC; `getImmersiveMode()` (434) returns it. **The port MUST expose +`_sapp_ios_immersive_mode` as a non-static symbol with C++ linkage-visible name** (the +extern in tcPlatform_ios.mm must resolve). This is a fragile cross-TU coupling: if the +port renames/statics it, iOS immersive mode silently breaks at link time. + +`sapp_ios_set_supported_orientations` and `_sapp_ios_immersive_mode` are TrussC +additions (TRUSSC_MODIFICATIONS.md patches #4, #5) — not in upstream. Preserve the +exact symbol names. + +--- + +## 9. sapp_get_environment / sapp_get_swapchain (Metal) + +Same shape as mac; the iOS branch just reads `_sapp.ios.*` instead of `_sapp.macos.*`. + +### `sapp_get_environment()` (14387–14415) — asserts `_sapp.valid` +- `defaults.color_format = sapp_color_format()` → **`SAPP_PIXELFORMAT_RGB10A2`** on + Metal (14040–14042, TrussC 10-bit patch, shared with mac/D3D11). +- `defaults.depth_format = sapp_depth_format()` → `SAPP_PIXELFORMAT_DEPTH_STENCIL` + (14048). +- `defaults.sample_count = sapp_sample_count()` → `_sapp.sample_count`. +- `#if SOKOL_METAL … #else res.metal.device = _sapp.ios.mtl.device` (14397). + +### `sapp_get_swapchain()` (14417–14430) — asserts `_sapp.valid` +iOS Metal branch (14425–14428): +- `metal.current_drawable = (__bridge) _sapp_ios_mtl_swapchain_next()` — **`[layer + nextDrawable]` is called HERE, lazily**, exactly once per frame (TrussC calls + `sglue_swapchain()` inside `sg_begin_pass`). Calling twice per frame acquires two + drawables — must be called exactly once. +- `metal.depth_stencil_texture = _sapp.ios.mtl.depth_tex` +- `metal.msaa_color_texture = _sapp.ios.mtl.msaa_tex` (nil when sample_count==1) +- Common tail: `width/height` (framebuffer, min 1), `color_format=RGB10A2`, + `depth_format=DEPTH_STENCIL`, `sample_count`. + +These feed `sglue_environment()`/`sglue_swapchain()` (sokol_glue.h) exactly as on mac; +`_sglue_to_sgpixelformat` maps `RGB10A2 → SG_PIXELFORMAT_RGB10A2`. All d3d11/wgpu/gl +fields are memset-0 and ignored. + +### `sapp_ios_get_window()` (14498–14506) — TrussC-consumed +```c +#if defined(_SAPP_IOS) return (__bridge const void*) _sapp.ios.window; // asserts non-null +#else return 0; #endif +``` +Returns the `UIWindow*`. **Consumer:** `tcFileDialog_ios.mm:23` +`UIWindow* window = (__bridge UIWindow*)sapp_ios_get_window();` → `window.rootViewController` +to present the `UIDocumentPickerViewController`. **Keep this getter and its non-null +guarantee** or the iOS document picker (open/save dialogs) loses its presentation +anchor. + +--- + +## 10. Features NOT applicable on iOS (explicitly not-ported) + +Upstream's iOS backend simply has no code for these; the public API functions fall to +no-op/zero branches. The port should stub them the same way (and, per the single-window +adaptation, log on the plural-window ones): + +| Feature | iOS reality | +|---|---| +| **Mouse / cursor / pointer-lock** | No pointer model. `sapp_show_mouse`, `sapp_lock_mouse`, `sapp_set_mouse_cursor`, `sapp_bind_mouse_cursor_image` have no iOS branch → inert. | +| **Clipboard** | No `_SAPP_IOS` branch in `sapp_set/get_clipboard_string` (14247/14265 are `_SAPP_MACOS`). `sapp_get_clipboard_string()` returns `""`. No `CLIPBOARD_PASTED`. | +| **Drag & drop (OS)** | No iOS drop path. `enable_dragndrop` is inert; TrussC's file access on iOS is the document picker (§9), not sokol drop. | +| **Window title** | `sapp_set_window_title` (14282) is `_SAPP_MACOS`-only → no-op. No title bar. | +| **Icon / favicon** | `sapp_set_icon` (14308) `_SAPP_MACOS`-only. iOS icon comes from the app bundle (`CFBundleIconName`, set by trussc_app.cmake). | +| **Fullscreen** | iOS is inherently fullscreen. `sapp_toggle_fullscreen` (14105 `_SAPP_MACOS`) no-op; `_sapp.fullscreen` unused. Immersive mode (§8) is the closest analogue. | +| **Quit** | No `sapp_request_quit`→OS route (§3). App exit is OS/user-driven (home/SIGKILL). | +| **`skip_present`** | Metal presents via sokol_gfx commit; no present-suppression path on iOS (same as mac Metal). Inert. | +| **Secondary windows** | Single `UIWindow` bound to the scene. `sapp_create_window()` / plural-window getters → invalid handle + log (like web). Map window #0's identity to `_sapp.ios.window`. | +| **GLES3 / EAGL** | Present in-source (6535–6573, 6561, 6566) but TrussC builds `SOKOL_METAL` only (§11). **DEAD — do not port.** | + +--- + +## 11. TrussC-side wiring (verified against the repo) + +**THE MIGRATION IN ONE LINE:** `core/platform/ios/sokol_impl.mm` currently +`#include "sokol_app.h"` (with `SOKOL_IMPL`). The desktop TUs already include +`sokol_app_tc.h`. The iOS leg = give `sokol_app_tc.h` a `TARGET_OS_IPHONE` backend and +switch this one include. + +- **`core/platform/ios/sokol_impl.mm` (25 lines):** `#define SOKOL_IMPL`, + `#define SOKOL_NO_ENTRY`, GCC diag push (unused var/func, missing-field-init), then + includes in order: `sokol_log.h`, **`sokol_app.h`** ← swap to `sokol_app_tc.h`, + `sokol_gfx.h`, `sokol_glue.h`, `util/sokol_gl_tc.h`, `util/sokol_memtrack.h`. No + `SOKOL_METAL` define here — it comes from CMake. +- **`core/CMakeLists.txt`:** iOS detected at 61–65 (`CMAKE_SYSTEM_NAME STREQUAL "iOS"` → + `TC_PLATFORM_IOS`, backend METAL); platform sources globbed `platform/ios/*.mm` + (117–118); **`SOKOL_METAL` defined PUBLIC at 198** (`elseif(TC_PLATFORM_IOS)`). No + `SOKOL_GLES3` on iOS anywhere → the EAGL path is unreachable. Confirms §0/§10. +- **`core/platform/ios/tcPlatform_ios.mm`** — every sokol touchpoint: + - `setOrientation` → `sapp_ios_set_supported_orientations` (454) — §8(1). + - `extern bool _sapp_ios_immersive_mode;` (421) + `setImmersiveMode`/`getImmersiveMode` + (423–434) — §8(2). **The port must keep this non-static extern.** + - `captureWindow` (136–226): reads the drawable this frame rendered into via + `internal::currentWindowContext().lastSwapchainDrawable`, falling back to + `sapp_get_swapchain().metal.current_drawable` (141–142); blits to a shared-storage + staging texture (because `framebufferOnly=false` but reading the drawable directly + still asserts under validation); unpacks `MTLPixelFormatRGB10A2Unorm` → RGBA8 + (204–214). Depends on the RGB10A2 layer patch (§2) and framebufferOnly=false (issue + #56). +- **`core/platform/ios/tcFileDialog_ios.mm`:** `sapp_ios_get_window()` (23) → root VC to + present the document picker — §9. The ONLY consumer of the window getter. +- **`TrussC.h`:** `#define SOKOL_NO_ENTRY` (15); `_event_cb` switch handles + `TOUCHES_BEGAN/MOVED/ENDED/CANCELLED` (2373–2440) and `RESIZED` (2444), but **no + SUSPENDED/RESUMED case** (§3 finding); `runApp` → `sapp_run` on the non-Android path + (2634–2637). `buildAppDescriptor` sets no `desc.ios.*` (keyboard_resizes_canvas stays + false, §7). +- **`addons/tcxImGui/src/sokol_imgui.h`:** `sapp_show_keyboard`/`sapp_keyboard_shown` + (2661–2665) — §7 consumer. +- **`core/resources/Info-iOS.plist.in`:** `LSRequiresIPhoneOS`, + `UISupportedInterfaceOrientations`(~ipad), usage strings. **No + `UIApplicationSceneManifest`** — scene lifecycle is adopted programmatically (§1). No + `UILaunchStoryboardName` value. `trussc_app.cmake` wires this plist at 1023–1029 and + sets `TARGETED_DEVICE_FAMILY "1,2"`, automatic code signing. + +**Used and must be preserved on iOS:** `_sapp_ios_run` → `UIApplicationMain` + +`_sapp_scene_delegate` (programmatic scene config); Metal init/swapchain/display-link; +per-frame `update_dimensions` with the view-bounds + drawable-readback patches; touch +events (identifier + changed); on-screen keyboard via UITextField; RGB10A2 layer + MSAA ++ framebufferOnly=false; suspend/resume display-link pause; `sapp_ios_get_window`; +`sapp_ios_set_supported_orientations`; the non-static `_sapp_ios_immersive_mode` global; +`sapp_get_environment`/`sapp_get_swapchain` Metal contract. + +**Not applicable / gaps on iOS:** §10 table (mouse/clipboard/drop/title/icon/fullscreen/ +quit/skip_present/secondary-windows/GLES3); SUSPENDED/RESUMED fire but TrussC drops them +(§3); tvOS press code compiled but unexercised (§6). + +--- + +## 12. PORT CHECKLIST (`sokol_app_tc.h` `TARGET_OS_IPHONE` section) + +### (a) Lift verbatim (line ranges in sokol_app.h) +1. **Metal texture/swapchain helpers** — 6373–6432 (`_sapp_ios_mtl_create_texture`, + `_swapchain_create/destroy/resize/next`). Keep the **RGB10A2 MSAA format** (6407) and + depth `Depth32Float_Stencil8`. +2. **Metal init + layer setup** — 6474–6501. Keep **all TrussC patches**: + `framebufferOnly=false` (6487), `pixelFormat=RGB10A2Unorm` (6488), the + `_sapp_ios_view_ctrl` subclass (6494), and the "layer as sublayer of view.layer" + arrangement (6491). +3. **Display link + timing** — 6434–6472 (start/stop link, timing init/update/duration). +4. **`update_framebuffer_dimensions`** — 6511–6532 (TrussC set-then-readback patch #7). +5. **`update_dimensions`** — 6657–6676 (view-bounds patch #6 + fallback + RESIZED guard). +6. **`_sapp_ios_frame`** — 6678–6690 (timing + `@autoreleasepool` + `_sapp_frame`). +7. **Touch event** — 6636–6655 (allTouches enumeration, identifier, `changed`). +8. **On-screen keyboard** — 6692–6725 + `_sapp_textfield_dlg` 6799–6874 (CHAR/KEY bridge, + keyboard notifications). +9. **Scene delegate** — 6727–6797 (programmatic `UISceneConfiguration`, willConnect setup, + sceneWillResignActive/DidBecomeActive suspend/resume+display-link pause, willTerminate + cleanup order). +10. **View + view-controller** — 6876–6930 (`_sapp_ios_view` displayLinkFired/touches; + `_sapp_ios_view_ctrl` orientation/immersive overrides; keep + `bool _sapp_ios_immersive_mode = false;` NON-STATIC at 6919). +11. **@interface decls** — 2827–2879 (scene delegate, textfield delegate, + `_sapp_ios_view_ctrl`, `_sapp_ios_view`, `_sapp_ios_t` struct). +12. **Public API iOS branches** — `sapp_run` (13945–13946), `sapp_frame_duration` + (13991–13992), `sapp_show_keyboard` (14087–14088), env/swapchain iOS branches + (14397, 14425–14428), `sapp_ios_get_window` (14498–14506), + `sapp_ios_set_supported_orientations` (14508–14520). + +### (b) Adapt to the tc window model (single window = window #0) +1. **`sapp_create_window()` and plural-window APIs → invalid handle + log** (like web). + The scene owns exactly one `UIWindow`; a second is not representable. +2. **New `sapp_window_*` getters** → for window #0 return real values (the iOS + environment/swapchain, `_sapp.ios.window`); any other index → 0 + log. +3. **`_sapp.ios.window` is the "window handle."** No HWND/XID analogue beyond + `sapp_ios_get_window()`'s `UIWindow*`. +4. **DPI is single, screen-wide** (`windowScene.screen.nativeScale`, gated by + `high_dpi`). No per-window DPI. +5. **Compile as ObjC++** (`#error` guard) — reuse the macOS branch's guard. +6. **Owned loop lives in UIKit:** `_sapp_ios_run` calls `UIApplicationMain` (blocks); the + frame callback is `displayLinkFired:`. No PeekMessage/XPending/rAF equivalent to write. + +### (c) Known hazards +1. **Suspend MUST pause the display link** (§3) — rendering to a Metal drawable while + backgrounded triggers the iOS watchdog. Don't drop the `display_link.paused` toggle. +2. **`_sapp_ios_immersive_mode` symbol name is an ABI dependency** — `tcPlatform_ios.mm` + has `extern bool _sapp_ios_immersive_mode;`. Keep it non-static, same name (§8). +3. **Programmatic scene config is load-bearing** (§1) — the TrussC Info.plist has no + scene manifest; the delegate's `configurationForConnectingSceneSession:` is the only + thing making scene lifecycle work. +4. **Drawable-size readback** (§5, 6526–6530) — skip it and scissor-exceeds-render-pass + validation aborts on some devices. +5. **`applicationWillTerminate:` rarely runs** (SIGKILL) — do not rely on clean teardown. +6. **`nextDrawable` exactly once per frame** (§9) — twice acquires two drawables. +7. **GLES3/EAGL path is DEAD** — do not port 6535–6573 / 6561 / 6566 / the `_SAPP_ANY_GL` + frame-buffer-binding read (6683–6685). iOS is Metal-only in TrussC (§11). + +### (d) Device-verification checklist (iOS is NOT in CI — verify on the user's iPhone) +There is no CI coverage for iOS; the only gate is a real device. Before calling the port +done, confirm on-device: +- [ ] App launches, renders (RGB10A2 layer, correct colors, no banding), holds framerate. +- [ ] **Touch**: multi-touch works; began/moved/ended/cancelled reach app; `identifier` + stable across a drag; coordinates correct at Retina scale. +- [ ] **Rotation**: rotate device → RESIZED fires, framebuffer + drawable resize, no + scissor/validation abort; both portrait and landscape. +- [ ] **Orientation lock**: `setOrientation(...)` (→ `sapp_ios_set_supported_orientations`) + actually constrains rotation (test iOS 16+ path). +- [ ] **On-screen keyboard**: ImGui text field (tcxImGui) raises/dismisses the keyboard; + CHAR + Enter/Space/Backspace events arrive. +- [ ] **Suspend/resume**: background then foreground → display link pauses (no GPU work / + no watchdog kill) and resumes; SUSPENDED/RESUMED fire at the sokol layer. +- [ ] **Immersive mode**: `setImmersiveMode(true)` hides the status bar + home indicator; + `false` restores them. +- [ ] **Document picker**: `tcFileDialog` open/save presents (proves `sapp_ios_get_window` + → root VC still anchors the picker). +- [ ] **Screenshot**: `captureWindow()` returns correct pixels (RGB10A2 unpack path). + +--- + +## Appendix: function → line index (iOS Metal-relevant) + +| symbol | line | +|---|---| +| iOS platform detect (`_SAPP_IOS` @2322, API `#error` @2323) | 2314–2329 | +| iOS UIKit/Metal includes (GLES3 GLKit path @2441 DEAD) | 2435–2447 | +| Apple ARC / `_SAPP_OBJC_RELEASE` / timestamp (mach) | 2554–2630 | +| `sapp_ios_desc` (`keyboard_resizes_canvas`) | 2031–2033 | +| `sapp_ios_get_window` / `sapp_ios_set_supported_orientations` decls | 2233 / 2235 | +| iOS @interface decls (`_sapp_scene_delegate`, textfield dlg, view_ctrl, view) | 2827–2848 | +| `_sapp_ios_t` struct | 2850–2877 | +| `_sapp_ios_t ios;` embed | 3301 | +| `_sapp_ios_max_fps` | 6367 | +| `_sapp_ios_mtl_create_texture` | 6373 | +| `_sapp_ios_mtl_swapchain_create` (MSAA RGB10A2 @6407) / `destroy` / `resize` / `next` | 6401 / 6414 / 6423 / 6428 | +| `_sapp_ios_mtl_timing_init` / `update` / `frame_duration` | 6434 / 6439 / 6451 | +| `_sapp_ios_mtl_start_display_link` / `stop` | 6456 / 6466 | +| `_sapp_ios_mtl_init` (framebufferOnly=false @6487, RGB10A2 @6488, view_ctrl @6494) | 6474 | +| `_sapp_ios_mtl_discard_state` | 6503 | +| `_sapp_ios_mtl_update_framebuffer_dimensions` (patch #7 readback @6526) | 6511 | +| `_sapp_ios_gles3_init` / `discard` / `update_fb_dims` — **DEAD** | 6536 / 6561 / 6566 | +| `_sapp_ios_discard_state` | 6575 | +| `_sapp_ios_run` (UIApplicationMain) | 6588 | +| `main` (SOKOL_NO_ENTRY out) | 6595 | +| `_sapp_ios_app_event` | 6604 | +| `_sapp_tvos_press_event` — DEAD-but-kept | 6611 | +| `_sapp_ios_touch_event` | 6636 | +| `_sapp_ios_update_dimensions` (patch #6 view.bounds @6661) | 6657 | +| `_sapp_ios_frame` | 6678 | +| `_sapp_ios_show_keyboard` | 6692 | +| `@implementation _sapp_scene_delegate` (config @6728, willConnect @6737, resign @6763, active @6775, terminate @6791) | 6727 | +| `@implementation _sapp_textfield_dlg` (char/key bridge @6831) | 6799 | +| `@implementation _sapp_ios_view` (displayLinkFired @6878, touches @6903) | 6876 | +| `_sapp_ios_immersive_mode` global + `_sapp_ios_view_ctrl` impl | 6919 / 6920 | +| `#endif /* TARGET_OS_IPHONE */` | 6932 | +| `sapp_run` → `_sapp_ios_run` | 13945–13946 | +| `sapp_frame_duration` (iOS Metal branch) | 13991–13992 | +| `sapp_show_keyboard` / `sapp_keyboard_shown` (iOS) | 14086 / 14096 | +| `sapp_color_format` RGB10A2 (Metal) / `sapp_depth_format` | 14040 / 14048 | +| `sapp_get_environment` (iOS metal.device @14397) | 14387 | +| `sapp_get_swapchain` (iOS metal branch @14425–14428) | 14417 | +| `sapp_ios_get_window` | 14498 | +| `sapp_ios_set_supported_orientations` (iOS16 nudge @14512) | 14508 | +| helpers: `_sapp_timing_clamp` / `_sapp_roundf_gzero` / `_sapp_call_cleanup` | 2666 / 3423 / 3450 | diff --git a/docs/dev/sapp-mac-impl-spec.md b/docs/dev/sapp-mac-impl-spec.md new file mode 100644 index 00000000..fa55ea89 --- /dev/null +++ b/docs/dev/sapp-mac-impl-spec.md @@ -0,0 +1,400 @@ +# sokol_app.h macOS (Metal) Backend — Behavioral Specification + +Implementation contract for replacing sokol_app.h's macOS main-window / app-lifecycle +with `sokol_app_tc.h`. All line references are into +`core/include/sokol/sokol_app.h` (14602 lines, this worktree). Focus: `_SAPP_MACOS` ++ `SOKOL_METAL`. GL/WGPU paths noted only where they diverge; iOS/Win/Linux/web ignored. + +The replacement already covers secondary windows (NSWindow+CAMetalLayer, CADisplayLink, +event mapping w/ 111-key table, occlusion gating, depth/MSAA textures). This document +specifies the **main window + app lifecycle** that is still missing. + +--- + +## 0. Global state touched (the shared contract) + +The single global is `static _sapp_t _sapp;` (definition around line 3298 embeds +`_sapp_macos_t macos;`). The macOS struct is at **2790–2823**: + +```c +typedef struct { + uint32_t flags_changed_store; // last NSEventModifierFlags seen by flagsChanged + uint8_t mouse_buttons; // bitmask of held buttons (enter/leave suppression during drag) + NSWindow* window; + NSTrackingArea* tracking_area; + id keyup_monitor; // local event monitor (Cmd+key-up workaround) + _sapp_macos_app_delegate* app_dlg; + _sapp_macos_window_delegate* win_dlg; + _sapp_macos_view* view; + NSCursor* standard_cursors[_SAPP_MOUSECURSOR_NUM]; + NSCursor* custom_cursors[_SAPP_MOUSECURSOR_NUM]; + struct { // SOKOL_METAL only + id device; + CAMetalLayer* layer; + CADisplayLink* display_link; + NSTimer* fallback_timer; + id depth_tex; + id msaa_tex; + struct { CFTimeInterval timestamp; CFTimeInterval frame_duration_sec; } timing; + } mtl; +} _sapp_macos_t; +``` + +Cross-platform `_sapp` fields the mac path reads/writes: `desc`, `valid`, +`first_frame`, `init_called`, `cleanup_called`, `quit_requested`, `quit_ordered`, +`fullscreen`, `window_width/height`, `framebuffer_width/height`, `dpi_scale`, +`sample_count`, `swap_interval`, `frame_count`, `timing`, `mouse.*`, `clipboard.*`, +`drop.*`, `event`, `keycodes[]`, `window_title`, `custom_cursor_bound[]`, +`event_consumed`, `skip_present`, `onscreen_keyboard_shown`. + +`_sapp_init_state()` (**3554**) is called first by `_sapp_macos_run` and initializes +all of the above from `desc` (defaults applied by `_sapp_desc_defaults` at **3524**: +sample_count→1, swap_interval→1, clipboard_size→8192, max_dropped_files→1, +max_dropped_file_path_length→2048, window_title→"sokol"). Notably it sets +`first_frame=true`, `dpi_scale=1.0`, `mouse.shown=true`, `fullscreen=desc.fullscreen`, +and copies width/height verbatim (**may be 0** — the mac backend must resolve 0). + +--- + +## 1. NSApplication bootstrap + +### Entry (`_sapp_macos_run`, 5519–5542; `main`, 5545–5551; `sapp_run`, 13941) + +Order is exact and load-bearing: + +1. `_sapp_init_state(desc)` — zero+init global state. +2. `_sapp_macos_init_keytable()` — fill `_sapp.keycodes[]` (111 entries, 5359–5471). +3. `[NSApplication sharedApplication]` — creates NSApp (does **not** set activation policy yet). +4. `sapp_set_icon(&_sapp.desc.icon)` — set dock icon "as early as possible" (comment at 5524). This calls `_sapp_macos_set_icon` (5816) which writes `NSApp.dockTile.contentView`. **Main-window path does NOT require icon support** — it is a dock-tile decoration only, safe to stub/skip. `sapp_set_icon` (14291) no-ops when `num_images==0` / default not requested. +5. `_sapp.macos.app_dlg = [[_sapp_macos_app_delegate alloc] init]; NSApp.delegate = app_dlg;` +6. Install **Cmd-key-up workaround** (5530–5537): a block-based `addLocalMonitorForEventsMatchingMask:NSEventMaskKeyUp`. When a keyUp arrives while `NSEventModifierFlagCommand` is held, it force-forwards the event to `[NSApp keyWindow]` (Cocoa otherwise swallows key-ups under Cmd). The monitor id is stored in `_sapp.macos.keyup_monitor`. Removed in `_sapp_macos_discard_state` via `[NSEvent removeMonitor:]` (which also releases it). +7. `[NSApp run]` — **never returns**. All cleanup lives in `applicationWillTerminate`. + +`SOKOL_NO_ENTRY`: when defined, the `int main(...)` at 5546 is compiled out; the user +supplies their own entry that calls `sapp_run(desc)` → `_sapp_macos_run`. Otherwise +sokol's `main` calls `sokol_main(argc,argv)` to obtain `desc` then `_sapp_macos_run`. + +### NSApplicationDelegate methods (`@implementation _sapp_macos_app_delegate`, 5872–5945) + +- **`applicationDidFinishLaunching:` (5873–5932)** — the real app setup, runs inside `[NSApp run]`. See §2. +- **`applicationShouldTerminateAfterLastWindowClosed:` (5934–5937)** — returns `YES`. Closing the (sole) window terminates the app. +- **`applicationWillTerminate:` (5939–5944)** — the cleanup site. Runs, in order: `_sapp_call_cleanup()` (user cleanup_cb, once), `_sapp_macos_discard_state()` (release ObjC objects incl. Metal device/layer/textures, stop display link/timer, remove tracking area + keyup monitor), `_sapp_discard_state()` (free clipboard/drop/icon buffers, unbind custom cursors, zero `_sapp`). + +No `applicationShouldTerminate:` is implemented — termination is driven by window close +(see §3). **sokol does NOT create a default menu bar** (no `NSMenu`/`mainMenu` setup +anywhere in the mac path). Activation policy is `NSApplicationActivationPolicyRegular` +set inside `applicationDidFinishLaunching` (5876), explicitly kept **before window +creation** (comment cites floooh/sokol#1500). + +--- + +## 2. Main window creation (`applicationDidFinishLaunching:`, 5873–5932) + +Exact sequence: + +1. `NSApp.activationPolicy = NSApplicationActivationPolicyRegular;` (must precede window creation). +2. `_sapp_macos_init_cursors()` (5502) — populate `standard_cursors[]` (see §6). +3. If `window_width==0 || window_height==0` → `_sapp_macos_init_default_dimensions()` (5608): sets `dpi_scale` (backingScaleFactor if `high_dpi` else 1.0), computes default = **4/5 of `NSScreen.mainScreen.frame`** for each 0 dimension, and sets `framebuffer_width/height = default * dpi_scale`. +4. **Style mask is fixed** (5881–5885): `Titled | Closable | Miniaturizable | Resizable`. There is **no borderless / no fullscreen-derived style** — `desc.fullscreen` does not change the style mask; fullscreen is entered via `toggleFullScreen:` after creation (step 11). +5. `window_rect = NSMakeRect(0,0,window_width,window_height)` (content rect, points). +6. `_sapp.macos.window = [[_sapp_macos_window alloc] initWithContentRect:window_rect styleMask:style backing:NSBackingStoreBuffered defer:NO]`. The custom `_sapp_macos_window initWithContentRect:` (6044) also calls `registerForDraggedTypes:@[NSPasteboardTypeFileURL]` (drag&drop is always registered on the main window regardless of `enable_dragndrop`; the *handler* gates on `_sapp.drop.enabled` indirectly, see §7). +7. `window.releasedWhenClosed = NO` — **required** so the window object survives `performClose`/close until `applicationWillTerminate` cleanup (comment at 5892). +8. `window.title = NSString(window_title)`, `window.acceptsMouseMovedEvents = YES`, `window.restorable = YES`. +9. `win_dlg = [[_sapp_macos_window_delegate alloc] init]; window.delegate = win_dlg;` +10. Renderer init: **`_sapp_macos_mtl_init()` (5211)** for Metal (creates device, CAMetalLayer, view, starts display link, inits timing — see §10). Then `window.contentView = _sapp.macos.view; [window makeFirstResponder:view]; [window center];` +11. `_sapp.valid = true;` (set **before** fullscreen toggle — comment 5911 notes GL renders a frame during toggle). If `_sapp.fullscreen`: `[window toggleFullScreen:self]`. +12. `[NSApp activateIgnoringOtherApps:YES];` then `[window makeKeyAndOrderFront:nil];` +13. `_sapp_macos_update_dimensions()` (5632) — sample real bounds, resize swapchain, but **suppresses** the RESIZED event because `first_frame` is still true (see §11). +14. `[NSEvent setMouseCoalescingEnabled:NO]` — full-resolution mouse-move events. +15. **Focus workaround** (5919–5931, cites sokol#982): posts a synthetic `NSEventTypeAppKitDefined` / `NSEventSubtypeApplicationActivated` event to `NSApp` at-start, so the window is focused even if the init callback blocks for a long time. + +### High-DPI / dpi_scale sourcing + +- `desc.high_dpi` gates everything. `dpi_scale` is sampled from `backingScaleFactor` of the *screen* (`NSScreen.mainScreen.backingScaleFactor` at startup in `_sapp_macos_init_default_dimensions`; `[_sapp.macos.window screen].backingScaleFactor` on every `_sapp_macos_update_dimensions`, 5634). If `!high_dpi`, `dpi_scale = 1.0` always. +- `framebuffer = round(view.bounds.size * dpi_scale)`; `window_width/height = round(view.bounds.size)` in points. +- `view.layer.contentsScale = dpi_scale` is re-applied every update (5639) because `windowWillStartLiveResize` sets a non-scaling `layerContentsPlacement`. +- `sapp_high_dpi()` (14056) returns `desc.high_dpi && dpi_scale >= 1.5` (NOT just `high_dpi`). + +Initial size logic: `desc.width/height==0` → 4/5 of main screen. Centering via +`[window center]`. Title from `window_title`. `collectionBehavior` is **not** set +(default) — standard AppKit fullscreen behavior applies via `toggleFullScreen:`. + +--- + +## 3. Quit flow semantics (critical) + +State flags: `quit_requested`, `quit_ordered` (both bool in `_sapp`). + +### Public API +- `sapp_request_quit()` (14225): `quit_requested = true;` (nothing else). +- `sapp_cancel_quit()` (14229): `quit_requested = false;`. +- `sapp_quit()` (14233): `quit_ordered = true;` (hard, unconditional — no user veto). + +### How a quit reaches the OS + +There is **no NSApp terminate call** in the mac path. Termination is driven entirely +through **window close**: + +1. **`_sapp_macos_frame()` tail (5867–5869):** after every frame, `if (quit_requested || quit_ordered) [_sapp.macos.window performClose:nil];`. So a `sapp_request_quit()` or `sapp_quit()` made from user code takes effect on the *next frame boundary* by asking the window to close. +2. **`windowShouldClose:` (5948–5966)** — the decision point, invoked by `performClose:`, the red close button, or Cmd+Q routed to the window: + ``` + if (!quit_ordered) { // sapp_quit() not already called + quit_requested = true; + fire SAPP_EVENTTYPE_QUIT_REQUESTED; // user can call sapp_cancel_quit() here + if (quit_requested) quit_ordered = true; // user did NOT cancel → commit + } + return quit_ordered ? YES : NO; // YES closes the window + ``` + - **Red close button / Cmd+Q path:** `quit_ordered` starts false → QUIT_REQUESTED is delivered synchronously; if the event handler calls `sapp_cancel_quit()`, `quit_requested` becomes false, `quit_ordered` stays false, returns **NO** (window stays open). Otherwise returns **YES**. + - **`sapp_quit()` path:** `quit_ordered` already true → skips the whole block, no QUIT_REQUESTED fired, returns **YES** immediately (unvetoable). +3. Window actually closes → (sole window) `applicationShouldTerminateAfterLastWindowClosed:`=YES → app terminates → **`applicationWillTerminate:`** runs cleanup: `_sapp_call_cleanup()` (frame/user cleanup_cb) **then** `_sapp_macos_discard_state()` (which releases the Metal device etc.). **Ordering guarantee:** user `cleanup_cb` runs *before* any GPU/Metal teardown. (TrussC does its own `sg_shutdown` inside cleanup_cb; sokol only releases the *view/layer/device* afterward — sokol_gfx shutdown is the app's responsibility.) + +QUIT_REQUESTED event population: plain `_sapp_init_event(SAPP_EVENTTYPE_QUIT_REQUESTED)` +via `_sapp_macos_app_event` (5600), only if `_sapp_events_enabled()` (event_cb set AND +`init_called`). + +--- + +## 4. Frame timing + +Two layers: the platform-agnostic filtered timer (`_sapp.timing`, a `_sapp_timing_t`) +and a Metal-specific CADisplayLink-timestamp timer (`_sapp.macos.mtl.timing`). + +### Platform-agnostic filter (`_sapp_timing_*`, 2643–2711) +Config (`_sapp_timing_init`, 2655): `dt_min=1µs`, `dt_max=100ms`, `dt_threshold=4ms`, +`alpha=0.025`, initial `dt=ema=smooth_dt=1/60`. + +`_sapp_timing_update(t, external_now)` (2694): `now = external_now==0 ? timestamp_now() +: external_now`. On the **first** call `t->last==0` so no delta is produced (just stores +`last=now`). Subsequent calls compute `dt = now - last` → `_sapp_timing_delta`. + +`_sapp_timing_delta` (2677): clamp raw `dt` to [dt_min,dt_max] → store as unfiltered +`t->dt`. Then `error = |dt - smooth_dt|`; if `error > dt_threshold` **reset** the filter +(`ema = smooth_dt = dt`), else EMA: `ema += alpha*(dt - ema); smooth_dt = clamp(ema)`. + +`_sapp_timing_get` (2709) returns `smooth_dt`. `sapp_frame_duration_unfiltered()` +(13998) returns raw `t->dt`. + +### Metal CADisplayLink timing (`_sapp_macos_mtl_timing_*`, 5126–5154) +Because `CADisplayLink.timestamp` is very stable, Metal uses it *instead of* the measured +filter when the display link is active: +- `_timing_init` (5126): `timestamp=0`, `frame_duration_sec = 1.0/_sapp_macos_max_fps()` (`max_fps = [NSScreen.mainScreen maximumFramesPerSecond]`, 5056). +- `_timing_update` (5131), called each frame from `_sapp_macos_frame`: only when display link active. `cur = display_link.timestamp`; **first frame is skipped** (`timing.timestamp>0` guard) — `frame_duration_sec` keeps its refresh-rate seed; otherwise `dt = cur - prev`, run through `_sapp_timing_clamp` (min/max clamp only, NOT the EMA) → `frame_duration_sec`. Store `timestamp=cur`. +- `_timing_frame_duration` (5147): if display link active return `frame_duration_sec`; else fall back to `_sapp_timing_get(&_sapp.timing)` (the EMA) — this is the **occluded / fallback-timer** case. + +`sapp_frame_duration()` (13988) on macOS+Metal → `_sapp_macos_mtl_timing_frame_duration()`. + +### Frame count +`_sapp.frame_count` incremented in `_sapp_frame()` (3655), *after* `_sapp_call_frame()`. +On the very first `_sapp_frame` call `first_frame` is cleared and `init_cb` is called +before the first `frame_cb`. `sapp_frame_count()` (13984) returns the raw counter. +`_sapp_init_event` stamps `event.frame_count = _sapp.frame_count` (3617), so events fired +during frame N carry count N (count is incremented only after the frame_cb returns). +First frame value: 0 (frame_cb runs with count still 0, becomes 1 after). + +Per-frame timing driver (`_sapp_macos_frame`, 5849): `_sapp_timing_update(&_sapp.timing, 0.0)` +runs **every** frame (feeds the fallback path), then `_sapp_macos_mtl_timing_update()`, +then (inside `@autoreleasepool`) `_sapp_frame()`. + +--- + +## 5. Clipboard + +Enabled only if `desc.enable_clipboard`. `_sapp_init_state` allocates +`clipboard.buffer` of `clipboard_size` bytes (default 8192) and sets `clipboard.enabled`, +`clipboard.buf_size` (3576–3580). + +- **`sapp_set_clipboard_string(str)` (14242):** no-op if `!clipboard.enabled`. Calls `_sapp_macos_set_clipboard_string` (5664): inside `@autoreleasepool`, `[[NSPasteboard generalPasteboard] declareTypes:@[NSPasteboardTypeString] owner:nil]` then `setString:@(str) forType:NSPasteboardTypeString`. Then also copies `str` into `_sapp.clipboard.buffer`. +- **`sapp_get_clipboard_string()` (14261):** returns `""` if disabled; else `_sapp_macos_get_clipboard_string` (5672): zeroes `buffer[0]`, checks pasteboard `types` contains `NSPasteboardTypeString`, reads `stringForType:` UTF8 into `buffer` (clamped to `buf_size`). Returns the internal buffer pointer. +- **CLIPBOARD_PASTED on mac:** fired from `keyDown:` (6292–6296): if `clipboard.enabled && mods == SAPP_MODIFIER_SUPER && key_code == SAPP_KEYCODE_V`, a `SAPP_EVENTTYPE_CLIPBOARD_PASTED` event is emitted (after the KEY_DOWN and CHAR events). **Exact-match** on modifiers: `mods == SAPP_MODIFIER_SUPER` (Cmd alone; Cmd+Shift+V would not match). Unlike Win/Linux, mac does *not* pre-populate the clipboard buffer before the event — the app is expected to call `sapp_get_clipboard_string()` in its handler. + +--- + +## 6. Mouse cursor + +### Show/hide +- `sapp_show_mouse(show)` (14131): **does not stack** (comment 14130). Only acts when `mouse.shown != show`, routing through `_sapp_update_cursor(current_cursor, show)` (14116) which calls `_sapp_macos_update_cursor` then stores `mouse.current_cursor`/`mouse.shown`. +- `_sapp_macos_show_mouse(visible)` (5715) — used by lock path only; uses `CGDisplayShowCursor`/`CGDisplayHideCursor(kCGDirectMainDisplay)`. +- `_sapp_macos_update_cursor(cursor, shown)` (5750): if `shown != mouse.shown` it stacks `[NSCursor unhide]`/`[NSCursor hide]` (note: this *does* stack, so it is gated on an actual change). Then selects the NSCursor: custom if `custom_cursor_bound[cursor]`, else `standard_cursors[cursor]`, else `[NSCursor arrowCursor]`; `[ns_cursor set]`. + +### Standard cursor table (`_sapp_macos_init_cursors`, 5502–5517) +`ARROW→arrowCursor`, `IBEAM→IBeamCursor`, `CROSSHAIR→crosshairCursor`, +`POINTING_HAND→pointingHandCursor`, and the resize cursors use **undocumented private +selectors** declared in a category (5495–5500): +`RESIZE_EW→_windowResizeEastWestCursor` (fallback `resizeLeftRightCursor`), +`RESIZE_NS→_windowResizeNorthSouthCursor` (fb `resizeUpDownCursor`), +`RESIZE_NWSE→_windowResizeNorthWestSouthEastCursor` (fb `closedHandCursor`), +`RESIZE_NESW→_windowResizeNorthEastSouthWestCursor` (fb `closedHandCursor`), +`RESIZE_ALL→closedHandCursor`, `NOT_ALLOWED→operationNotAllowedCursor`. +Each uses `[NSCursor respondsToSelector:...] ? private : fallback`. + +### Custom cursors +- `sapp_bind_mouse_cursor_image(cursor, desc)` (14170): asserts hotspot `< dim-1` and `pixels.size == w*h*4`. Unbinds any existing, then `_sapp_macos_make_custom_mouse_cursor` (5774): builds an `NSBitmapImageRep` (RGBA8, `NSBitmapFormatAlphaNonpremultiplied`, `NSCalibratedRGBColorSpace`), `memcpy`s `desc->pixels`, wraps in `NSImage`, creates `[[NSCursor alloc] initWithImage:hotSpot:]` into `custom_cursors[cursor]`. Sets `custom_cursor_bound[cursor]=res`. If the bound cursor is current, re-applies immediately. Returns the passed-in cursor. +- `sapp_unbind_mouse_cursor_image` (14203): if bound, clears the flag, re-applies default if current (before destroy), then `_sapp_macos_destroy_custom_mouse_cursor` releases the NSCursor. There are `_SAPP_MOUSECURSOR_NUM` slots (the "CUSTOM_0..15" model is per-enum-slot; on mac every standard cursor enum can be overridden by a bound custom image). + +### Cursor re-application (tracking area interplay) +`updateTrackingArea` (6134) builds an `NSTrackingArea` with options +`MouseEnteredAndExited | ActiveInKeyWindow | EnabledDuringMouseDrag | CursorUpdate | +InVisibleRect | AssumeInside`. Because `CursorUpdate` is set, AppKit calls +**`cursorUpdate:` (6351)** which re-applies `_sapp_macos_update_cursor(current_cursor, +shown)` whenever the pointer (re)enters — this is what keeps a custom/hidden cursor +sticky across window regions. `mouseEntered:` (6157) also re-syncs mouse position. + +--- + +## 7. Drag & drop + +- Registration: **always** on the main window (`registerForDraggedTypes:@[NSPasteboardTypeFileURL]` in `_sapp_macos_window initWithContentRect:` at 6050, guarded by `__MAC_OS_X_VERSION_MAX_ALLOWED >= 101300`). Not gated on `enable_dragndrop`; the buffer/handler is what gates. +- `desc.enable_dragndrop` → `_sapp_init_state` allocates `drop.buffer` of `max_files * max_path_length` bytes and sets `drop.enabled/max_files/max_path_length/buf_size` (3581–3587). Defaults: 1 file, 2048 bytes/path. +- NSDraggingDestination methods on `_sapp_macos_window` (6056–6094): `draggingEntered:`→`NSDragOperationCopy`, `draggingUpdated:`→`NSDragOperationCopy`, `performDragOperation:` does the extraction. +- `performDragOperation:` (6064): if pasteboard has `NSPasteboardTypeFileURL`: `_sapp_clear_drop_buffer()`; `num_files = min(pasteboardItems.count, drop.max_files)`; for each item build `[NSURL fileURLWithPath:[item stringForType:NSPasteboardTypeFileURL]]` and `_sapp_strcpy(fileUrl.standardizedURL.path.UTF8String, _sapp_dropped_file_path_ptr(i), max_path_length)`. If any path overflows → `_SAPP_ERROR(DROPPED_FILE_PATH_TOO_LONG)`, abort, clear buffer, `num_files=0`. On success, if `_sapp_events_enabled()`: update mouse pos from `draggingLocation`, fire `SAPP_EVENTTYPE_FILES_DROPPED` with `modifiers = _sapp_macos_mods(nil)`. Returns YES if handled. +- Retrieval: `sapp_get_num_dropped_files()` (14319) returns `drop.num_files` (0 if disabled); `sapp_get_dropped_file_path(i)` (14326) returns `_sapp_dropped_file_path_ptr(i)`. +- Path extraction uses **NSURL / NSPasteboardTypeFileURL**, not the legacy NSFilenamesPboardType. + +--- + +## 8. Fullscreen + +- `sapp_is_fullscreen()` (14100): returns `_sapp.fullscreen` (a tracked bool, NOT a styleMask query). +- `sapp_toggle_fullscreen()` (14104) → `_sapp_macos_toggle_fullscreen` (5655): flips `_sapp.fullscreen` **and** calls `[window toggleFullScreen:nil]`. Note the flag is toggled optimistically here **and** authoritatively corrected by the notifications below — a double source of truth. +- `windowDidEnterFullScreen:` (6032) sets `fullscreen=true`; `windowDidExitFullScreen:` (6037) sets `fullscreen=false`. These are the reliable authority (native OS fullscreen, mission-control gestures, etc.). +- Startup `desc.fullscreen=true`: after window creation and `valid=true`, `applicationDidFinishLaunching` calls `[window toggleFullScreen:self]` (5910–5913). The window is created windowed first, then toggled. + +--- + +## 9. sapp_get_environment / sapp_get_swapchain (Metal) + +### `sapp_get_environment()` (14387) — asserts `_sapp.valid` +- `defaults.color_format = sapp_color_format()` → **`SAPP_PIXELFORMAT_RGB10A2`** on Metal (TrussC 10-bit patch, 14040–14042; upstream would be BGRA8). +- `defaults.depth_format = sapp_depth_format()` → always `SAPP_PIXELFORMAT_DEPTH_STENCIL` (14048). +- `defaults.sample_count = sapp_sample_count()` → `_sapp.sample_count`. +- `metal.device = (__bridge const void*) _sapp.macos.mtl.device` (14395). + +### `sapp_get_swapchain()` (14417) — asserts `_sapp.valid` +Metal branch (14421–14424): +- `metal.current_drawable = (__bridge) _sapp_macos_mtl_swapchain_next()` — **`[layer nextDrawable]` is called HERE, lazily, at swapchain-query time** (5116; asserts non-nil). It is NOT acquired at frame start. TrussC calls `sglue_swapchain()` inside `sg_begin_pass`, so the drawable is acquired at begin-pass time each frame. Calling `sapp_get_swapchain()` twice per frame would acquire two drawables — must be called exactly once per frame. +- `metal.depth_stencil_texture = _sapp.macos.mtl.depth_tex` +- `metal.msaa_color_texture = _sapp.macos.mtl.msaa_tex` (nil when sample_count==1). +- Common tail: `width = sapp_width()` (framebuffer_width, min 1), `height = sapp_height()`, `color_format = RGB10A2`, `depth_format = DEPTH_STENCIL`, `sample_count`. + +### glue contract (`sokol_glue.h`) +`sglue_environment()` (159) reads only: `env.defaults.color_format/depth_format/sample_count` +and `env.metal.device` (maps sapp_pixel_format→sg_pixel_format via `_sglue_to_sgpixelformat` +which handles `RGB10A2→SG_PIXELFORMAT_RGB10A2`, 149). `sglue_swapchain()` (178) reads: +`sc.width/height/sample_count/color_format/depth_format` and +`sc.metal.current_drawable / depth_stencil_texture / msaa_color_texture`. **These are the +only sapp fields the shim must fill for the Metal render path.** All d3d11/wgpu/vulkan/gl +fields are memset-0 and ignored on mac. + +--- + +## 10. Metal view/layer details (main window) + +### Layer/view creation (`_sapp_macos_mtl_init`, 5211–5227) +- `device = MTLCreateSystemDefaultDevice()`. +- `layer = [CAMetalLayer layer]`; `layer.device = device`; `layer.magnificationFilter = kCAFilterNearest`; `layer.opaque = true`; **`layer.pixelFormat = MTLPixelFormatRGB10A2Unorm`** (TrussC 10-bit patch, 5217); **`layer.framebufferOnly = false`** (TrussC patch enabling `captureWindow()` GPU reads, issue #56, 5218). `maximumDrawableCount` left at default 3 (commented note about 2, 5219). `colorspace` not set (FIXME). +- `view = [[_sapp_macos_view alloc] init]; [view updateTrackingAreas]; view.wantsLayer = YES; view.layer = layer;` (a **CAMetalLayer-backed NSView**, not MTKView — TRUSSC_MODIFICATIONS note confirms upstream migrated off MTKView to CAMetalLayer). +- `_sapp_macos_mtl_start_display_link()` (5156), `_sapp_macos_mtl_timing_init()`. + +### Display link + swap interval +`_sapp_macos_mtl_start_display_link` (5156): if link exists, unpause and return. Else +`[view displayLinkWithTarget:view selector:@selector(displayLinkFired:)]`; sets +`preferredFrameRateRange = {p,p,p}` where `p = max_fps / swap_interval`; adds to current +run loop in `NSRunLoopCommonModes`. `displaySyncEnabled`/`maximumDrawableCount` are not +explicitly set; vsync is implied by the CAMetalLayer + display-link cadence and +`swap_interval` scales the preferred fps. `_sapp_macos_mtl_display_link_active()` = +link non-nil and not paused. + +### Occlusion / fallback timer (already handled for secondary windows, documented for parity) +`_transition_to_occluded` (5197): if active → stop display link (pause) + start a repeating +`NSTimer` at `_SAPP_MACOS_MTL_OBSCURED_FRAME_DURATION_IN_SECONDS = 0.0166667` (~60Hz) +firing `fallbackTimerFired:` (5119). `_transition_to_visible` (5204): stop fallback timer, +(re)start display link. Triggered by `windowDidChangeOcclusionState:` (6012, +`occlusionState & NSWindowOcclusionStateVisible`), `windowDidMiniaturize:`/`Deminiaturize:`. + +### Swapchain textures (`_sapp_macos_mtl_create_texture`, 5061; `_swapchain_create`, 5089) +- Depth: `MTLPixelFormatDepth32Float_Stencil8`, sample_count, panic on nil. +- MSAA color (only if `sample_count > 1`): **`MTLPixelFormatRGB10A2Unorm`** (TrussC patch, 5095). +- Descriptor: `textureType = MTLTextureType2DMultisample` if sample_count>1 else `2D`; `usage = MTLTextureUsageRenderTarget`; `resourceOptions = MTLResourceStorageModePrivate`; mip=1, arrayLength=1, depth=1. +- `_swapchain_resize` (5111) = destroy + create. Called from `_sapp_macos_mtl_update_framebuffer_dimensions` (5237) only when the drawable size actually changed; also sets `layer.drawableSize = {framebuffer_width, framebuffer_height}`. + +### RGB10A2 patch locations (must replicate all four) +1. `layer.pixelFormat = MTLPixelFormatRGB10A2Unorm` (5217). +2. MSAA texture format `MTLPixelFormatRGB10A2Unorm` (5095). +3. `sapp_color_format()` returns `SAPP_PIXELFORMAT_RGB10A2` on Metal/D3D11 (14042). +4. glue `_sglue_to_sgpixelformat` maps `RGB10A2→SG_PIXELFORMAT_RGB10A2` (149). +Plus `layer.framebufferOnly = false` (5218) for captureWindow reads. + +--- + +## 11. Window/app events delivered on mac (beyond raw input) + +All go through `_sapp_macos_app_event(type)` (5600) → gated by `_sapp_events_enabled()`. +`_sapp_init_event` (3614) populates every event with: `frame_count`, +`mouse_button=INVALID`, `window_width/height` (points), `framebuffer_width/height` +(pixels), `mouse_x/y/dx/dy`. Per-type specifics: + +- **RESIZED** — fired from `_sapp_macos_update_dimensions` (5632→5651) **only if** dimensions changed **and** `!first_frame` (so the startup update at step 13 is silent). Triggered by `windowDidResize:` (5986) and `windowDidChangeScreen:` (5991). Event carries the new `window_width/height` (points) and `framebuffer_width/height` (pixels) — both already updated before the event fires. +- **ICONIFIED** — `windowDidMiniaturize:` (5996); also transitions Metal to occluded first. +- **RESTORED** — `windowDidDeminiaturize:` (6004); transitions Metal to visible first. +- **FOCUSED** — `windowDidBecomeKey:` (6022). +- **UNFOCUSED** — `windowDidResignKey:` (6027). +- **QUIT_REQUESTED** — `windowShouldClose:` (see §3). +- **SUSPENDED / RESUMED** — **not used on macOS** (event table lines 156–157 mark them iOS/Android/web only; the mac window delegate never fires them). `windowDidChangeOcclusionState:` only drives Metal render gating, it does NOT emit an app event. +- **DISPLAY_CHANGED** — no such event type on mac; `windowDidChangeScreen:` re-runs `update_dimensions` (may emit RESIZED if dpi/size changed) but there is no dedicated display-changed event. +- FILES_DROPPED / CLIPBOARD_PASTED — see §7 / §5 (fired from window/view, not the delegate). + +`_sapp_events_enabled()` (3629) requires `(event_cb || event_userdata_cb) && init_called` +— so **no events fire before the first frame's init_cb** runs. This is why the RESIZED +`!first_frame` guard matters (events would be dropped anyway pre-init, but the guard also +covers the case where init already ran). + +--- + +## 12. Misc + +- **`sapp_set_window_title(str)` (14279):** copies into `_sapp.window_title`, then `_sapp_macos_update_window_title` (5689): `[window setTitle:NSString(window_title)]`. +- **`sapp_high_dpi()` (14056):** `desc.high_dpi && dpi_scale >= 1.5` (not a plain flag). +- **`sapp_isvalid()` (13972):** returns `_sapp.valid` (set true at 5909, inside `applicationDidFinishLaunching` after renderer init, before fullscreen toggle). +- **`sapp_show_keyboard(show)` (14086):** **mac no-op** (only iOS/Android implemented). `sapp_keyboard_shown()` returns `_sapp.onscreen_keyboard_shown` (always false on mac). +- **`sapp_skip_present()` (14597):** sets `_sapp.skip_present=true`, but on the **Metal path it is IGNORED** — only the Vulkan (`_sapp_vk_frame`, 5023) and D3D11/WGPU frame functions honor it. `_sapp_macos_frame` (5849) has no skip_present check; the Metal drawable is presented by sokol_gfx's commit. So on mac+Metal, `sapp_skip_present()` has no effect (event-driven present suppression is a D3D11/Vulkan feature). +- **Dock icon / `sapp_set_icon`:** dock-tile only (`_sapp_macos_set_icon`, 5816 writes `NSApp.dockTile.contentView`). **Main-window rendering path does not require it** — safe to omit in the shim. +- **AppKit-notification-driven event_cb sites** (outside the NSView): `windowShouldClose:`, `windowDidResize:`, `windowDidChangeScreen:`, `windowDidMiniaturize:`, `windowDidDeminiaturize:`, `windowDidBecomeKey:`, `windowDidResignKey:` (all in the window delegate), and `performDragOperation:` (in the NSWindow subclass). The shim must route these to `event_cb` just like the view routes input. +- **GL/WGPU divergences** (not needed for Metal shim, noted for completeness): GL uses `NSOpenGLView` + a 1ms `NSTimer`→`setNeedsDisplay`→`drawRect:`→`_sapp_macos_frame`, and `prepareOpenGL` sets swap interval 1; WGPU mirrors the Metal CADisplayLink setup. Both share the same window/delegate/event code. + +--- + +## Appendix: function → line index (Metal-relevant) + +| function | line | +|---|---| +| `_sapp_macos_max_fps` | 5056 | +| `_sapp_macos_mtl_create_texture` | 5061 | +| `_sapp_macos_mtl_swapchain_create/destroy/resize/next` | 5089/5102/5111/5116 | +| `_sapp_macos_mtl_timing_init/update/frame_duration` | 5126/5131/5147 | +| `_sapp_macos_mtl_start/stop_display_link` | 5156/5173 | +| `_sapp_macos_mtl_start/stop_fallback_timer` | 5179/5190 | +| `_sapp_macos_mtl_transition_to_occluded/visible` | 5197/5204 | +| `_sapp_macos_mtl_init/discard_state` | 5211/5229 | +| `_sapp_macos_mtl_update_framebuffer_dimensions` | 5237 | +| `_sapp_macos_init_keytable` | 5359 | +| `_sapp_macos_discard_state` | 5473 | +| `_sapp_macos_init_cursors` | 5502 | +| `_sapp_macos_run` / `main` | 5519 / 5546 | +| `_sapp_macos_mods` | 5553 | +| `_sapp_macos_mouse/key/app_event` | 5581/5590/5600 | +| `_sapp_macos_init_default_dimensions` | 5608 | +| `_sapp_macos_update_dimensions` | 5632 | +| `_sapp_macos_toggle_fullscreen` | 5655 | +| `_sapp_macos_set/get_clipboard_string` | 5664/5672 | +| `_sapp_macos_update_window_title` | 5689 | +| `_sapp_macos_mouse_update_from_nspoint/nsevent` | 5693/5711 | +| `_sapp_macos_show/lock_mouse` | 5715/5724 | +| `_sapp_macos_update_cursor` | 5750 | +| `_sapp_macos_make/destroy_custom_mouse_cursor` | 5774/5810 | +| `_sapp_macos_set_icon` | 5816 | +| `_sapp_macos_frame` | 5849 | +| `app_delegate` (`applicationDidFinishLaunching:` etc.) | 5872 | +| `window_delegate` | 5947 | +| `_sapp_macos_window` (drag&drop) | 6043 | +| `_sapp_macos_view` (input, timers) | 6097 | diff --git a/docs/dev/sapp-web-impl-spec.md b/docs/dev/sapp-web-impl-spec.md new file mode 100644 index 00000000..c5647b33 --- /dev/null +++ b/docs/dev/sapp-web-impl-spec.md @@ -0,0 +1,965 @@ +# sokol_app.h Emscripten (Web) Backend — Behavioral Specification + +Implementation contract for replacing sokol_app.h's Emscripten/Web main-window / +app-lifecycle with `sokol_app_tc.h` (project "P3 Web driver"). All line references +are into `core/include/sokol/sokol_app.h` (14602 lines, this worktree). Focus: +`_SAPP_EMSCRIPTEN` with BOTH graphics backends — `SOKOL_WGPU` (TrussC web default) +and `SOKOL_GLES3` (WebGL2, opt-in). + +This is the fourth sibling of `sapp-mac-impl-spec.md` (event-driven `[NSApp run]`), +`sapp-win32-impl-spec.md` (owned PeekMessage loop) and `sapp-x11-impl-spec.md` +(owned XPending loop). **The web backend is unlike all three: there is NO owned +loop.** The browser owns the loop. `_sapp_emsc_run()` sets everything up, hands a +per-frame callback to `emscripten_request_animation_frame_loop()` (or +`emscripten_set_main_loop()`), and **returns immediately**. All rendering and +event dispatch happen later, re-entrant, from browser-driven callbacks. There is +no blocking swap, no timer, no message pump, no `sleep`. The whole backend is a +collection of C callbacks + `EM_JS` JavaScript shims registered against the +canvas / window / document. + +**Which #ifdef path is live in TrussC.** `core/CMakeLists.txt` compiles the web +target with **`SOKOL_WGPU` by default** and `SOKOL_GLES3` as an option (see §13). +sokol_app resolves the API at 2330–2335: `__EMSCRIPTEN__` → `_SAPP_EMSCRIPTEN`, +then requires `SOKOL_GLES3 || SOKOL_WGPU`. So on TrussC/web the live default path +is **`SOKOL_WGPU`**; **`SOKOL_GLES3`/WebGL2** is the fallback. This document +documents BOTH because the port must keep both working. Vulkan/Metal/D3D11 forks +inside shared code are dead on web and marked inline. + +**Single-window is baked in harder than any native backend.** The web "window" is +one HTML `` element, addressed by a CSS selector string +`_sapp.html5_canvas_selector` (default `"#canvas"`, 3324). There is no window +object, no NSWindow/HWND/Xwindow XID, no window list. **A second window is not +representable** in this backend at all (see §12, §14). The multiwindow port must +treat web as strictly single-window (window #0) and stub/no-op the plural-window +APIs. + +--- + +## 0. Global state touched (the shared contract) + +Single global `static _sapp_t _sapp;` embeds `_sapp_emsc_t emsc;` (3302–3303, +only under `_SAPP_EMSCRIPTEN`) and, under `SOKOL_WGPU`, `_sapp_wgpu_t wgpu;` +(3292–3294, compiled on ALL platforms that select WGPU, not web-specific). + +### `_sapp_emsc_t` (2881–2887) — the ENTIRE web-specific per-app state +```c +typedef struct { + bool mouse_lock_requested; // deferred pointer-lock (§8): request must fire from inside a user-gesture event + uint16_t mouse_buttons; // last-seen browser button bitmask (bit0=L, bit1=R, bit2=M) — see §4 mods +} _sapp_emsc_t; +``` +That is the whole struct. **Everything else the web backend needs lives in +JavaScript-side globals** (`Module.sapp_emsc_target`, `Module.sokol_*`, +`Module.__sapp_custom_cursors`, `specialHTMLTargets`) reachable only from `EM_JS` +blocks, NOT in C. This is the biggest structural surprise vs native: a large part +of the "state" is in JS closures on `Module`, not in `_sapp`. + +### `_sapp_wgpu_t` (2720–2733, `SOKOL_WGPU`) — shared with native WGPU +```c +typedef struct { + WGPUInstance instance; + WGPUAdapter adapter; + WGPUDevice device; + WGPUSurface surface; + WGPUTextureFormat render_format; // picked from surface caps (RGBA8Unorm / BGRA8Unorm) + WGPUTexture msaa_tex; // only if sample_count > 1 + WGPUTextureView msaa_view; + WGPUTexture depth_stencil_tex; // Depth32FloatStencil8 + WGPUTextureView depth_stencil_view; + WGPUTextureView swapchain_view; // per-frame; acquired in sapp_get_swapchain, released in _sapp_wgpu_frame + bool init_done; // GATE: frame callbacks no-op until async device+swapchain ready (§3) +} _sapp_wgpu_t; +``` + +### `_sapp.gl.framebuffer` (`_sapp_gl_t`, `_SAPP_ANY_GL` only) +The default framebuffer object id captured after +`emscripten_webgl_make_context_current` (§2). On WebGL2 this is **0** (the default +framebuffer). `sapp_get_swapchain()` reports it as `res.gl.framebuffer` (§11). + +Cross-platform `_sapp` fields the web path reads/writes: `desc`, `valid`, +`first_frame`, `quit_requested`, `quit_ordered`, `fullscreen`, +`window_width/height`, `framebuffer_width/height`, `dpi_scale`, `sample_count`, +`timing`, `mouse.*` (x/y/dx/dy/locked/shown/pos_valid/current_cursor), +`clipboard.*`, `drop.*`, `event`, `custom_cursor_bound[]`, `window_title`, +`html5_ask_leave_site`, `onscreen_keyboard_shown` (always false on web), +`skip_present` (TrussC patch — but **NOT USED on web WGPU**, see §3/§14), and +`html5_canvas_selector[_SAPP_MAX_TITLE_LENGTH]` (3324, the canvas CSS selector, +filled from `desc.html5.canvas_selector`). + +### `_sapp_init_state(desc)` / `_sapp_desc_defaults` (relevant subset, ~3520–3574) +`_sapp_def(val,def)` = "use def if val==0" (2291). Web-relevant defaults: +`sample_count→1` (3527), `swap_interval→1` (3528, **ignored on web** — the browser +paces via rAF), `html5.canvas_selector→"#canvas"` (3546), `clipboard_size→8192` +(3547), `max_dropped_files→1` (3548), `max_dropped_file_path_length→2048` (3549), +`window_title→"sokol"` (3550). The canvas selector string is copied into +`_sapp.html5_canvas_selector` and the desc pointer re-pointed at that stable +buffer (3573–3574). `_SAPP_FALLBACK_DEFAULT_WINDOW_WIDTH/HEIGHT = 640/480` +(2301–2302), used only in the `canvas_resize` path (§9). + +--- + +## 1. Entry / run flow — there is NO loop + +### The include + API-select block (2330–2335, 2412–2417, 2448–2453) +- **API select (2330–2335):** `__EMSCRIPTEN__` → `#define _SAPP_EMSCRIPTEN (1)`, + then `#error` unless `SOKOL_GLES3 || SOKOL_WGPU`. +- **`_SAPP_ANY_GL` (2381–2383):** defined for `SOKOL_GLCORE || SOKOL_GLES3`. On web + it's set only in the GLES3 build; **NOT set for WGPU**. Gates `_sapp.gl.*` and + the `res.gl.framebuffer` swapchain field. +- **WGPU header (2412–2417):** `#if defined(SOKOL_WGPU)` → `#include `, + and `#if !defined(__EMSCRIPTEN__)` → `#define _SAPP_WGPU_HAS_WAIT (1)`. **On web + `_SAPP_WGPU_HAS_WAIT` is NOT defined** → the whole WGPU init is async-callback + driven (§3). This single macro forks nearly all WGPU control flow between + native (synchronous `wgpuInstanceWaitAny`) and web (chained callbacks). +- **Emscripten includes (2448–2453):** `#if defined(SOKOL_GLES3) #include ` + then unconditionally `#include ` and + `#include `. **Note: no `` for the WGPU build.** + +### `_sapp_emsc_run(const sapp_desc* desc)` (8064–8102) — setup then return +1. `_sapp_init_state(desc)`. +2. **`_sapp.fullscreen = false`** (8066) — hard override of user's `desc.fullscreen` + with comment "can't start in fullscreen on the web!" (browsers only allow + fullscreen from a user gesture). This is web-specific and must be kept. +3. `document_title = desc->html5.update_document_title ? _sapp.window_title : 0` + (8067) → `sapp_js_init(_sapp.html5_canvas_selector, document_title)` (8068, + §1-JSinit) — resolves the canvas element in JS and optionally sets + `document.title`. +4. **Canvas size determination (8069–8083):** + - If `desc.html5.canvas_resize` (8070–8072): the app OWNS the canvas size — + `w/h = _sapp_def(desc.width, 640) / _sapp_def(desc.height, 480)`. No resize + listener registered. + - Else (8073–8076): the canvas size is TRACKED — + `emscripten_get_element_css_size(selector, &w, &h)` reads the CSS size, and + `emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, ..., + _sapp_emsc_size_changed)` registers the window-resize handler (§10). + - `if (desc.high_dpi) dpi_scale = emscripten_get_device_pixel_ratio()` (8077–8079) + — else `dpi_scale` stays 1.0. **`high_dpi=false` genuinely disables DPR + scaling on web** (unlike X11 where it's unconditional). + - `window_width/height = round(w/h)`; `framebuffer_width/height = round(w/h * dpi_scale)` + (8080–8083). `_sapp_roundf_gzero` = round, clamped to ≥ 0. + - `emscripten_set_canvas_element_size(selector, framebuffer_width, framebuffer_height)` + (8084) — sets the canvas **backing-store (drawing-buffer) pixel size** to the + framebuffer size. CSS size stays whatever the page set. +5. **Renderer bring-up (8085–8089):** `#if SOKOL_GLES3 _sapp_emsc_webgl_init()` + (§2) `#elif SOKOL_WGPU _sapp_wgpu_init()` (§3). NOTE: on WGPU this only KICKS OFF + async adapter/device request; the device is not ready yet when run() returns. +6. `_sapp.valid = true` (8090). +7. `_sapp_emsc_register_eventhandlers()` (8091, §4/§5/§8) — registers every DOM + callback. **This is where the TrussC canvas-keyboard patch lives (§6).** +8. `sapp_set_icon(&desc->icon)` (8092) → `_sapp_emsc_set_icon` sets a favicon (§10). +9. **Start the frame loop (8094–8099):** + - If `desc.html5.use_emsc_set_main_loop`: + `emscripten_set_main_loop(_sapp_emsc_frame_main_loop, 0, desc.html5.emsc_set_main_loop_simulate_infinite_loop)`. + - Else (default): `emscripten_request_animation_frame_loop(_sapp_emsc_frame_animation_loop, 0)`. +10. **Returns.** Comment 8100–8101: "NOT A BUG: do not call `_sapp_discard_state()` + here" — cleanup happens later inside the frame callback on quit (§12). + +### Entry point (8104–8110) and the SOKOL_NO_ENTRY dispatch +- Default: `#if !defined(SOKOL_NO_ENTRY)` → `int main(int argc,char**){ desc = sokol_main(...); _sapp_emsc_run(&desc); return 0; }` (8105–8108). +- **TrussC compiles `SOKOL_NO_ENTRY`** so this `main` is compiled out. The live + path is `sapp_run(desc)` (13941, `#if defined(SOKOL_NO_ENTRY)`) → + `_sapp_emsc_run(desc)` (13947–13948). TrussC's `runApp()` calls `sapp_run()`. + On web `sapp_run()` **returns immediately** (docs 1174–1190) — so no user code + may run after it; the app lives entirely in callbacks. + +### The frame callbacks +- **`_sapp_emsc_frame_animation_loop(double time, void* ud)` (8027–8055)** — the + rAF callback. `time` is the rAF DOMHighResTimeStamp in **milliseconds**. + 1. `_sapp_timing_update(&_sapp.timing, time / 1000.0)` (8029) — feed the EXTERNAL + browser clock (converted ms→s) into the EMA timer. **This is the web timing + source; there is no `emscripten_get_now`/`performance.now` inside the tick + itself for the rAF path — the browser hands the timestamp.** + 2. `#if SOKOL_WGPU _sapp_wgpu_frame()` (8031–8032, §3) `#else _sapp_frame()` + (8034). `_sapp_frame` runs `init_cb` on first frame then `frame_cb`. + 3. **Quit handling (8037–8053):** if `quit_requested` → fire + `SAPP_EVENTTYPE_QUIT_REQUESTED`; if still requested → `quit_ordered=true`. + If `quit_ordered` → `_sapp_emsc_unregister_eventhandlers()` (§4), + `#if SOKOL_WGPU _sapp_wgpu_discard()`, `_sapp_call_cleanup()`, + `_sapp_discard_state()`, `return EM_FALSE` (stops the rAF loop). Else `EM_TRUE`. +- **`_sapp_emsc_frame_main_loop(void)` (8057–8062)** — the `set_main_loop` variant. + `time = emscripten_performance_now()` (8058, **THIS is the `performance.now()` + path**), then calls the animation-loop body; if it returns false, + `emscripten_cancel_main_loop()`. So the two loop modes differ only in the clock + source (rAF timestamp vs `performance.now()`) and the stop mechanism. + +### Frame timing plumbing (2579–2580, 2609–2610, 2626–2629, 2881) +- `_sapp_timestamp_t` on emscripten is `{ int _dummy; }` (2579–2580); + `_sapp_timestamp_init` is `(void)ts` (2609–2610); **`_sapp_timestamp_now` + asserts `false` and returns 0.0 (2626–2629)** — the internal monotonic clock is + deliberately UNUSED on web. All timing comes from the `time` argument passed into + `_sapp_timing_update` by the frame callbacks. The EMA filter itself + (`_sapp_timing_*`, dt_min/dt_max/threshold/alpha, seed 1/60) is identical to the + other backends. `sapp_frame_duration()` → smoothed; `_unfiltered()` → raw dt. + +--- + +## 2. GLES3 / WebGL2 renderer bring-up (`SOKOL_GLES3` only) + +### Context creation (`_sapp_emsc_webgl_init`, 7935–7950) +```c +EmscriptenWebGLContextAttributes attrs; +emscripten_webgl_init_context_attributes(&attrs); // fills defaults +attrs.alpha = _sapp.desc.alpha; +attrs.depth = true; +attrs.stencil = true; +attrs.antialias = _sapp.sample_count > 1; // MSAA on iff sample_count>1 +attrs.premultipliedAlpha = _sapp.desc.html5.premultiplied_alpha; +attrs.preserveDrawingBuffer= _sapp.desc.html5.preserve_drawing_buffer; +attrs.enableExtensionsByDefault = true; +attrs.majorVersion = 2; // WebGL2 (== GLES3) +EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_create_context(_sapp.html5_canvas_selector, &attrs); +// FIXME: error message? (no error check on ctx==0) +emscripten_webgl_make_context_current(ctx); +glGetIntegerv(GL_FRAMEBUFFER_BINDING, (GLint*)&_sapp.gl.framebuffer); // capture default FBO (== 0) +``` +- **`attrs.powerPreference` / `attrs.majorVersion` note:** upstream sets NO + `powerPreference` here (defaults to browser default). `majorVersion=2` is the + WebGL2/GLES3 selector; there is no `minorVersion` for WebGL. The context handle + `ctx` is discarded after make-current (**not stored in `_sapp`** — the current + context is thread-implicit like GLX). No error handling on failure (FIXME). +- **Swapchain semantics:** the "swapchain" is just the default framebuffer id + (0). `sapp_get_swapchain()` returns `res.gl.framebuffer = _sapp.gl.framebuffer` + (14478) + width/height/color_format/depth_format/sample_count. **No + render_view / textures** — WebGL resolves MSAA implicitly via the context's + `antialias` attribute, so there's no explicit MSAA texture like WGPU/Metal. +- `sample_count` meaning on WebGL: it only toggles `antialias` on/off; the actual + sample count is browser-chosen. `sapp_sample_count()` still returns the + requested `_sapp.sample_count` (14052) — a mild lie the port should preserve for + API consistency (sokol_gfx swapchain expects the requested count). + +### Context loss (`_sapp_emsc_webgl_context_cb`, 7918–7933, `SOKOL_GLES3` only) +Maps `EMSCRIPTEN_EVENT_WEBGLCONTEXTLOST → SAPP_EVENTTYPE_SUSPENDED` and +`WEBGLCONTEXTRESTORED → SAPP_EVENTTYPE_RESUMED`, fires the sapp event. Registered +in `register_eventhandlers` (7985–7988) / unregistered (8021–8024), both guarded +by `#if defined(SOKOL_GLES3)`. **Note: the lost/restored handlers do NOT actually +recreate the GL context** — they only surface SUSPENDED/RESUMED to the app. Real +context recovery is not implemented (app is expected to rebuild GPU resources on +RESUMED). WGPU has no analogous webgl-context-loss path (it has device-lost, §3). + +--- + +## 3. WGPU renderer bring-up (`SOKOL_WGPU`, TrussC web DEFAULT) + +**Header / API flavor — CRITICAL (2412–2413, 3891–3895):** this vendored sokol +uses the **modern Dawn-style `webgpu.h`** (`WGPUStringView`, `WGPUFuture`, +`wgpuInstanceWaitAny`, `WGPURequestAdapterCallbackInfo`, `WGPUSurfaceConfiguration`, +`wgpuSurfaceGetCurrentTexture`, `wgpuSurfacePresent`), NOT the old Emscripten +`emscripten/html5_webgpu.h` / `wgpuInstanceCreateSurface`-with- +`WGPUSurfaceDescriptorFromCanvasHTMLSelector`. The surface source struct is +`WGPUEmscriptenSurfaceSourceCanvasHTMLSelector` with +`sType = WGPUSType_EmscriptenSurfaceSourceCanvasHTMLSelector` (3892–3894) — these +symbols come from **emdawnwebgpu** (Emscripten's Dawn port). **The port's web WGPU +build MUST link `--use-port=emdawnwebgpu`** (or equivalent), NOT the legacy +`-sUSE_WEBGPU=1` bindings. This is a hard ABI dependency — the whole `_sapp_wgpu_*` +code will not compile against the old bindings. (Verify against §13 CMake flags.) + +### The async device dance (web = `!_SAPP_WGPU_HAS_WAIT`) +On web, adapter→device→swapchain are chained through callbacks because there is no +synchronous wait. Call order and the `init_done` gate: + +1. **`_sapp_wgpu_init()` (4213–4235):** create `WGPUInstance` via + `wgpuCreateInstance(&desc)` (on web, `desc.requiredFeatures` is empty because + `_SAPP_WGPU_HAS_WAIT` is off — the `TimedWaitAny` feature block 4218–4224 is + compiled out). PANIC `WGPU_CREATE_INSTANCE_FAILED` on null. Then + `_sapp_wgpu_create_adapter()`. The `#if _SAPP_WGPU_HAS_WAIT` synchronous + `_sapp_wgpu_create_device_and_swapchain()` (4231–4233) is **compiled out on web** + — comment 4229: "on Emscripten, device and swapchain creation are chained in + the callbacks." +2. **`_sapp_wgpu_create_adapter()` (4199–4210):** `callbackmode = AllowProcessEvents` + (web, 3846) — NOT `WaitAnyOnly`. `wgpuInstanceRequestAdapter(instance, 0, cb_info)`. + On web the returned `WGPUFuture` is ignored (no await); the callback fires later. +3. **`_sapp_wgpu_request_adapter_cb` (4180–4197):** stores `_sapp.wgpu.adapter`; + on web (`#if !_SAPP_WGPU_HAS_WAIT`, 4193–4195) chains + `_sapp_wgpu_create_device_and_swapchain()`. +4. **`_sapp_wgpu_create_device_and_swapchain()` (4110–4177):** queries adapter + features (BC/ETC2/ASTC compression, DualSourceBlending, ShaderF16, + Float32Filterable, Float32Blendable, TextureFormatsTier2 — added iff present), + always requires `Depth32FloatStencil8` (4114–4116). Sets device-lost + + uncaptured-error callbacks. `callbackmode = AllowProcessEvents` on web. + `wgpuAdapterRequestDevice(...)`; web ignores the future. +5. **`_sapp_wgpu_request_device_cb` (4087–4108):** stores `_sapp.wgpu.device`; + `#if !_SAPP_EMSCRIPTEN` sets a logging callback (4101–4105) — **skipped on web** + (emdawnwebgpu has no `wgpuDeviceSetLoggingCallback`). Then + `_sapp_wgpu_create_swapchain(false)` and **`_sapp.wgpu.init_done = true`** + (4107) — this flips the gate that lets frames render. +6. **`_sapp_wgpu_device_lost_cb` (4044–4055):** logs unless reason is + `CallbackCancelled` (fired on instance release). `_sapp_wgpu_device_logging_cb` + (4058–4074) and `_sapp_wgpu_uncaptured_error_cb` (4077–4085) exist; the logging + one is `#if !_SAPP_EMSCRIPTEN` (4057), so not used on web. + +### Swapchain creation (`_sapp_wgpu_create_swapchain`, 3880–3982) +- **Surface (first call only, 3888–3926):** builds a `WGPUSurfaceDescriptor` whose + `nextInChain` is the emscripten canvas-selector struct (3891–3895): + `html_canvas_desc.selector = _sapp_wgpu_stringview(_sapp.html5_canvas_selector)`. + So the WGPU surface is bound to the SAME canvas selector as everything else. + `wgpuInstanceCreateSurface` → PANIC `WGPU_SWAPCHAIN_CREATE_SURFACE_FAILED` on null. + `wgpuSurfaceGetCapabilities(surface, adapter, &surf_caps)` → PANIC on non-Success. + `render_format = _sapp_wgpu_pick_render_format(...)` (3864–3878): picks the first + of `RGBA8Unorm`/`BGRA8Unorm` (non-SRGB only). This drives `sapp_color_format()` + (14019–14028: RGBA8Unorm→RGBA8, BGRA8Unorm→BGRA8). +- **Surface config (3928–3943):** `WGPUSurfaceConfiguration` with device, + `render_format`, `usage = RenderAttachment`, size = framebuffer w/h, + `alphaMode = Opaque` **but on emscripten (3936–3940) if + `desc.html5.premultiplied_alpha` → `alphaMode = Premultiplied`**, + `presentMode = Fifo` (vsync). `wgpuSurfaceConfigure(...)`. +- **Depth-stencil texture (3945–3961):** always created, `Depth32FloatStencil8`, + size = framebuffer w/h, sample_count = `_sapp.sample_count`. PANIC on failure. +- **MSAA texture (3963–3980):** only if `sample_count > 1`; `render_format`, + sample_count. PANIC on failure. + +### Per-frame (`_sapp_wgpu_frame`, 4253–4270) +```c +wgpuInstanceProcessEvents(_sapp.wgpu.instance); // pump async callbacks (adapter/device ready) — ESSENTIAL on web +if (_sapp.wgpu.init_done) { // GATE: no rendering until device+swapchain ready + _sapp_frame(); // init_cb (first frame) + frame_cb + if (_sapp.wgpu.swapchain_view) { // released the view acquired in sapp_get_swapchain + wgpuTextureViewRelease(_sapp.wgpu.swapchain_view); + _sapp.wgpu.swapchain_view = 0; + } + #if !defined(_SAPP_EMSCRIPTEN) // <-- present is a NATIVE-ONLY call + if (!_sapp.skip_present) { wgpuSurfacePresent(_sapp.wgpu.surface); } + else { _sapp.skip_present = false; } + #endif +} +``` +- **`wgpuInstanceProcessEvents` every frame is load-bearing on web** — it's how the + async adapter/device-request callbacks actually get delivered; without it + `init_done` never flips and nothing ever renders. +- **`init_done` gate:** for the first N rAF ticks (until the async device arrives) + `_sapp_wgpu_frame` does nothing but pump events. The app's `init_cb`/`frame_cb` + do not run until the device is ready. **The port must replicate this gate** — + callers of `_sapp_frame` on web-WGPU must wait for the device. +- **No explicit present on web (4261 `#if !_SAPP_EMSCRIPTEN`)** — the browser + auto-presents the configured surface at rAF boundaries. Consequently + **`skip_present` (the TrussC D3D11-flicker patch) is a no-op on web** — the whole + present block is compiled out. TrussC's event-driven-render "skip present" trick + cannot suppress a web frame; don't rely on it there (§14). + +### Swapchain acquire / resize / discard +- **`_sapp_wgpu_swapchain_next()` (4009–4035):** called from `sapp_get_swapchain()` + (14442–14444). `wgpuSurfaceGetCurrentTexture`; on `SuccessOptimal/Suboptimal` + create the view; on `Timeout/Outdated/Lost` release + **discard & recreate the + swapchain** (4024–4025) then... (falls through — note: does not re-acquire in the + same call, so `swapchain_view` may be left 0; there's a FIXME at 14445 that null + view "should skip the frame" but currently asserts). On `Error` → PANIC. +- **`_sapp_wgpu_swapchain_size_changed()` (4037–4042):** discard(true) + + create(true) — keeps the surface, recreates depth/MSAA + reconfigures. Called + from `_sapp_emsc_size_changed` (7548–7551, `#if SOKOL_WGPU`) on every canvas + resize. +- **`_sapp_wgpu_discard_swapchain(bool from_resize)` (3984–4006):** releases + msaa/depth views+textures; releases the surface only when `!from_resize` (4001). +- **`_sapp_wgpu_discard()` (4237–4251):** swapchain + device + adapter + instance + release. Called from the quit path (8047–8048). + +### WGPU environment / swapchain getters mapping (14404–14455) +- `sapp_get_environment().wgpu.device = _sapp.wgpu.device` (14405). +- `sapp_get_swapchain()` (14442–14454): asserts `swapchain_view==0`, calls + `_sapp_wgpu_swapchain_next()`, then (sample_count>1) `render_view = msaa_view`, + `resolve_view = swapchain_view`; else `render_view = swapchain_view`; + `depth_stencil_view = depth_stencil_view`. These map straight to + `sglue_swapchain()` (sokol_glue.h 193–195). **NOTE: the doc-comment public + getters `sapp_wgpu_get_device/render_view/resolve_view/depth_stencil_view` + (declared in the header comment at 361–364) are NOT implemented in this version** + — the live contract is `sapp_get_environment()`/`sapp_get_swapchain()` consumed + by `sglue_environment()`/`sglue_swapchain()`. Don't resurrect the old getters; + keep the environment/swapchain path. + +--- + +## 4. Event registration and dispatch model + +### Registration (`_sapp_emsc_register_eventhandlers`, 7953–7989) +All handlers are registered with `useCapture = true` (the `true` third arg) and +`0` userData. Targets: + +| DOM event | emscripten setter | target | callback | +|---|---|---|---| +| mousedown/up/move/enter/leave | `emscripten_set_mouse*_callback` | **canvas selector** | `_sapp_emsc_mouse_cb` | +| wheel | `emscripten_set_wheel_callback` | **canvas selector** | `_sapp_emsc_wheel_cb` | +| keydown/keyup/keypress | `emscripten_set_key*_callback` | **canvas selector** ⟵ TrussC PATCH (§6) | `_sapp_emsc_key_cb` | +| touchstart/move/end/cancel | `emscripten_set_touch*_callback` | **canvas selector** | `_sapp_emsc_touch_cb` | +| pointerlockchange/error | `emscripten_set_pointerlock*_callback` | `EMSCRIPTEN_EVENT_TARGET_DOCUMENT` | `_sapp_emsc_pointerlock*_cb` | +| focus/blur | `emscripten_set_focus/blur_callback` | `EMSCRIPTEN_EVENT_TARGET_WINDOW` | `_sapp_emsc_focus_cb`/`_blur_cb` | +| fullscreenchange | `emscripten_set_fullscreenchange_callback` | **canvas selector** | `_sapp_emsc_fullscreenchange_cb` | +| beforeunload | `sapp_js_add_beforeunload_listener` (EM_JS) | `window` | JS closure | +| paste (if clipboard) | `sapp_js_add_clipboard_listener` (EM_JS) | `window` | JS → `_sapp_emsc_onpaste` | +| dragenter/leave/over/drop (if drop) | `sapp_js_add_dragndrop_listeners` (EM_JS) | canvas | JS → begin/drop/end | +| webglcontextlost/restored (GLES3 only) | `emscripten_set_webglcontext*_callback` | canvas | `_sapp_emsc_webgl_context_cb` | + +**Upstream comment 7954–7956** (now stale after the patch): "HTML canvas doesn't +receive input focus, this is why key event handlers are added to the window object +(this could be worked around by adding a 'tab index' to the canvas)". The TrussC +patch (§6) does exactly that workaround. Note the resize handler is registered +separately in `_sapp_emsc_run` (8075), not here. + +### Unregistration (`_sapp_emsc_unregister_eventhandlers`, 7991–8025) +Mirror image — all set to callback `0`. **Asymmetry to preserve (8011–8013):** the +resize callback is only unregistered here `if (!_sapp.desc.html5.canvas_resize)` +(because it was only registered when tracking). Keyboard callbacks are also +unregistered from the canvas selector (patched, comment 7998). + +### The `_sapp_events_enabled()` gate + consume/bubble model +Every callback body wraps event delivery in `if (_sapp_events_enabled())` +(event_cb set AND init_called) — no events before the first frame, identical to +native. Callbacks return an `EM_BOOL` **"consume" value**: returning true tells +the browser this handler consumed the event (preventDefault). The `bubble_*` +desc flags invert this: +- `consume_event = !_sapp.desc.html5.bubble_mouse_events` (7561) — default bubble + off ⇒ consume on. Then `consume_event |= _sapp_call_event(&_sapp.event)` (7622): + the app's `sapp_consume_event()` also forces consume. Same shape for wheel + (`bubble_wheel_events`, 7635), touch (`bubble_touch_events`, 7855), keys (§5). +- **`sapp_consume_event()` (14237–14239)** just sets `_sapp.event_consumed`; on web + the return of `_sapp_call_event` reflects that and becomes the DOM + preventDefault. So consuming a web event = preventDefault on the DOM event. + +--- + +## 5. Per-event-type semantics + +### Mouse (`_sapp_emsc_mouse_cb`, 7559–7630) +- Stores `_sapp.emsc.mouse_buttons = emsc_event->buttons` (browser bitmask) every + call. **Position:** if `mouse.locked` → `dx/dy = movementX/movementY` (raw + pointer-lock deltas, 7563–7565); else `x/y = targetX/targetY * dpi_scale`, and + `dx/dy` computed vs previous when `pos_valid` (7566–7576). Note **targetX/Y are + CSS pixels, scaled by dpi_scale to framebuffer pixels** — the app sees + framebuffer-space coords. +- Type map (7581–7604): MOUSEDOWN→MOUSE_DOWN (button event), MOUSEUP→MOUSE_UP + (button event), MOUSEMOVE→MOUSE_MOVE, MOUSEENTER→MOUSE_ENTER (clear dx/dy), + MOUSELEAVE→MOUSE_LEAVE (clear dx/dy). Guarded by + `emsc_event->button in [0, SAPP_MAX_MOUSEBUTTONS)` (7577). +- **Button map (7613–7618):** 0→LEFT, 1→MIDDLE, 2→RIGHT (browser order differs from + sokol's L/M/R — note this is NOT a straight cast for 1/2). Non-button events set + `mouse_button = SAPP_MOUSEBUTTON_INVALID`. +- Modifiers (`_sapp_emsc_mouse_event_mods`, 7477–7485): ctrl/shift/alt/meta → + CTRL/SHIFT/ALT/SUPER, plus held-button mods from `mouse_buttons` via + `_sapp_emsc_mouse_button_mods` (7469–7475: bit0→LMB, **bit1→RMB, bit2→MMB** — + "not a bug" comments; browser button bitmask order R/M swapped vs sokol enum). +- **Pointer-lock activation (7625–7627):** `_sapp_emsc_update_mouse_lock_state()` + is called only on button events (mouse-lock can only be requested from a user + gesture; see §8). + +### Wheel (`_sapp_emsc_wheel_cb`, 7632–7654) +- MOUSE_SCROLL. **Delta scaling by `deltaMode` (7641–7647):** `DOM_DELTA_PIXEL → + -0.01`, `DOM_DELTA_LINE → -1.33`, `DOM_DELTA_PAGE → -10.0` (guessed), + default `-0.1`. `scroll_x/y = scale * deltaX/deltaY`. **Sign is negated** (natural + scroll direction). This exact table (github issue #339) is load-bearing — + lift verbatim. Also updates mouse-lock state (7652). + +### Keyboard (`_sapp_emsc_key_cb`, 7783–7851) + keymap (7656–7775) +- Type map: KEYDOWN→KEY_DOWN, KEYUP→KEY_UP, KEYPRESS→CHAR (7788–7801). +- `key_repeat = emsc_event->repeat` (7805). Modifiers via + `_sapp_emsc_key_event_mods` (7487–7495). +- **CHAR path (7807–7810):** `char_code = emsc_event->charCode`; consume unless + `bubble_char_events`. (Comment: charCode unsupported on Android Chrome.) +- **KEY path (7811–7840):** + - **Keycode table = HTML5 `code` strings** (7812–7815): if `emsc_event->code[0]` + non-empty (desktop browsers send physical `code` like "KeyA", "Digit1", + "ArrowLeft") → `_sapp_emsc_translate_key(code)`. Else (mobile browsers send + only localized `key`) → `_sapp_emsc_translate_key(key)` — works only for a small + localization-agnostic subset (Enter/arrows/etc.), alpha-numerics become + INVALID. **This is a `str→sapp_keycode` linear-search table + `_sapp_emsc_keymap[]` (7656–7763)** keyed on `code` strings ("Backspace", + "Tab", "Enter", "ShiftLeft"…"KeyZ", "Digit0"…"Numpad0"…"F1"…, punctuation). + ~115 entries. **Lift the entire table verbatim** — it's the authoritative + web keycode map. Note the "Quote" → `SAPP_KEYCODE_GRAVE_ACCENT` "FIXME: ???" + quirk (7761) is a known upstream bug; keep as-is for parity. + - **macOS Super-key keyup hack (7824–7834):** on KEY_DOWN, if a non-Super key is + pressed while SUPER modifier is held, set `send_keyup_followup = true` — because + macOS browsers don't deliver keyUp while Cmd is held, so keys would "stick". A + synthetic KEY_UP is fired right after the KEY_DOWN (7843–7846). Load-bearing; + lift. + - **Forward-special-keys / char-key logic (7836–7840):** `_sapp_emsc_is_char_key` + (7777–7781) = `key_code < SAPP_KEYCODE_WORLD_1`. **Only NON-char keys are + consumed** (`consume_event |= !bubble_key_events` at 7838–7840). Char keys are + deliberately left to bubble so the browser can generate the follow-up + `keypress`/CHAR event. This is the "forward special keys" rule: prevent-default + F-keys/Tab/Backspace/arrows/etc. (non-char) to stop browser default actions, + but let character keys through. **This interacts with the TrussC canvas patch + (§6):** with keys on the canvas + tabindex, preventDefault on the canvas stops + the page from scrolling on space/arrows while the canvas is focused. +- `_sapp_emsc_update_mouse_lock_state()` at 7849 (keys are user gestures too). + +### Touch (`_sapp_emsc_touch_cb`, 7853–7894) +- **Real multi-touch, NOT mouse emulation.** TOUCHSTART→TOUCHES_BEGAN, + MOVE→TOUCHES_MOVED, END→TOUCHES_ENDED, CANCEL→TOUCHES_CANCELLED (7858–7873). + `num_touches = min(emsc numTouches, SAPP_MAX_TOUCHPOINTS)` (7878–7881). Per point: + `identifier`, `pos_x/y = targetX/Y * dpi_scale`, `changed = isChanged` + (7882–7889). Consume unless `bubble_touch_events`. No synthetic mouse events are + generated from touch — the app gets native touch events. + +### Focus / blur (7896–7916) +`_sapp_emsc_focus_cb` → FOCUSED, `_sapp_emsc_blur_cb` → UNFOCUSED. Registered on +`EMSCRIPTEN_EVENT_TARGET_WINDOW` (not canvas). Always return true. + +### Fullscreen change (`_sapp_emsc_fullscreenchange_cb`, 7378–7384) +Syncs `_sapp.fullscreen = emsc_event->isFullscreen` — needed to catch the user +pressing Esc to exit fullscreen. No sapp event is fired. + +### Resize (`_sapp_emsc_size_changed`, 7507–7557) — see §10. + +--- + +## 6. THE TRUSSC CANVAS-KEYBOARD PATCH (native feature of the port) + +**TRUSSC_MODIFICATIONS.md patch #2 "Keyboard Events on Canvas (Emscripten)".** The +patched lines are in `_sapp_emsc_register_eventhandlers` (7963–7968) and +`_sapp_emsc_unregister_eventhandlers` (7998–8001): + +```c +// Modified by tettou771 for TrussC: register keyboard events on canvas instead of window +// This allows other page elements (like Monaco editor) to handle keyboard events +// Canvas must have tabindex attribute to receive focus +emscripten_set_keydown_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_key_cb); +emscripten_set_keyup_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_key_cb); +emscripten_set_keypress_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_key_cb); +``` + +**What differs from upstream:** upstream sokol registers keydown/keyup/keypress on +`EMSCRIPTEN_EVENT_TARGET_WINDOW` (so the app grabs ALL page keystrokes globally). +TrussC registers them on `_sapp.html5_canvas_selector` instead, so keyboard events +only reach the sokol app when the **canvas element itself has focus**. This lets +other DOM elements on the same page (the Monaco code editor in TrussSketch, form +inputs, etc.) receive keyboard input independently — critical for the web IDE +coexistence. + +**Requirement this imposes:** an HTML canvas element cannot receive keyboard focus +unless it has a `tabindex` attribute. **The shell/host page MUST set `tabindex` +(e.g. `tabindex="0"` or `-1`) on the canvas**, and something must give the canvas +focus (click-to-focus works by default once tabindex is present; the page may also +call `canvas.focus()`). Without tabindex, keyboard events never fire → the app +appears to ignore the keyboard. The stale upstream comment at 7954–7956 documents +exactly this workaround. + +**Port directive:** this is a NATIVE feature of `sokol_app_tc.h`, not an optional +patch. **P3 must register keyboard handlers on the canvas selector by default** +(matching this behavior), and the TrussC web shell.html must keep `tabindex` on the +canvas (verify against §13). Do NOT revert to window-target keyboard. + +**Other emscripten-relevant patches in TRUSSC_MODIFICATIONS.md:** patch #1 +"Skip Present" adds `skip_present` to the WGPU present path — but that path is +`#if !_SAPP_EMSCRIPTEN` (4261), so **skip_present is a no-op on web** (§3/§14). +Patch #3 (RGB10A2) is Metal/D3D11 only; comment explicitly says "WebGL unchanged" +— web color format is RGBA8/BGRA8 (§11). No other patches touch the emscripten +path. + +--- + +## 7. Clipboard, drag&drop, fetch (EM_JS-heavy) + +### Clipboard +- **Set (`_sapp_emsc_set_clipboard_string` → `sapp_js_write_clipboard`, 7079–7099):** + creates a hidden off-screen `