var video = use('sdl_video'); // SDL Video Actor // This actor runs on the main thread and handles all SDL video operations var surface = use('surface'); var input = use('input') var ren var win 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 config = Object.assign({}, default_window, arg[0] || {}); win = new video.window(config); log.console(win.title) log.console(win.size) log.console(win.visible) log.console(win.minimized) log.console(win.position) win.maximized = true // Resource tracking var resources = { texture: {}, surface: {}, cursor: {} }; // ID counter for resource allocation var next_id = 1; // Helper to allocate new ID function allocate_id() { return next_id++; } // Message handler $_.receiver(function(msg) { if (!msg.kind || !msg.op) { send(msg, {error: "Message must have 'kind' and 'op' fields"}); return; } var response = {}; try { switch (msg.kind) { case 'window': response = handle_window(msg); break; case 'renderer': response = handle_renderer(msg); break; case 'texture': response = handle_texture(msg); break; case 'surface': response = handle_surface(msg); break; case 'cursor': response = handle_cursor(msg); break; case 'mouse': response = handle_mouse(msg); break; case 'keyboard': response = handle_keyboard(msg); break; case 'input': response = input.get_events(); break; default: response = {error: "Unknown kind: " + msg.kind}; } } catch (e) { response = {error: e.toString()}; log.error(e) } send(msg, response); }); // Window operations function handle_window(msg) { switch (msg.op) { case 'destroy': win.destroy(); win = undefined return {success: true}; case 'show': win.visible = true; return {success: true}; case 'hide': win.visible = false; return {success: true}; case 'get': var prop = msg.data ? msg.data.property : null; if (!prop) return {error: "Missing property name"}; // Handle special cases if (prop === 'surface') { var surf = win.surface; if (!surf) return {data: null}; var surf_id = allocate_id(); resources.surface[surf_id] = surf; return {data: surf_id}; } return {data: win[prop]}; case 'set': var prop = msg.data ? msg.data.property : null; var value = msg.data ? msg.data.value : undefined; if (!prop) return {error: "Missing property name"}; // Validate property is settable var readonly = ['id', 'pixelDensity', 'displayScale', 'sizeInPixels', 'flags', 'surface']; if (readonly.indexOf(prop) !== -1) { return {error: "Property '" + prop + "' is read-only"}; } win[prop] = value; return {success: true}; case 'fullscreen': win.fullscreen(); return {success: true}; case 'updateSurface': win.updateSurface(); return {success: true}; case 'updateSurfaceRects': if (!msg.data || !msg.data.rects) return {error: "Missing rects array"}; win.updateSurfaceRects(msg.data.rects); return {success: true}; case 'raise': win.raise(); return {success: true}; case 'restore': win.restore(); return {success: true}; case 'flash': win.flash(msg.data ? msg.data.operation : 'briefly'); return {success: true}; case 'sync': win.sync(); return {success: true}; case 'setIcon': if (!msg.data || !msg.data.surface_id) return {error: "Missing surface_id"}; var surf = resources.surface[msg.data.surface_id]; if (!surf) return {error: "Invalid surface id"}; win.set_icon(surf); return {success: true}; case 'makeRenderer': log.console("MAKE RENDERER") if (ren) return {reason: "Already made a renderer"} ren = win.make_renderer() return {success:true}; default: return {error: "Unknown window operation: " + msg.op}; } } // Renderer operations function handle_renderer(msg) { if (!ren) return{reason:'no renderer!'} switch (msg.op) { case 'destroy': ren = undefined return {success: true}; case 'clear': ren.clear(); return {success: true}; case 'present': ren.present(); return {success: true}; case 'flush': ren.flush(); return {success: true}; case 'get': var prop = msg.data ? msg.data.property : null; if (!prop) return {error: "Missing property name"}; // Handle special getters that might return objects if (prop === 'drawColor') { var color = ren[prop]; if (color && typeof color === 'object') { // Convert color object to array format [r,g,b,a] return {data: [color.r || 0, color.g || 0, color.b || 0, color.a || 255]}; } } return {data: ren[prop]}; case 'set': var prop = msg.prop var value = msg.value if (!prop) return {error: "Missing property name"}; if (!value) return {error: "No value to set"} // Validate property is settable var readonly = ['window', 'name', 'outputSize', 'currentOutputSize', 'logicalPresentationRect', 'safeArea']; if (readonly.indexOf(prop) !== -1) { return {error: "Property '" + prop + "' is read-only"}; } // Special handling for render target if (prop === 'target' && value !== null && value !== undefined) { var tex = resources.texture[value]; if (!tex) return {error: "Invalid texture id"}; value = tex; } ren[prop] = value; return {success: true}; case 'line': if (!msg.data || !msg.data.points) return {error: "Missing points array"}; ren.line(msg.data.points); return {success: true}; case 'point': if (!msg.data || !msg.data.points) return {error: "Missing points"}; ren.point(msg.data.points); return {success: true}; case 'rect': if (!msg.data || !msg.data.rect) return {error: "Missing rect"}; ren.rect(msg.data.rect); return {success: true}; case 'fillRect': if (!msg.data || !msg.data.rect) return {error: "Missing rect"}; ren.fillRect(msg.data.rect); return {success: true}; case 'rects': if (!msg.data || !msg.data.rects) return {error: "Missing rects"}; ren.rects(msg.data.rects); return {success: true}; case 'lineTo': if (!msg.data || !msg.data.a || !msg.data.b) return {error: "Missing points a and b"}; ren.lineTo(msg.data.a, msg.data.b); return {success: true}; case 'texture': if (!msg.data) return {error: "Missing texture data"}; var tex_id = msg.data.texture_id; if (!tex_id || !resources.texture[tex_id]) return {error: "Invalid texture id"}; ren.texture( resources.texture[tex_id], msg.data.src, msg.data.dst, msg.data.angle || 0, msg.data.anchor || {x:0.5, y:0.5} ); return {success: true}; case 'copyTexture': if (!msg.data) return {error: "Missing texture data"}; var tex_id = msg.data.texture_id; if (!tex_id || !resources.texture[tex_id]) return {error: "Invalid texture id"}; var tex = resources.texture[tex_id]; // Use the texture method with normalized coordinates ren.texture( tex, msg.data.src || {x:0, y:0, width:tex.width, height:tex.height}, msg.data.dest || {x:0, y:0, width:tex.width, height:tex.height}, 0, // No rotation {x:0, y:0} // Top-left anchor ); return {success: true}; case 'sprite': if (!msg.data || !msg.data.sprite) return {error: "Missing sprite data"}; ren.sprite(msg.data.sprite); return {success: true}; case 'geometry': if (!msg.data) return {error: "Missing geometry data"}; var tex_id = msg.data.texture_id; var tex = tex_id ? resources.texture[tex_id] : null; ren.geometry(tex, msg.data.geometry); return {success: true}; case 'debugText': if (!msg.data || !msg.data.text) return {error: "Missing text"}; ren.debugText([msg.data.pos.x, msg.data.pos.y], msg.data.text); return {success: true}; case 'clipEnabled': return {data: ren.clipEnabled()}; case 'texture9Grid': if (!msg.data) return {error: "Missing data"}; var tex_id = msg.data.texture_id; if (!tex_id || !resources.texture[tex_id]) return {error: "Invalid texture id"}; ren.texture9Grid( resources.texture[tex_id], msg.data.src, msg.data.leftWidth, msg.data.rightWidth, msg.data.topHeight, msg.data.bottomHeight, msg.data.scale, msg.data.dst ); return {success: true}; case 'textureTiled': if (!msg.data) return {error: "Missing data"}; var tex_id = msg.data.texture_id; if (!tex_id || !resources.texture[tex_id]) return {error: "Invalid texture id"}; ren.textureTiled( resources.texture[tex_id], msg.data.src, msg.data.scale || 1.0, msg.data.dst ); return {success: true}; case 'readPixels': var surf = ren.readPixels(msg.data ? msg.data.rect : null); if (!surf) return {error: "Failed to read pixels"}; var surf_id = allocate_id(); resources.surface[surf_id] = surf; return {id: surf_id}; case 'loadTexture': if (!msg.data) throw new Error("Missing data") var tex; // Direct surface data var surf = new surface(msg.data) if (!surf) throw new Error("Must provide surface_id or surface data") tex = ren.load_texture(surf); if (!tex) throw new Error("Failed to load texture") var tex_id = allocate_id(); resources.texture[tex_id] = tex; return { id: tex_id, }; case 'flush': ren.flush(); return {success: true}; case 'coordsFromWindow': if (!msg.data || !msg.data.pos) return {error: "Missing pos"}; return {data: ren.coordsFromWindow(msg.data.pos)}; case 'coordsToWindow': if (!msg.data || !msg.data.pos) return {error: "Missing pos"}; return {data: ren.coordsToWindow(msg.data.pos)}; case 'batch': if (!msg.data || !Array.isArray(msg.data)) return {error: "Missing or invalid data array"}; var results = []; for (var i = 0; i < msg.data.length; i++) { var result = handle_renderer(msg.data[i]); results.push(result); } return {results: results}; default: return {error: "Unknown renderer operation: " + msg.op}; } } // Texture operations function handle_texture(msg) { // Special case: create needs a renderer if (msg.op === 'create') { if (!msg.data) return {error: "Missing texture data"}; var ren_id = msg.data.renderer_id; if (!ren_id || !resources.renderer[ren_id]) return {error: "Invalid renderer id"}; var tex; var renderer = resources.renderer[ren_id]; // Create from surface if (msg.data.surface_id) { var surf = resources.surface[msg.data.surface_id]; if (!surf) return {error: "Invalid surface id"}; tex = new video.texture(renderer, surf); } // Create from properties else if (msg.data.width && msg.data.height) { tex = new video.texture(renderer, { width: msg.data.width, height: msg.data.height, format: msg.data.format || 'rgba8888', pixels: msg.data.pixels, pitch: msg.data.pitch }); } else { log.console(json.encode(msg.data)) return {error: "Must provide either surface_id or width/height"}; } var tex_id = allocate_id(); resources.texture[tex_id] = tex; return {id: tex_id, data: {size: tex.size}}; } // All other operations require a valid texture ID if (!msg.id || !resources.texture[msg.id]) { return {error: "Invalid texture id: " + msg.id}; } var tex = resources.texture[msg.id]; switch (msg.op) { case 'destroy': delete resources.texture[msg.id]; // Texture is automatically destroyed when all references are gone return {success: true}; case 'get': var prop = msg.data ? msg.data.property : null; if (!prop) return {error: "Missing property name"}; return {data: tex[prop]}; case 'set': var prop = msg.data ? msg.data.property : null; var value = msg.data ? msg.data.value : undefined; if (!prop) return {error: "Missing property name"}; // Validate property is settable var readonly = ['size', 'width', 'height']; if (readonly.indexOf(prop) !== -1) { return {error: "Property '" + prop + "' is read-only"}; } tex[prop] = value; return {success: true}; case 'update': if (!msg.data) return {error: "Missing update data"}; tex.update( msg.data.rect || null, msg.data.pixels, msg.data.pitch || 0 ); return {success: true}; case 'lock': var result = tex.lock(msg.data ? msg.data.rect : null); return {data: result}; case 'unlock': tex.unlock(); return {success: true}; case 'query': return {data: tex.query()}; default: return {error: "Unknown texture operation: " + msg.op}; } } // Surface operations (mainly for cleanup) function handle_surface(msg) { switch (msg.op) { case 'destroy': if (!msg.id || !resources.surface[msg.id]) { return {error: "Invalid surface id: " + msg.id}; } delete resources.surface[msg.id]; return {success: true}; default: return {error: "Unknown surface operation: " + msg.op}; } } // Cursor operations function handle_cursor(msg) { switch (msg.op) { case 'create': var surf = new surface(msg.data) var hotspot = msg.data.hotspot || [0, 0]; var cursor = video.createCursor(surf, hotspot); var cursor_id = allocate_id(); resources.cursor[cursor_id] = cursor; return {id: cursor_id}; case 'set': var cursor = null; if (msg.id && resources.cursor[msg.id]) { cursor = resources.cursor[msg.id]; } video.setCursor(cursor); return {success: true}; case 'destroy': if (!msg.id || !resources.cursor[msg.id]) { return {error: "Invalid cursor id: " + msg.id}; } delete resources.cursor[msg.id]; return {success: true}; default: return {error: "Unknown cursor operation: " + msg.op}; } } // Utility function to create window and renderer prosperon.endowments = prosperon.endowments || {}; // Mouse operations function handle_mouse(msg) { var mouse = video.mouse; switch (msg.op) { case 'show': if (msg.data === undefined) return {error: "Missing show parameter"}; mouse.show(msg.data); return {success: true}; case 'capture': if (msg.data === undefined) return {error: "Missing capture parameter"}; mouse.capture(msg.data); return {success: true}; case 'get_state': return {data: mouse.get_state()}; case 'get_global_state': return {data: mouse.get_global_state()}; case 'get_relative_state': return {data: mouse.get_relative_state()}; case 'warp_global': if (!msg.data) return {error: "Missing position"}; mouse.warp_global(msg.data); return {success: true}; case 'warp_in_window': if (!msg.data || !msg.data.window_id || !msg.data.pos) return {error: "Missing window_id or position"}; var window = resources.window[msg.data.window_id]; if (!window) return {error: "Invalid window id"}; mouse.warp_in_window(window, msg.data.pos); return {success: true}; case 'cursor_visible': return {data: mouse.cursor_visible()}; case 'get_cursor': var cursor = mouse.get_cursor(); if (!cursor) return {data: null}; // Find or create cursor ID for (var id in resources.cursor) { if (resources.cursor[id] === cursor) { return {data: id}; } } // Not tracked, add it var cursor_id = allocate_id(); resources.cursor[cursor_id] = cursor; return {data: cursor_id}; case 'get_default_cursor': var cursor = mouse.get_default_cursor(); if (!cursor) return {data: null}; // Find or create cursor ID for (var id in resources.cursor) { if (resources.cursor[id] === cursor) { return {data: id}; } } // Not tracked, add it var cursor_id = allocate_id(); resources.cursor[cursor_id] = cursor; return {data: cursor_id}; case 'create_system_cursor': if (msg.data === undefined) return {error: "Missing cursor type"}; var cursor = mouse.create_system_cursor(msg.data); var cursor_id = allocate_id(); resources.cursor[cursor_id] = cursor; return {id: cursor_id}; case 'get_focus': var window = mouse.get_focus(); if (!window) return {data: null}; // Find window ID for (var id in resources.window) { if (resources.window[id] === window) { return {data: id}; } } // Not tracked, add it var win_id = allocate_id(); resources.window[win_id] = window; return {data: win_id}; default: return {error: "Unknown mouse operation: " + msg.op}; } } // Keyboard operations function handle_keyboard(msg) { var keyboard = video.keyboard; switch (msg.op) { case 'get_state': return {data: keyboard.get_state()}; case 'get_focus': var window = keyboard.get_focus(); if (!window) return {data: null}; // Find window ID for (var id in resources.window) { if (resources.window[id] === window) { return {data: id}; } } // Not tracked, add it var win_id = allocate_id(); resources.window[win_id] = window; return {data: win_id}; case 'start_text_input': var window = null; if (msg.data && msg.data.window_id) { window = resources.window[msg.data.window_id]; if (!window) return {error: "Invalid window id"}; } keyboard.start_text_input(window); return {success: true}; case 'stop_text_input': var window = null; if (msg.data && msg.data.window_id) { window = resources.window[msg.data.window_id]; if (!window) return {error: "Invalid window id"}; } keyboard.stop_text_input(window); return {success: true}; case 'text_input_active': var window = null; if (msg.data && msg.data.window_id) { window = resources.window[msg.data.window_id]; if (!window) return {error: "Invalid window id"}; } return {data: keyboard.text_input_active(window)}; case 'get_text_input_area': var window = null; if (msg.data && msg.data.window_id) { window = resources.window[msg.data.window_id]; if (!window) return {error: "Invalid window id"}; } return {data: keyboard.get_text_input_area(window)}; case 'set_text_input_area': if (!msg.data || !msg.data.rect) return {error: "Missing rect"}; var window = null; if (msg.data.window_id) { window = resources.window[msg.data.window_id]; if (!window) return {error: "Invalid window id"}; } keyboard.set_text_input_area(msg.data.rect, msg.data.cursor || 0, window); return {success: true}; case 'clear_composition': var window = null; if (msg.data && msg.data.window_id) { window = resources.window[msg.data.window_id]; if (!window) return {error: "Invalid window id"}; } keyboard.clear_composition(window); return {success: true}; case 'screen_keyboard_shown': if (!msg.data || !msg.data.window_id) return {error: "Missing window_id"}; var window = resources.window[msg.data.window_id]; if (!window) return {error: "Invalid window id"}; return {data: keyboard.screen_keyboard_shown(window)}; case 'reset': keyboard.reset(); return {success: true}; default: return {error: "Unknown keyboard operation: " + msg.op}; } }