Files
cell/prosperon/sdl_video.ce

768 lines
22 KiB
Plaintext

var video = use('sdl_video');
log.console("STATED VIDE")
// 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);
// 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};
}
}