From aac0c3813b47cf348b56160e0bc66424aabfcff2 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Sun, 25 May 2025 19:06:24 -0500 Subject: [PATCH] window actor now created when use('sdl_video') is called --- scripts/core/_sdl_video.js | 61 +++++++ scripts/core/sdl_video.js | 2 - scripts/modules/sdl_video.js | 5 - source/jsffi.c | 2 - source/jsffi.h | 2 - source/qjs_sdl_gpu.c | 1 + source/qjs_sdl_video.c | 344 ++++++++++++++++++++++++++++------- tests/sdl_video.js | 7 + 8 files changed, 349 insertions(+), 75 deletions(-) create mode 100644 scripts/core/_sdl_video.js delete mode 100644 scripts/core/sdl_video.js delete mode 100644 scripts/modules/sdl_video.js create mode 100644 tests/sdl_video.js diff --git a/scripts/core/_sdl_video.js b/scripts/core/_sdl_video.js new file mode 100644 index 00000000..58cf959b --- /dev/null +++ b/scripts/core/_sdl_video.js @@ -0,0 +1,61 @@ +// SDL Video Actor +// This actor runs on the main thread and handles all SDL video operations + +// Default window configuration - documents all available window options +var default_window = { + // Basic properties + title: "Prosperon Window", + width: 640, + height: 480, + + // Position - can be numbers or "centered" + x: undefined, // SDL_WINDOWPOS_UNDEFINED by default + y: undefined, // SDL_WINDOWPOS_UNDEFINED by default + + // Window behavior flags + resizable: true, + fullscreen: false, + hidden: false, + borderless: false, + alwaysOnTop: false, + minimized: false, + maximized: false, + + // Input grabbing + mouseGrabbed: false, + keyboardGrabbed: false, + + // Display properties + highPixelDensity: false, + transparent: false, + opacity: 1.0, // 0.0 to 1.0 + + // Focus behavior + notFocusable: false, + + // Special window types (mutually exclusive) + utility: false, // Utility window (not in taskbar) + tooltip: false, // Tooltip window (requires parent) + popupMenu: false, // Popup menu window (requires parent) + + // Graphics API flags (let SDL choose if not specified) + opengl: false, // Force OpenGL context + vulkan: false, // Force Vulkan context + metal: false, // Force Metal context (macOS) + + // Advanced properties + parent: undefined, // Parent window for tooltips/popups/modal + modal: false, // Modal to parent window (requires parent) + externalGraphicsContext: false, // Use external graphics context + + // Input handling + textInput: true, // Enable text input on creation +}; + +var winwin + +// Export the video functions for other actors to call +$_.receiver(function(msg) { + console.log("Video actor received message:", msg); + winwin = new prosperon.endowments.window(default_window) +}); diff --git a/scripts/core/sdl_video.js b/scripts/core/sdl_video.js deleted file mode 100644 index 987c3da6..00000000 --- a/scripts/core/sdl_video.js +++ /dev/null @@ -1,2 +0,0 @@ -var video = prosperon.endowments.video - diff --git a/scripts/modules/sdl_video.js b/scripts/modules/sdl_video.js deleted file mode 100644 index b4cc2607..00000000 --- a/scripts/modules/sdl_video.js +++ /dev/null @@ -1,5 +0,0 @@ -var sdl_video = this - - - -return sdl_video \ No newline at end of file diff --git a/source/jsffi.c b/source/jsffi.c index d60c4f3e..517c0e6b 100644 --- a/source/jsffi.c +++ b/source/jsffi.c @@ -52,8 +52,6 @@ #include "qjs_steam.h" #endif -SDL_Window *global_window; - #include void gui_input(SDL_Event *e); diff --git a/source/jsffi.h b/source/jsffi.h index 24d4b8b3..55559bc0 100644 --- a/source/jsffi.h +++ b/source/jsffi.h @@ -9,8 +9,6 @@ #define JS_SetProperty(js, tar, str, val) JS_SetPropertyStr(js, tar, #str, val) #define JS_GetProperty(js, tar, atom) JS_GetPropertyStr(js, tar, #atom) -extern SDL_Window *global_window; - // Core FFI functions void ffi_load(JSContext *js); int js_print_exception(JSContext *js, JSValue v); diff --git a/source/qjs_sdl_gpu.c b/source/qjs_sdl_gpu.c index 6a7383c5..369a3df1 100644 --- a/source/qjs_sdl_gpu.c +++ b/source/qjs_sdl_gpu.c @@ -11,6 +11,7 @@ // Global GPU device and window static SDL_GPUDevice *global_gpu; +static SDL_Window *global_window; // GPU Free functions void SDL_GPUDevice_free(JSRuntime *rt, SDL_GPUDevice *d) diff --git a/source/qjs_sdl_video.c b/source/qjs_sdl_video.c index b87c43a7..4f4298fd 100644 --- a/source/qjs_sdl_video.c +++ b/source/qjs_sdl_video.c @@ -11,10 +11,12 @@ #include #include #include +#include +#include -// External globals -extern SDL_Window *global_window; -extern SDL_ThreadID main_thread; +// External function declarations +extern prosperon_rt *create_actor(int argc, char **argv, void (*hook)(JSContext *)); +extern const char *register_actor(const char *id, prosperon_rt *rt, int main_thread); // SDL Window free function void SDL_Window_free(JSRuntime *rt, SDL_Window *w) @@ -109,57 +111,235 @@ static inline rect renderer_worldrect_to_screen(renderer_ctx *ctx, rect r_world) return out; } -// Window creation function -#define JS_SDL_PROP(JS, VAL, SDLPROP, PROP) \ -{ \ -JSValue v = JS_GetPropertyStr(JS,VAL,#PROP); \ -const char *str = JS_ToCString(JS, v); \ -SDL_SetAppMetadataProperty(SDLPROP, str); \ -JS_FreeCString(JS,str); \ -JS_FreeValue(js,v); \ -} \ - -JSC_CCALL(os_engine_start, - if (!SDL_Init(SDL_INIT_VIDEO)) - return JS_ThrowInternalError(js, "Unable to initialize video subsystem: %s", SDL_GetError()); - - if (SDL_GetCurrentThreadID() != main_thread) - return JS_ThrowInternalError(js, "This can only be called from the root actor."); - - if (global_window) - return JS_ThrowReferenceError(js, "The engine was already started."); - - JSValue p = argv[0]; - JS_SDL_PROP(js, p, SDL_PROP_APP_METADATA_NAME_STRING, name) - JS_SDL_PROP(js, p, SDL_PROP_APP_METADATA_VERSION_STRING, version) - JS_SDL_PROP(js, p, SDL_PROP_APP_METADATA_IDENTIFIER_STRING, identifier) - JS_SDL_PROP(js, p, SDL_PROP_APP_METADATA_CREATOR_STRING, creator) - JS_SDL_PROP(js, p, SDL_PROP_APP_METADATA_COPYRIGHT_STRING, copyright) - JS_SDL_PROP(js, p, SDL_PROP_APP_METADATA_URL_STRING, url) - JS_SDL_PROP(js, p, SDL_PROP_APP_METADATA_TYPE_STRING, type) +// Window constructor function +static JSValue js_window_constructor(JSContext *js, JSValueConst new_target, int argc, JSValueConst *argv) +{ + if (argc < 1 || !JS_IsObject(argv[0])) + return JS_ThrowTypeError(js, "Window constructor requires an object argument"); - const char *title; - JSValue title_val = JS_GetPropertyStr(js, argv[0], "title"); - title = JS_ToCString(js, title_val); + JSValue opts = argv[0]; + + // Get basic properties (defaults are handled in JavaScript) + const char *title = NULL; + JSValue title_val = JS_GetPropertyStr(js, opts, "title"); + if (!JS_IsUndefined(title_val) && !JS_IsNull(title_val)) { + title = JS_ToCString(js, title_val); + } JS_FreeValue(js, title_val); - JSValue width_val = JS_GetPropertyStr(js, argv[0], "width"); - JSValue height_val = JS_GetPropertyStr(js, argv[0], "height"); - int width = js2number(js, width_val); - int height = js2number(js, height_val); + if (!title) { + return JS_ThrowTypeError(js, "Window title is required"); + } + + int width = 640; + JSValue width_val = JS_GetPropertyStr(js, opts, "width"); + if (!JS_IsUndefined(width_val) && !JS_IsNull(width_val)) { + width = js2number(js, width_val); + } JS_FreeValue(js, width_val); + + int height = 480; + JSValue height_val = JS_GetPropertyStr(js, opts, "height"); + if (!JS_IsUndefined(height_val) && !JS_IsNull(height_val)) { + height = js2number(js, height_val); + } JS_FreeValue(js, height_val); - SDL_Window *new = SDL_CreateWindow(title, width, height, SDL_WINDOW_RESIZABLE); - - JS_FreeCString(js,title); - - if (!new) return JS_ThrowReferenceError(js, "Couldn't open window: %s\n", SDL_GetError()); - - SDL_StartTextInput(new); - global_window = new; - return SDL_Window2js(js,new); -) + // Handle window position + int x = SDL_WINDOWPOS_UNDEFINED; + JSValue x_val = JS_GetPropertyStr(js, opts, "x"); + if (!JS_IsUndefined(x_val)) { + if (JS_IsString(x_val)) { + const char *pos = JS_ToCString(js, x_val); + if (strcmp(pos, "centered") == 0) x = SDL_WINDOWPOS_CENTERED; + JS_FreeCString(js, pos); + } else { + x = js2number(js, x_val); + } + } + JS_FreeValue(js, x_val); + + int y = SDL_WINDOWPOS_UNDEFINED; + JSValue y_val = JS_GetPropertyStr(js, opts, "y"); + if (!JS_IsUndefined(y_val)) { + if (JS_IsString(y_val)) { + const char *pos = JS_ToCString(js, y_val); + if (strcmp(pos, "centered") == 0) y = SDL_WINDOWPOS_CENTERED; + JS_FreeCString(js, pos); + } else { + y = js2number(js, y_val); + } + } + JS_FreeValue(js, y_val); + + // Build window flags + Uint64 flags = 0; + + // Check boolean properties + JSValue resizable = JS_GetPropertyStr(js, opts, "resizable"); + if (JS_ToBool(js, resizable)) flags |= SDL_WINDOW_RESIZABLE; + JS_FreeValue(js, resizable); + + JSValue fullscreen = JS_GetPropertyStr(js, opts, "fullscreen"); + if (JS_ToBool(js, fullscreen)) flags |= SDL_WINDOW_FULLSCREEN; + JS_FreeValue(js, fullscreen); + + JSValue hidden = JS_GetPropertyStr(js, opts, "hidden"); + if (JS_ToBool(js, hidden)) flags |= SDL_WINDOW_HIDDEN; + JS_FreeValue(js, hidden); + + JSValue borderless = JS_GetPropertyStr(js, opts, "borderless"); + if (JS_ToBool(js, borderless)) flags |= SDL_WINDOW_BORDERLESS; + JS_FreeValue(js, borderless); + + JSValue always_on_top = JS_GetPropertyStr(js, opts, "alwaysOnTop"); + if (JS_ToBool(js, always_on_top)) flags |= SDL_WINDOW_ALWAYS_ON_TOP; + JS_FreeValue(js, always_on_top); + + JSValue minimized = JS_GetPropertyStr(js, opts, "minimized"); + if (JS_ToBool(js, minimized)) flags |= SDL_WINDOW_MINIMIZED; + JS_FreeValue(js, minimized); + + JSValue maximized = JS_GetPropertyStr(js, opts, "maximized"); + if (JS_ToBool(js, maximized)) flags |= SDL_WINDOW_MAXIMIZED; + JS_FreeValue(js, maximized); + + JSValue mouse_grabbed = JS_GetPropertyStr(js, opts, "mouseGrabbed"); + if (JS_ToBool(js, mouse_grabbed)) flags |= SDL_WINDOW_MOUSE_GRABBED; + JS_FreeValue(js, mouse_grabbed); + + JSValue keyboard_grabbed = JS_GetPropertyStr(js, opts, "keyboardGrabbed"); + if (JS_ToBool(js, keyboard_grabbed)) flags |= SDL_WINDOW_KEYBOARD_GRABBED; + JS_FreeValue(js, keyboard_grabbed); + + JSValue high_pixel_density = JS_GetPropertyStr(js, opts, "highPixelDensity"); + if (JS_ToBool(js, high_pixel_density)) flags |= SDL_WINDOW_HIGH_PIXEL_DENSITY; + JS_FreeValue(js, high_pixel_density); + + JSValue transparent = JS_GetPropertyStr(js, opts, "transparent"); + if (JS_ToBool(js, transparent)) flags |= SDL_WINDOW_TRANSPARENT; + JS_FreeValue(js, transparent); + + JSValue not_focusable = JS_GetPropertyStr(js, opts, "notFocusable"); + if (JS_ToBool(js, not_focusable)) flags |= SDL_WINDOW_NOT_FOCUSABLE; + JS_FreeValue(js, not_focusable); + + // Special window types + JSValue utility = JS_GetPropertyStr(js, opts, "utility"); + if (JS_ToBool(js, utility)) flags |= SDL_WINDOW_UTILITY; + JS_FreeValue(js, utility); + + JSValue tooltip = JS_GetPropertyStr(js, opts, "tooltip"); + if (JS_ToBool(js, tooltip)) flags |= SDL_WINDOW_TOOLTIP; + JS_FreeValue(js, tooltip); + + JSValue popup_menu = JS_GetPropertyStr(js, opts, "popupMenu"); + if (JS_ToBool(js, popup_menu)) flags |= SDL_WINDOW_POPUP_MENU; + JS_FreeValue(js, popup_menu); + + // Graphics API flags + JSValue opengl = JS_GetPropertyStr(js, opts, "opengl"); + if (JS_ToBool(js, opengl)) flags |= SDL_WINDOW_OPENGL; + JS_FreeValue(js, opengl); + + JSValue vulkan = JS_GetPropertyStr(js, opts, "vulkan"); + if (JS_ToBool(js, vulkan)) flags |= SDL_WINDOW_VULKAN; + JS_FreeValue(js, vulkan); + + JSValue metal = JS_GetPropertyStr(js, opts, "metal"); + if (JS_ToBool(js, metal)) flags |= SDL_WINDOW_METAL; + JS_FreeValue(js, metal); + + // Handle parent window for tooltips, popups, etc + SDL_Window *parent = NULL; + JSValue parent_val = JS_GetPropertyStr(js, opts, "parent"); + if (!JS_IsUndefined(parent_val)) { + parent = js2SDL_Window(js, parent_val); + } + JS_FreeValue(js, parent_val); + + // Create window with properties if we need special handling + SDL_Window *window = NULL; + + if (parent || (flags & (SDL_WINDOW_TOOLTIP | SDL_WINDOW_POPUP_MENU))) { + // Use SDL_CreateWindowWithProperties for advanced features + SDL_PropertiesID props = SDL_CreateProperties(); + + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, width); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, height); + SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, title); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, x); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, y); + + // Set boolean properties + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, flags & SDL_WINDOW_RESIZABLE); + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_FULLSCREEN_BOOLEAN, flags & SDL_WINDOW_FULLSCREEN); + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_HIDDEN_BOOLEAN, flags & SDL_WINDOW_HIDDEN); + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_BORDERLESS_BOOLEAN, flags & SDL_WINDOW_BORDERLESS); + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_ALWAYS_ON_TOP_BOOLEAN, flags & SDL_WINDOW_ALWAYS_ON_TOP); + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_MINIMIZED_BOOLEAN, flags & SDL_WINDOW_MINIMIZED); + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_MAXIMIZED_BOOLEAN, flags & SDL_WINDOW_MAXIMIZED); + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_MOUSE_GRABBED_BOOLEAN, flags & SDL_WINDOW_MOUSE_GRABBED); + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_TRANSPARENT_BOOLEAN, flags & SDL_WINDOW_TRANSPARENT); + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_FOCUSABLE_BOOLEAN, !(flags & SDL_WINDOW_NOT_FOCUSABLE)); + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_UTILITY_BOOLEAN, flags & SDL_WINDOW_UTILITY); + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_TOOLTIP_BOOLEAN, flags & SDL_WINDOW_TOOLTIP); + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_MENU_BOOLEAN, flags & SDL_WINDOW_POPUP_MENU); + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_OPENGL_BOOLEAN, flags & SDL_WINDOW_OPENGL); + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_VULKAN_BOOLEAN, flags & SDL_WINDOW_VULKAN); + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_METAL_BOOLEAN, flags & SDL_WINDOW_METAL); + + if (parent) { + SDL_SetPointerProperty(props, SDL_PROP_WINDOW_CREATE_PARENT_POINTER, parent); + } + + // Handle modal property + JSValue modal = JS_GetPropertyStr(js, opts, "modal"); + if (JS_ToBool(js, modal) && parent) { + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_MODAL_BOOLEAN, 1); + } + JS_FreeValue(js, modal); + + // Handle external graphics context + JSValue external_graphics = JS_GetPropertyStr(js, opts, "externalGraphicsContext"); + if (JS_ToBool(js, external_graphics)) { + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_EXTERNAL_GRAPHICS_CONTEXT_BOOLEAN, 1); + } + JS_FreeValue(js, external_graphics); + + window = SDL_CreateWindowWithProperties(props); + SDL_DestroyProperties(props); + } else { + // Use simpler API for basic windows + window = SDL_CreateWindow(title, width, height, flags); + } + + // Always free the title string since we allocated it + if (title) { + JS_FreeCString(js, title); + } + + if (!window) { + return JS_ThrowReferenceError(js, "Failed to create window: %s", SDL_GetError()); + } + + // Set additional properties that can't be set during creation + JSValue opacity_val = JS_GetPropertyStr(js, opts, "opacity"); + if (!JS_IsUndefined(opacity_val)) { + float opacity = js2number(js, opacity_val); + SDL_SetWindowOpacity(window, opacity); + } + JS_FreeValue(js, opacity_val); + + // Handle text input + JSValue text_input = JS_GetPropertyStr(js, opts, "textInput"); + if (JS_ToBool(js, text_input)) { + SDL_StartTextInput(window); + } + JS_FreeValue(js, text_input); + + return SDL_Window2js(js, window); +} // Window functions JSC_SCALL(SDL_Window_make_renderer, @@ -662,7 +842,7 @@ static const JSCFunctionListEntry js_renderer_ctx_funcs[] = { extern SDL_ScaleMode js2SDL_ScaleMode(JSContext *js, JSValue v); static const JSCFunctionListEntry js_sdl_video_funcs[] = { - MIST_FUNC_DEF(os, engine_start, 1), + // No regular functions, window is now a constructor }; JSC_CCALL(texture_mode, @@ -675,28 +855,64 @@ static const JSCFunctionListEntry js_SDL_Texture_funcs[] = { MIST_FUNC_DEF(texture, mode, 1), }; -JSValue js_sdl_video_use(JSContext *js) { - // In reality, this should return the ID of the ioactor. If it's not created yet, - // on any thread, it should be created. Then, the sdl_video_funcs need to be - // attached to it so it has access via prosperon.endowments.video - - JSValue mod = JS_NewObject(js); - JS_SetPropertyFunctionList(js,mod,js_sdl_video_funcs,countof(js_sdl_video_funcs)); +// Hook function to set up endowments for the video actor +static void video_actor_hook(JSContext *js) { + // Get prosperon object + JSValue prosperon = JS_GetPropertyStr(js, JS_GetGlobalObject(js), "prosperon"); + JSValue c_types = JS_GetPropertyStr(js, prosperon, "c_types"); // Initialize classes - JSValue c_types = JS_GetPropertyStr(js, JS_GetGlobalObject(js), "c_types"); - JSValue prosperon = JS_GetPropertyStr(js, JS_GetGlobalObject(js), "prosperon"); - c_types = JS_GetPropertyStr(js, prosperon, "c_types"); - JS_FreeValue(js, prosperon); - - // add the singular ioactor address to the exported module - QJSCLASSPREP_FUNCS(SDL_Window) QJSCLASSPREP_FUNCS(renderer_ctx) QJSCLASSPREP_FUNCS(SDL_Texture) JS_FreeValue(js, c_types); - return mod; + // Create window constructor + JSValue window_ctor = JS_NewCFunction2(js, js_window_constructor, "window", 1, JS_CFUNC_constructor, 0); + + // Set prototype on constructor + JS_SetConstructor(js, window_ctor, SDL_Window_proto); + + // Get or create endowments object + JSValue endowments = JS_GetPropertyStr(js, prosperon, "endowments"); + if (JS_IsUndefined(endowments)) { + endowments = JS_NewObject(js); + JS_SetPropertyStr(js, prosperon, "endowments", JS_DupValue(js, endowments)); + } + + // Set window constructor in endowments + JS_SetPropertyStr(js, endowments, "window", window_ctor); + + JS_FreeValue(js, endowments); + JS_FreeValue(js, prosperon); } +JSValue js_sdl_video_use(JSContext *js) { + if (!SDL_Init(SDL_INIT_VIDEO)) + return JS_ThrowInternalError(js, "Unable to initialize video subsystem: %s", SDL_GetError()); + + // Generate a unique ID for the video actor + char id[64]; + snprintf(id, sizeof(id), "video_%d", SDL_GetTicks()); + + printf("id is %s\n", id); + + // Prepare argv for create_actor + // We need to create the actor on the main thread + const char *argv[] = { + "./prosperon", + "spawn", + "--id", id, + "--program", "scripts/core/_sdl_video.js", + "--main", "1" + }; + int argc = 8; + + // Create the actor with the hook to set up endowments + prosperon_rt *rt = create_actor(argc, (char**)argv, video_actor_hook); + return JS_NewString(js,id); + JSValue ret = JS_NewObject(js); + JS_SetPropertyStr(js, ret, "id", JS_NewString(js,id)); + return ret; +} diff --git a/tests/sdl_video.js b/tests/sdl_video.js new file mode 100644 index 00000000..4062347b --- /dev/null +++ b/tests/sdl_video.js @@ -0,0 +1,7 @@ +var video = use('sdl_video') + +console.log(video) + +send({__ACTORDATA__:{id:video}}, {kind:"HELLO!"}) + +$_.delay($_.stop, 2) \ No newline at end of file