Application.lua (32214B)
1 -- Application.lua - Application instance management with function exports 2 -- Manages application lifecycle, process information, and exported functions 3 4 local Application = {} 5 Application.__index = Application 6 Application.__metatable = false -- Prevent metatable access/modification 7 8 -- Track used PIDs to avoid collisions 9 if not _G._used_pids then 10 _G._used_pids = {} 11 end 12 13 -- Helper: Generate a random PID that hasn't been used 14 local function generatePid() 15 local max_attempts = 1000 16 for i = 1, max_attempts do 17 -- Generate random PID between 1000 and 65535 18 local pid = math.random(1000, 65535) 19 20 -- Check if this PID is already in use 21 if not _G._used_pids[pid] then 22 _G._used_pids[pid] = true 23 return pid 24 end 25 end 26 27 -- Fallback: if we somehow can't find a free PID after 1000 attempts, 28 -- just use a sequential counter 29 if not _G._fallback_pid then 30 _G._fallback_pid = 1000 31 end 32 33 while _G._used_pids[_G._fallback_pid] do 34 _G._fallback_pid = _G._fallback_pid + 1 35 end 36 37 local pid = _G._fallback_pid 38 _G._used_pids[pid] = true 39 _G._fallback_pid = _G._fallback_pid + 1 40 return pid 41 end 42 43 -- Helper: Free a PID when application terminates 44 local function freePid(pid) 45 if pid and _G._used_pids[pid] then 46 _G._used_pids[pid] = nil 47 end 48 end 49 50 -- Helper: Get current timestamp (simplified for bare metal) 51 local function getTimestamp() 52 -- In a real implementation, this would query a hardware timer 53 -- For now, we'll use a simple counter 54 if not _G._app_timestamp then 55 _G._app_timestamp = 0 56 end 57 _G._app_timestamp = _G._app_timestamp + 1 58 return _G._app_timestamp 59 end 60 61 -- Helper: Parse parameter string like "arg1=number,arg2=string,..." or "string,number,..." 62 local function parseParameters(paramStr) 63 if not paramStr or paramStr == "" then 64 return {} 65 end 66 67 local params = {} 68 local argIndex = 1 69 70 for param in paramStr:gmatch("[^,]+") do 71 -- Trim whitespace 72 param = param:match("^%s*(.-)%s*$") 73 74 -- Parse name=type format 75 local name, paramType = param:match("^([%w_]+)=([%w_]+)$") 76 if name and paramType then 77 table.insert(params, { 78 name = name, 79 type = paramType 80 }) 81 else 82 -- No equals sign - treat whole string as type, generate arg name 83 table.insert(params, { 84 name = "arg" .. argIndex, 85 type = param 86 }) 87 argIndex = argIndex + 1 88 end 89 end 90 91 return params 92 end 93 94 -- Helper: Validate export definition 95 local function validateExport(exportDef) 96 -- Required fields 97 if type(exportDef) ~= "table" then 98 return false, "Export definition must be a table" 99 end 100 101 if type(exportDef.name) ~= "string" or exportDef.name == "" then 102 return false, "Export must have a non-empty 'name' field" 103 end 104 105 if type(exportDef.func) ~= "function" then 106 return false, "Export must have a 'func' field that is a function" 107 end 108 109 -- Optional fields validation 110 if exportDef.args and type(exportDef.args) ~= "string" then 111 return false, "Export 'args' field must be a string" 112 end 113 114 if exportDef.rets and type(exportDef.rets) ~= "string" then 115 return false, "Export 'rets' field must be a string" 116 end 117 118 if exportDef.description and type(exportDef.description) ~= "string" then 119 return false, "Export 'description' field must be a string" 120 end 121 122 return true 123 end 124 125 --- Create a new Application instance 126 -- @param appPath: Path to the application (e.g., "/apps/com.example.myapp") 127 -- @param options: Optional table with configuration 128 -- - pid: Override auto-generated process ID 129 -- - name: Application name (defaults to last component of appPath) 130 -- @return Application instance 131 function Application.new(appPath, options) 132 options = options or {} 133 134 if type(appPath) ~= "string" or appPath == "" then 135 error("Application.new requires a valid appPath string") 136 end 137 138 -- Extract app name from path if not provided 139 local appName = options.name 140 if not appName then 141 appName = appPath:match("([^/]+)$") or "unknown" 142 end 143 144 local self = setmetatable({}, Application) 145 146 -- Process information 147 self.pid = options.pid or generatePid() 148 149 self.appPath = appPath 150 self.appName = appName 151 self.startTime = getTimestamp() 152 153 -- Export registry 154 self.exports = {} 155 156 -- Application state 157 self.status = "initialized" -- initialized, running, paused, stopped 158 159 -- Output capture 160 self.stdout = "" -- Captured standard output 161 self.stderr = "" -- Captured standard error (for future use) 162 163 -- Window management 164 self.windows = {} -- Array of windows created by this app 165 166 if osprint then 167 osprint("Application created: " .. appName .. " (PID: " .. self.pid .. ", Path: " .. appPath .. ")\n") 168 end 169 170 return self 171 end 172 173 --- Export a function from this application 174 -- @param exportDef: Table containing export definition 175 -- - name: Function name (required) 176 -- - func: The function to export (required) 177 -- - args: Input parameter specification (optional, e.g., "arg1=number,arg2=string") 178 -- - rets: Output type specification (optional, e.g., "number" or "result=table") 179 -- - description: Human-readable description (optional) 180 -- @return true on success, or (nil, error_message) on failure 181 function Application:export(exportDef) 182 -- Validate the export definition 183 local valid, err = validateExport(exportDef) 184 if not valid then 185 if osprint then 186 osprint("ERROR: Invalid export definition: " .. err .. "\n") 187 end 188 return nil, err 189 end 190 191 -- Check for duplicate exports 192 if self.exports[exportDef.name] then 193 local err = "Function '" .. exportDef.name .. "' is already exported" 194 if osprint then 195 osprint("WARNING: " .. err .. "\n") 196 end 197 return nil, err 198 end 199 200 -- Parse input and output specifications 201 local inputParams = parseParameters(exportDef.args or "") 202 local outputParams = parseParameters(exportDef.rets or "") 203 204 -- Store the export with metadata 205 self.exports[exportDef.name] = { 206 name = exportDef.name, 207 func = exportDef.func, 208 input = inputParams, 209 output = outputParams, 210 description = exportDef.description or "", 211 exportedAt = getTimestamp() 212 } 213 214 if osprint then 215 osprint("Exported function: " .. exportDef.name .. " from " .. self.appName .. "\n") 216 end 217 218 return true 219 end 220 221 --- Call an exported function 222 -- @param functionName: Name of the exported function 223 -- @param ...: Arguments to pass to the function 224 -- @return Function result, or (nil, error_message) on failure 225 function Application:call(functionName, ...) 226 local exportDef = self.exports[functionName] 227 if not exportDef then 228 return nil, "Function '" .. functionName .. "' is not exported" 229 end 230 231 -- Call the function with provided arguments 232 local success, result = pcall(exportDef.func, ...) 233 if not success then 234 return nil, "Function call failed: " .. tostring(result) 235 end 236 237 return result 238 end 239 240 --- Get information about an exported function 241 -- @param functionName: Name of the exported function 242 -- @return Export metadata table, or nil if not found 243 function Application:getExport(functionName) 244 return self.exports[functionName] 245 end 246 247 --- List all exported functions 248 -- @return Array of export names 249 function Application:listExports() 250 local names = {} 251 for name, _ in pairs(self.exports) do 252 table.insert(names, name) 253 end 254 table.sort(names) 255 return names 256 end 257 258 --- Get application status information 259 -- @return Table with application metadata 260 function Application:getInfo() 261 return { 262 pid = self.pid, 263 name = self.appName, 264 path = self.appPath, 265 status = self.status, 266 startTime = self.startTime, 267 uptime = getTimestamp() - self.startTime, 268 exportCount = self:getExportCount() 269 } 270 end 271 272 --- Get count of exported functions 273 -- @return Number of exported functions 274 function Application:getExportCount() 275 local count = 0 276 for _ in pairs(self.exports) do 277 count = count + 1 278 end 279 return count 280 end 281 282 --- Update application status 283 -- @param newStatus: New status string (running, paused, stopped, etc.) 284 function Application:setStatus(newStatus, reason) 285 self.status = newStatus 286 self.stopReason = reason 287 if osprint then 288 local msg = "Application " .. self.appName .. " status changed to: " .. newStatus 289 if reason then 290 msg = msg .. " (reason: " .. reason .. ")" 291 end 292 osprint(msg .. "\n") 293 end 294 end 295 296 --- Generate a help message for an exported function 297 -- @param functionName: Name of the exported function 298 -- @return Help string, or nil if not found 299 function Application:getHelp(functionName) 300 local exportDef = self.exports[functionName] 301 if not exportDef then 302 return nil 303 end 304 305 local help = {} 306 table.insert(help, "Function: " .. exportDef.name) 307 308 if exportDef.description and exportDef.description ~= "" then 309 table.insert(help, "Description: " .. exportDef.description) 310 end 311 312 if #exportDef.input > 0 then 313 local inputs = {} 314 for _, param in ipairs(exportDef.input) do 315 table.insert(inputs, param.name .. ": " .. param.type) 316 end 317 table.insert(help, "Input: " .. table.concat(inputs, ", ")) 318 else 319 table.insert(help, "Input: (none)") 320 end 321 322 if #exportDef.output > 0 then 323 local outputs = {} 324 for _, param in ipairs(exportDef.output) do 325 table.insert(outputs, param.type) 326 end 327 table.insert(help, "Output: " .. table.concat(outputs, ", ")) 328 else 329 table.insert(help, "Output: (unspecified)") 330 end 331 332 return table.concat(help, "\n") 333 end 334 335 --- Print all exported functions with their signatures 336 function Application:printExports() 337 local exportNames = self:listExports() 338 339 if #exportNames == 0 then 340 if osprint then 341 osprint("No exported functions\n") 342 end 343 return 344 end 345 346 if osprint then 347 osprint("Exported functions from " .. self.appName .. ":\n") 348 osprint(string.rep("=", 50) .. "\n") 349 350 for _, name in ipairs(exportNames) do 351 local help = self:getHelp(name) 352 if help then 353 osprint(help .. "\n") 354 osprint(string.rep("-", 50) .. "\n") 355 end 356 end 357 end 358 end 359 360 --- Write to stdout (append text to stdout buffer) 361 -- @param text: Text to append to stdout 362 function Application:writeStdout(text) 363 if type(text) ~= "string" then 364 text = tostring(text) 365 end 366 self.stdout = self.stdout .. text 367 end 368 369 --- Get stdout content 370 -- @return Current stdout buffer content 371 function Application:getStdout() 372 return self.stdout 373 end 374 375 --- Clear stdout buffer 376 function Application:clearStdout() 377 self.stdout = "" 378 end 379 380 --- Terminate the application and free its PID 381 function Application:terminate() 382 self.status = "terminated" 383 freePid(self.pid) 384 385 if osprint then 386 osprint("Application terminated: " .. self.appName .. " (PID: " .. self.pid .. ")\n") 387 end 388 end 389 390 --- Get process information 391 -- @return Table with pid, appName, appPath, startTime, status 392 function Application:getProcessInfo() 393 return { 394 pid = self.pid, 395 appName = self.appName, 396 appPath = self.appPath, 397 startTime = self.startTime, 398 status = self.status 399 } 400 end 401 402 --- Create a new window for this application 403 -- Supported overloads: 404 -- @param x, y, width, height, resizable: Position and size 405 -- @param title, x, y, width, height, resizable: Title with position and size 406 -- @param title, width, height, resizable: Title with centered position 407 -- @param width, height, resizable: Centered position 408 -- @return Window instance 409 function Application:newWindow(arg1, arg2, arg3, arg4, arg5, arg6) 410 local title = nil 411 local x, y, width, height, resizable 412 413 -- Handle overload: newWindow(title, x, y, width, height, resizable) - title with position 414 if type(arg1) == "string" and type(arg2) == "number" and type(arg3) == "number" and type(arg4) == "number" and type(arg5) == "number" then 415 title = arg1 416 x = arg2 417 y = arg3 418 width = arg4 419 height = arg5 420 resizable = arg6 421 -- Handle overload: newWindow(title, width, height, resizable) - center on screen with title 422 elseif type(arg1) == "string" and type(arg2) == "number" and type(arg3) == "number" then 423 title = arg1 424 width = arg2 425 height = arg3 426 resizable = arg4 427 -- Center on screen (assume 1024x768 for now, should get from gfx API) 428 x = math.floor((1024 - width) / 2) 429 y = math.floor((768 - height) / 2) 430 -- Handle overload: newWindow(x, y, width, height, resizable) - position and size 431 elseif type(arg1) == "number" and type(arg2) == "number" and type(arg3) == "number" and type(arg4) == "number" then 432 x = arg1 433 y = arg2 434 width = arg3 435 height = arg4 436 resizable = arg5 437 -- Handle overload: newWindow(width, height, resizable) - center on screen 438 elseif type(arg1) == "number" and type(arg2) == "number" then 439 width = arg1 440 height = arg2 441 resizable = arg3 442 -- Center on screen (assume 1024x768 for now, should get from gfx API) 443 x = math.floor((1024 - width) / 2) 444 y = math.floor((768 - height) / 2) 445 end 446 447 -- Validate parameters 448 if type(x) ~= "number" or type(y) ~= "number" or type(width) ~= "number" or type(height) ~= "number" then 449 error("newWindow requires (x, y, width, height), (width, height), or (title, width, height)") 450 end 451 452 if width <= 0 or height <= 0 then 453 error("Window width and height must be positive") 454 end 455 456 -- Default resizable to false if not provided 457 if resizable == nil then 458 resizable = false 459 end 460 461 -- Create window instance 462 local window = { 463 x = x, 464 y = y, 465 width = width, 466 height = height, 467 title = title, -- Window title (optional) 468 resizable = resizable, -- Whether window can be resized 469 visible = true, 470 app = self, 471 appInstance = self, -- Reference for icon drawing in title bar 472 drawCallback = nil, -- User-defined draw function 473 inputCallback = nil, -- User-defined input handler 474 buffer = nil, -- Per-window pixel buffer (allocated on first draw) 475 dirty = true, -- Needs redraw 476 createdAt = getTimestamp(), -- Track creation order for Z-ordering 477 -- Minimize/maximize state 478 minimized = false, 479 maximized = false, 480 -- Store pre-maximize position/size for restore 481 restoreX = nil, 482 restoreY = nil, 483 restoreWidth = nil, 484 restoreHeight = nil, 485 -- Text selection support 486 selectable = true, -- Whether text selection is enabled for this window 487 selectableText = {}, -- Array of {text, x, y, w, h, color, scale} 488 selection = nil -- {start={index,pos}, finish={index,pos}, content, type} 489 } 490 491 -- Window cursor API (window.cursor.add, window.cursor.remove, etc.) 492 window.cursor = { 493 -- Add a cursor drawing function for this window (overrides global when window is active) 494 -- @param draw_func function(state, gfx) - Called with cursor state and SafeGfx 495 -- @return number Index for removal 496 add = function(draw_func) 497 if _G._cursor_api and _G._cursor_api.windowAdd then 498 return _G._cursor_api.windowAdd(window, draw_func) 499 end 500 error("Cursor API not available") 501 end, 502 503 -- Remove a cursor drawing function from this window's stack 504 -- @param index number The index returned by add() 505 -- @return boolean True if removed 506 remove = function(index) 507 if _G._cursor_api and _G._cursor_api.windowRemove then 508 return _G._cursor_api.windowRemove(window, index) 509 end 510 return false 511 end, 512 513 -- Pop the top cursor from this window's stack 514 -- @return boolean True if popped 515 pop = function() 516 if _G._cursor_api and _G._cursor_api.windowPop then 517 return _G._cursor_api.windowPop(window) 518 end 519 return false 520 end, 521 522 -- Clear all cursors from this window's stack 523 clear = function() 524 if _G._cursor_api and _G._cursor_api.windowClear then 525 _G._cursor_api.windowClear(window) 526 end 527 end 528 } 529 530 -- Window draw method (will be called by LPM drawScreen) 531 function window:draw(safeGfx) 532 -- Clear selectable text array before each draw 533 self.selectableText = {} 534 if self.onDraw and self.visible then 535 self.onDraw(safeGfx) 536 end 537 end 538 539 -- Window visibility control 540 function window:show() 541 self.visible = true 542 self.dirty = true -- Mark for redraw when shown 543 end 544 545 function window:hide() 546 self.visible = false 547 end 548 549 -- Center window on screen 550 function window:centerScreen() 551 self.x = math.floor((1024 - self.width) / 2) 552 self.y = math.floor((768 - self.height) / 2) 553 self.dirty = true 554 end 555 556 -- Close window (with optional cancel via onClose callback) 557 function window:close() 558 -- Call onClose callback if it exists 559 if self.onClose then 560 local success, shouldCancel = pcall(self.onClose) 561 if success and shouldCancel == true then 562 -- onClose returned true, cancel the close 563 return false 564 end 565 end 566 567 -- Free window buffer to release memory 568 if self.buffer and VESAFreeWindowBuffer then 569 VESAFreeWindowBuffer(self.buffer) 570 self.buffer = nil 571 end 572 573 -- Hide the window 574 self:hide() 575 576 -- Fire WindowClosed hook 577 if _G.sys and _G.sys.hook and _G.sys.hook.run then 578 _G.sys.hook:run("WindowClosed", self) 579 end 580 581 return true 582 end 583 584 -- Minimize window 585 function window:minimize() 586 if self.minimized then return end 587 self.minimized = true 588 self.visible = false 589 self.dirty = true 590 591 -- Fire WindowMinimized hook 592 if _G.sys and _G.sys.hook and _G.sys.hook.run then 593 _G.sys.hook:run("WindowMinimized", self) 594 end 595 end 596 597 -- Restore from minimized state 598 function window:restore() 599 if not self.minimized then return end 600 self.minimized = false 601 self.visible = true 602 self.dirty = true 603 604 -- Bring to front by updating timestamp 605 self.createdAt = (_G.getTimestamp and _G.getTimestamp()) or (os.time() * 1000) 606 607 -- Fire WindowRestored hook 608 if _G.sys and _G.sys.hook and _G.sys.hook.run then 609 _G.sys.hook:run("WindowRestored", self) 610 end 611 end 612 613 -- Maximize/restore window 614 function window:maximize() 615 local TITLE_BAR_HEIGHT = self.TITLE_BAR_HEIGHT or 20 616 local BORDER_WIDTH = self.BORDER_WIDTH or 2 617 local TASKBAR_HEIGHT = 32 -- Account for taskbar at bottom 618 619 -- Get screen dimensions from sys.screens 620 local screenW = 1024 621 local screenH = 768 622 if _G.sys and _G.sys.screens and _G.sys.screens[1] then 623 screenW = _G.sys.screens[1].width or screenW 624 screenH = _G.sys.screens[1].height or screenH 625 end 626 627 if self.maximized then 628 -- Restore to previous size/position 629 if self.restoreX then 630 self.x = self.restoreX 631 self.y = self.restoreY 632 self.width = self.restoreWidth 633 self.height = self.restoreHeight 634 end 635 self.maximized = false 636 else 637 -- Save current position/size 638 self.restoreX = self.x 639 self.restoreY = self.y 640 self.restoreWidth = self.width 641 self.restoreHeight = self.height 642 643 -- Maximize to fill screen (accounting for title bar and taskbar) 644 self.x = BORDER_WIDTH 645 self.y = BORDER_WIDTH + TITLE_BAR_HEIGHT 646 self.width = screenW - (BORDER_WIDTH * 2) 647 self.height = screenH - TASKBAR_HEIGHT - TITLE_BAR_HEIGHT - (BORDER_WIDTH * 2) 648 self.maximized = true 649 end 650 651 -- Free old buffer and clear to force recreation with new size 652 if self.buffer and VESAFreeWindowBuffer then 653 VESAFreeWindowBuffer(self.buffer) 654 end 655 self.buffer = nil 656 self.dirty = true 657 658 if _G.osprint then 659 _G.osprint(string.format("[MAXIMIZE] Window now %dx%d, buffer cleared, maximized=%s\n", 660 self.width, self.height, tostring(self.maximized))) 661 end 662 663 -- Call onResize callback if it exists 664 if self.onResize then 665 local oldWidth = self.restoreWidth or self.width 666 local oldHeight = self.restoreHeight or self.height 667 if self.maximized then 668 -- We just maximized, old size is restore size 669 pcall(self.onResize, self.width, self.height, self.restoreWidth, self.restoreHeight) 670 else 671 -- We just restored, old size was the maximized size (screen size) 672 local screenW = 1024 673 local screenH = 768 674 if _G.sys and _G.sys.screens and _G.sys.screens[1] then 675 screenW = _G.sys.screens[1].width or screenW 676 screenH = _G.sys.screens[1].height or screenH 677 end 678 local maxW = screenW - (BORDER_WIDTH * 2) 679 local maxH = screenH - TASKBAR_HEIGHT - TITLE_BAR_HEIGHT - (BORDER_WIDTH * 2) 680 pcall(self.onResize, self.width, self.height, maxW, maxH) 681 end 682 end 683 end 684 685 -- Resize window 686 function window:resize(newWidth, newHeight) 687 if type(newWidth) ~= "number" or type(newHeight) ~= "number" then 688 error("resize requires numeric width and height") 689 end 690 691 if newWidth <= 0 or newHeight <= 0 then 692 error("Window dimensions must be positive") 693 end 694 695 local oldWidth = self.width 696 local oldHeight = self.height 697 698 self.width = newWidth 699 self.height = newHeight 700 701 -- Free old buffer and clear to force recreation with new size 702 if self.buffer and VESAFreeWindowBuffer then 703 VESAFreeWindowBuffer(self.buffer) 704 end 705 self.buffer = nil 706 self.dirty = true 707 708 -- Call onResize callback if it exists 709 if self.onResize then 710 pcall(self.onResize, newWidth, newHeight, oldWidth, oldHeight) 711 end 712 end 713 714 -- Set window size (alias for resize) 715 function window:setSize(newWidth, newHeight) 716 self:resize(newWidth, newHeight) 717 end 718 719 -- Set window position 720 function window:setPos(newX, newY) 721 if type(newX) ~= "number" or type(newY) ~= "number" then 722 error("setPos requires numeric x and y coordinates") 723 end 724 725 self.x = newX 726 self.y = newY 727 self.dirty = true 728 end 729 730 -- Mark window for redraw 731 function window:markDirty() 732 self.dirty = true 733 end 734 735 -- Render - draw window frame and flush buffer to screen 736 -- Call this after drawing to gfx to update the display 737 function window:render() 738 self.dirty = true 739 end 740 741 -- Internal draw operations buffer for imperative drawing 742 window._draw_ops = {} 743 744 -- Create gfx object for imperative drawing (draw anytime, call render() to display) 745 window.gfx = { 746 _window = window, 747 748 -- Clear the drawing buffer (call before redrawing) 749 clear = function(gfxSelf) 750 gfxSelf._window._draw_ops = {} 751 end, 752 753 fillRect = function(gfxSelf, x, y, w, h, color) 754 if type(gfxSelf) == "number" then 755 color = h 756 h = w 757 w = y 758 y = x 759 x = gfxSelf 760 gfxSelf = window.gfx 761 end 762 local r = bit.band(bit.rshift(color, 16), 0xFF) 763 local g = bit.band(bit.rshift(color, 8), 0xFF) 764 local b = bit.band(color, 0xFF) 765 table.insert(gfxSelf._window._draw_ops, {2, x, y, w, h, r, g, b}) 766 end, 767 768 drawRect = function(gfxSelf, x, y, w, h, color) 769 if type(gfxSelf) == "number" then 770 color = h 771 h = w 772 w = y 773 y = x 774 x = gfxSelf 775 gfxSelf = window.gfx 776 end 777 local r = bit.band(bit.rshift(color, 16), 0xFF) 778 local g = bit.band(bit.rshift(color, 8), 0xFF) 779 local b = bit.band(color, 0xFF) 780 table.insert(gfxSelf._window._draw_ops, {2, x, y, w, 1, r, g, b}) 781 table.insert(gfxSelf._window._draw_ops, {2, x, y + h - 1, w, 1, r, g, b}) 782 table.insert(gfxSelf._window._draw_ops, {2, x, y, 1, h, r, g, b}) 783 table.insert(gfxSelf._window._draw_ops, {2, x + w - 1, y, 1, h, r, g, b}) 784 end, 785 786 drawText = function(gfxSelf, x, y, text, color, scale) 787 if type(gfxSelf) == "string" then 788 scale = color 789 color = text 790 text = y 791 y = x 792 x = gfxSelf 793 gfxSelf = window.gfx 794 end 795 local r = bit.band(bit.rshift(color, 16), 0xFF) 796 local g = bit.band(bit.rshift(color, 8), 0xFF) 797 local b = bit.band(color, 0xFF) 798 scale = scale or 1 799 table.insert(gfxSelf._window._draw_ops, {10, x, y, text, r, g, b, scale}) 800 801 -- Record text for selection support (6 pixels per char at scale 1, 12 pixels height) 802 local charWidth = 8 * scale 803 local charHeight = 12 * scale 804 local textWidth = #text * charWidth 805 table.insert(gfxSelf._window.selectableText, { 806 text = text, 807 x = x, 808 y = y, 809 w = textWidth, 810 h = charHeight, 811 color = color, 812 scale = scale 813 }) 814 end, 815 816 drawUText = function(gfxSelf, x, y, text, color, scale) 817 -- Draw unselectable text (not recorded for selection) 818 if type(gfxSelf) == "number" then 819 scale = color 820 color = text 821 text = y 822 y = x 823 x = gfxSelf 824 gfxSelf = window.gfx 825 end 826 local r = bit.band(bit.rshift(color, 16), 0xFF) 827 local g = bit.band(bit.rshift(color, 8), 0xFF) 828 local b = bit.band(color, 0xFF) 829 scale = scale or 1 830 table.insert(gfxSelf._window._draw_ops, {10, x, y, text, r, g, b, scale}) 831 -- Note: not recording in selectableText 832 end, 833 834 drawImage = function(gfxSelf, image, x, y, w, h) 835 if type(gfxSelf) == "userdata" then 836 h = w 837 w = y 838 y = x 839 x = image 840 image = gfxSelf 841 gfxSelf = window.gfx 842 end 843 table.insert(gfxSelf._window._draw_ops, {11, image, x, y, w, h, 1}) 844 end, 845 846 drawPixel = function(gfxSelf, x, y, color) 847 if type(gfxSelf) == "number" then 848 color = y 849 y = x 850 x = gfxSelf 851 gfxSelf = window.gfx 852 end 853 local r = bit.band(bit.rshift(color, 16), 0xFF) 854 local g = bit.band(bit.rshift(color, 8), 0xFF) 855 local b = bit.band(color, 0xFF) 856 table.insert(gfxSelf._window._draw_ops, {0, x, y, r, g, b}) 857 end, 858 859 -- Draw a raw BGRA buffer (from Image object) directly 860 -- luaImage: Image object with .buffer and :getSize() 861 -- x, y: destination position 862 -- srcX, srcY, srcW, srcH: optional source region (defaults to full image) 863 drawBuffer = function(gfxSelf, luaImage, x, y, srcX, srcY, srcW, srcH) 864 if type(gfxSelf) ~= "table" or not gfxSelf._window then 865 -- Called without self, shift args 866 srcH = srcW 867 srcW = srcY 868 srcY = srcX 869 srcX = y 870 y = x 871 x = luaImage 872 luaImage = gfxSelf 873 gfxSelf = window.gfx 874 end 875 if not luaImage or not luaImage.buffer then return end 876 local bufW, bufH = luaImage:getSize() 877 -- {12, buffer, x, y, bufWidth, bufHeight, srcX, srcY, srcW, srcH} 878 table.insert(gfxSelf._window._draw_ops, {12, luaImage.buffer, x, y, bufW, bufH, srcX or 0, srcY or 0, srcW or 0, srcH or 0}) 879 end, 880 881 getWidth = function(gfxSelf) 882 if type(gfxSelf) == "table" and gfxSelf._window then 883 return gfxSelf._window.width 884 end 885 return window.width 886 end, 887 888 getHeight = function(gfxSelf) 889 if type(gfxSelf) == "table" and gfxSelf._window then 890 return gfxSelf._window.height 891 end 892 return window.height 893 end 894 } 895 896 -- Getters for window dimensions 897 function window:getWidth() 898 return self.width 899 end 900 901 function window:getHeight() 902 return self.height 903 end 904 905 function window:getX() 906 return self.x 907 end 908 909 function window:getY() 910 return self.y 911 end 912 913 -- Add to app's window list 914 table.insert(self.windows, window) 915 916 -- Set as active window in LPM 917 -- LPM removed - no longer needed 918 919 if osprint then 920 osprint("Window created for " .. self.appName .. " at (" .. x .. "," .. y .. ") size " .. width .. "x" .. height .. "\n") 921 end 922 923 -- Call onOpen callback after window is fully initialized 924 if window.onOpen then 925 pcall(window.onOpen) 926 end 927 928 -- Fire WindowCreated hook 929 if _G.sys and _G.sys.hook and _G.sys.hook.run then 930 _G.sys.hook:run("WindowCreated", window) 931 end 932 933 return window 934 end 935 936 --- Enter exclusive fullscreen mode 937 -- Creates a fullscreen window (1024x768) 938 function Application:enterFullscreen() 939 -- LPM is no longer required for fullscreen mode 940 941 -- Create fullscreen window if it doesn't exist 942 if not self.fullscreenWindow then 943 self.fullscreenWindow = { 944 x = 0, 945 y = 0, 946 width = 1024, 947 height = 768, 948 visible = true, 949 app = self, 950 appInstance = self, 951 drawCallback = nil, 952 inputCallback = nil, 953 isFullscreen = true, 954 isBorderless = true 955 } 956 957 -- Window draw method 958 function self.fullscreenWindow:draw(safeGfx) 959 if self.drawCallback and self.visible then 960 self.drawCallback(safeGfx) 961 end 962 end 963 964 -- Set draw callback 965 function self.fullscreenWindow:onDraw(callback) 966 if type(callback) ~= "function" then 967 error("onDraw requires a function") 968 end 969 self.drawCallback = callback 970 end 971 972 -- Set input callback 973 function self.fullscreenWindow:onInput(callback) 974 if type(callback) ~= "function" then 975 error("onInput requires a function") 976 end 977 self.inputCallback = callback 978 end 979 980 -- Add to app's window list 981 table.insert(self.windows, self.fullscreenWindow) 982 983 if osprint then 984 osprint("Fullscreen window created for " .. self.appName .. "\n") 985 end 986 end 987 988 return self.fullscreenWindow 989 end 990 991 --- Exit exclusive fullscreen mode 992 function Application:exitFullscreen() 993 -- LPM removed - exclusive fullscreen disabled 994 end 995 996 return Application