init.lua (80931B)
1 osprint("Test from init.lua\n") 2 3 -- Drawing operation constants (must match vesa.c) 4 local OP_PIXEL = 0 5 local OP_RECT = 1 6 local OP_RECT_FILL = 2 7 local OP_CIRCLE = 3 8 local OP_CIRCLE_FILL = 4 9 local OP_TRIANGLE = 5 10 local OP_TRIANGLE_FILL = 6 11 local OP_POLYGON = 7 12 local OP_POLYGON_FILL = 8 13 local OP_LINE = 9 14 local OP_TEXT = 10 15 local OP_IMAGE = 11 16 local OP_BUFFER = 12 17 18 -- Global cursor state 19 _G.cursor_state = { 20 x = 512, 21 y = 384, 22 visible = true, 23 mode = "cursor" -- "cursor", "left-click", "right-click", "hover-grab", "grab", "loading", "denied" 24 } 25 26 -- Cursor system constants 27 local CURSOR_SIZE = 30 28 local CURSOR_HOTSPOT_X = 5 29 local CURSOR_HOTSPOT_Y = 5 30 local CURSOR_TRANSPARENT_R = 0 31 local CURSOR_TRANSPARENT_G = 255 32 local CURSOR_TRANSPARENT_B = 0 33 34 -- Cursor drawing stack (top of stack is active) 35 _G.cursor_stack = _G.cursor_stack or {} 36 37 -- Cursor buffer (30x30 pixels, each pixel is {r, g, b}) 38 _G.cursor_buffer = _G.cursor_buffer or {} 39 _G.cursor_buffer_dirty = true 40 _G.cursor_last_mode = nil 41 42 -- Initialize cursor buffer with transparent pixels 43 local function initCursorBuffer() 44 _G.cursor_buffer = {} 45 for y = 1, CURSOR_SIZE do 46 _G.cursor_buffer[y] = {} 47 for x = 1, CURSOR_SIZE do 48 _G.cursor_buffer[y][x] = {CURSOR_TRANSPARENT_R, CURSOR_TRANSPARENT_G, CURSOR_TRANSPARENT_B} 49 end 50 end 51 end 52 initCursorBuffer() 53 54 -- Text selection helper: find text at position and return index + character position 55 local function findTextAtPosition(win, mx, my) 56 if not win.selectableText then 57 return nil 58 end 59 -- Mouse coordinates are already content-relative (window.x/y point to content area) 60 local content_mx = mx 61 local content_my = my 62 63 for i, textInfo in ipairs(win.selectableText) do 64 if content_mx >= textInfo.x and content_mx <= textInfo.x + textInfo.w and 65 content_my >= textInfo.y and content_my < textInfo.y + textInfo.h then 66 -- Calculate character position within the text 67 local charWidth = 8 * (textInfo.scale or 1) 68 local charPos = math.floor((content_mx - textInfo.x) / charWidth) + 1 69 if charPos < 1 then charPos = 1 end 70 if charPos > #textInfo.text then charPos = #textInfo.text end 71 return i, charPos 72 end 73 end 74 return nil 75 end 76 77 -- Text selection helper: get selected text content between start and finish 78 local function getSelectedText(win) 79 if not win.selection or not win.selection.start or not win.selection.finish then 80 return "" 81 end 82 local startIdx = win.selection.start.index 83 local startPos = win.selection.start.pos 84 local finishIdx = win.selection.finish.index 85 local finishPos = win.selection.finish.pos 86 87 -- Normalize so start is before finish 88 if startIdx > finishIdx or (startIdx == finishIdx and startPos > finishPos) then 89 startIdx, finishIdx = finishIdx, startIdx 90 startPos, finishPos = finishPos, startPos 91 end 92 93 local result = {} 94 for i = startIdx, finishIdx do 95 local textInfo = win.selectableText[i] 96 if textInfo then 97 local text = textInfo.text 98 local s = (i == startIdx) and startPos or 1 99 local e = (i == finishIdx) and finishPos or #text 100 table.insert(result, text:sub(s, e)) 101 end 102 end 103 return table.concat(result, "\n") 104 end 105 106 -- Calculate edited text boxes after selection replacement 107 -- textBoxes: array of {text, x, y, w, h, scale, ...} 108 -- point1, point2: selection endpoints {x, y} in content coordinates 109 -- newContent: string to replace selection with 110 -- Returns: new array of text boxes with selection replaced 111 local function calcSelectionEditted(textBoxes, point1, point2, newContent) 112 if not textBoxes or #textBoxes == 0 then 113 return textBoxes 114 end 115 116 -- Find which text boxes contain the selection points 117 local startIdx, startPos, finishIdx, finishPos 118 119 for i, box in ipairs(textBoxes) do 120 local charWidth = 8 * (box.scale or 1) 121 local charHeight = 12 * (box.scale or 1) 122 123 -- Check if point1 is in this box 124 if not startIdx then 125 if point1.y >= box.y and point1.y < box.y + charHeight and 126 point1.x >= box.x and point1.x <= box.x + box.w then 127 startIdx = i 128 startPos = math.floor((point1.x - box.x) / charWidth) + 1 129 if startPos < 1 then startPos = 1 end 130 if startPos > #box.text then startPos = #box.text end 131 end 132 end 133 134 -- Check if point2 is in this box 135 if not finishIdx then 136 if point2.y >= box.y and point2.y < box.y + charHeight and 137 point2.x >= box.x and point2.x <= box.x + box.w then 138 finishIdx = i 139 finishPos = math.floor((point2.x - box.x) / charWidth) + 1 140 if finishPos < 1 then finishPos = 1 end 141 if finishPos > #box.text then finishPos = #box.text end 142 end 143 end 144 end 145 146 if not startIdx or not finishIdx then 147 return textBoxes 148 end 149 150 -- Normalize so start is before finish 151 if startIdx > finishIdx or (startIdx == finishIdx and startPos > finishPos) then 152 startIdx, finishIdx = finishIdx, startIdx 153 startPos, finishPos = finishPos, startPos 154 end 155 156 -- Build new text boxes array 157 local result = {} 158 159 -- Copy boxes before selection 160 for i = 1, startIdx - 1 do 161 result[#result + 1] = textBoxes[i] 162 end 163 164 -- Handle the selection replacement 165 local firstBox = textBoxes[startIdx] 166 local lastBox = textBoxes[finishIdx] 167 168 if firstBox then 169 -- Text before selection in first box + newContent + text after selection in last box 170 local beforeText = firstBox.text:sub(1, startPos - 1) 171 local afterText = lastBox and lastBox.text:sub(finishPos + 1) or "" 172 173 -- Create merged box with replaced content 174 local newBox = {} 175 for k, v in pairs(firstBox) do 176 newBox[k] = v 177 end 178 newBox.text = beforeText .. (newContent or "") .. afterText 179 -- Recalculate width based on new text length 180 local scale = newBox.scale or 1 181 local charWidth = 8 * scale 182 newBox.w = #newBox.text * charWidth 183 184 result[#result + 1] = newBox 185 end 186 187 -- Copy boxes after selection 188 for i = finishIdx + 1, #textBoxes do 189 result[#result + 1] = textBoxes[i] 190 end 191 192 return result 193 end 194 195 -- Make calcSelectionEditted available globally 196 _G.calcSelectionEditted = calcSelectionEditted 197 198 -- SafeGfx for cursor drawing (draws to cursor_buffer) 199 local function createCursorGfx() 200 local gfx = {} 201 202 function gfx:clear() 203 initCursorBuffer() 204 end 205 206 function gfx:fillRect(x, y, w, h, color) 207 local r = bit.band(bit.rshift(color, 16), 0xFF) 208 local g = bit.band(bit.rshift(color, 8), 0xFF) 209 local b = bit.band(color, 0xFF) 210 for py = y, y + h - 1 do 211 for px = x, x + w - 1 do 212 if py >= 0 and py < CURSOR_SIZE and px >= 0 and px < CURSOR_SIZE then 213 _G.cursor_buffer[py + 1][px + 1] = {r, g, b} 214 end 215 end 216 end 217 end 218 219 function gfx:drawRect(x, y, w, h, color) 220 local r = bit.band(bit.rshift(color, 16), 0xFF) 221 local g = bit.band(bit.rshift(color, 8), 0xFF) 222 local b = bit.band(color, 0xFF) 223 -- Top and bottom 224 for px = x, x + w - 1 do 225 if px >= 0 and px < CURSOR_SIZE then 226 if y >= 0 and y < CURSOR_SIZE then 227 _G.cursor_buffer[y + 1][px + 1] = {r, g, b} 228 end 229 if y + h - 1 >= 0 and y + h - 1 < CURSOR_SIZE then 230 _G.cursor_buffer[y + h][px + 1] = {r, g, b} 231 end 232 end 233 end 234 -- Left and right 235 for py = y, y + h - 1 do 236 if py >= 0 and py < CURSOR_SIZE then 237 if x >= 0 and x < CURSOR_SIZE then 238 _G.cursor_buffer[py + 1][x + 1] = {r, g, b} 239 end 240 if x + w - 1 >= 0 and x + w - 1 < CURSOR_SIZE then 241 _G.cursor_buffer[py + 1][x + w] = {r, g, b} 242 end 243 end 244 end 245 end 246 247 function gfx:drawPixel(x, y, color) 248 if x >= 0 and x < CURSOR_SIZE and y >= 0 and y < CURSOR_SIZE then 249 local r = bit.band(bit.rshift(color, 16), 0xFF) 250 local g = bit.band(bit.rshift(color, 8), 0xFF) 251 local b = bit.band(color, 0xFF) 252 _G.cursor_buffer[y + 1][x + 1] = {r, g, b} 253 end 254 end 255 256 function gfx:drawLine(x1, y1, x2, y2, color) 257 local r = bit.band(bit.rshift(color, 16), 0xFF) 258 local g = bit.band(bit.rshift(color, 8), 0xFF) 259 local b = bit.band(color, 0xFF) 260 -- Bresenham's line algorithm 261 local dx = math.abs(x2 - x1) 262 local dy = math.abs(y2 - y1) 263 local sx = x1 < x2 and 1 or -1 264 local sy = y1 < y2 and 1 or -1 265 local err = dx - dy 266 while true do 267 if x1 >= 0 and x1 < CURSOR_SIZE and y1 >= 0 and y1 < CURSOR_SIZE then 268 _G.cursor_buffer[y1 + 1][x1 + 1] = {r, g, b} 269 end 270 if x1 == x2 and y1 == y2 then break end 271 local e2 = 2 * err 272 if e2 > -dy then err = err - dy; x1 = x1 + sx end 273 if e2 < dx then err = err + dx; y1 = y1 + sy end 274 end 275 end 276 277 function gfx:getWidth() return CURSOR_SIZE end 278 function gfx:getHeight() return CURSOR_SIZE end 279 280 return gfx 281 end 282 283 -- Default cursor drawing function 284 local function defaultCursorDraw(state, gfx) 285 gfx:clear() 286 287 -- Default arrow cursor bitmap (1 = black outline, 2 = white fill) 288 local arrow_cursor = { 289 {1,0,0,0,0,0,0,0,0,0,0,0}, 290 {1,1,0,0,0,0,0,0,0,0,0,0}, 291 {1,2,1,0,0,0,0,0,0,0,0,0}, 292 {1,2,2,1,0,0,0,0,0,0,0,0}, 293 {1,2,2,2,1,0,0,0,0,0,0,0}, 294 {1,2,2,2,2,1,0,0,0,0,0,0}, 295 {1,2,2,2,2,2,1,0,0,0,0,0}, 296 {1,2,2,2,2,2,2,1,0,0,0,0}, 297 {1,2,2,2,2,2,2,2,1,0,0,0}, 298 {1,2,2,2,2,2,2,2,2,1,0,0}, 299 {1,2,2,2,2,2,1,1,1,1,0,0}, 300 {1,2,2,1,2,2,1,0,0,0,0,0}, 301 {1,2,1,0,1,2,2,1,0,0,0,0}, 302 {1,1,0,0,1,2,2,1,0,0,0,0}, 303 {1,0,0,0,0,1,2,2,1,0,0,0}, 304 {0,0,0,0,0,1,2,2,1,0,0,0}, 305 {0,0,0,0,0,0,1,2,1,0,0,0}, 306 {0,0,0,0,0,0,1,1,0,0,0,0}, 307 } 308 309 -- Open hand cursor with pointing finger and thumb (for hover-grab) 310 -- Hotspot at fingertip 311 local open_hand_cursor = { 312 {0,0,0,0,1,1,0,0,0,0,0,0,0,0}, 313 {0,0,0,1,2,2,1,0,0,0,0,0,0,0}, 314 {0,0,0,1,2,2,1,0,0,0,0,0,0,0}, 315 {0,0,0,1,2,2,1,0,0,0,0,0,0,0}, 316 {0,0,0,1,2,2,1,0,0,0,0,0,0,0}, 317 {0,0,0,1,2,2,1,0,0,0,0,0,0,0}, 318 {0,0,0,1,2,2,1,1,1,0,1,1,0,0}, 319 {0,0,0,1,2,2,1,2,2,1,2,2,1,0}, 320 {0,0,0,1,2,2,2,2,2,1,2,2,1,0}, 321 {0,0,1,1,2,2,2,2,2,2,2,2,1,0}, 322 {0,1,2,2,1,2,2,2,2,2,2,2,1,0}, 323 {1,2,2,2,1,2,2,2,2,2,2,2,1,0}, 324 {1,2,2,2,2,2,2,2,2,2,2,1,0,0}, 325 {0,1,2,2,2,2,2,2,2,2,2,1,0,0}, 326 {0,0,1,2,2,2,2,2,2,2,1,0,0,0}, 327 {0,0,0,1,2,2,2,2,2,2,1,0,0,0}, 328 {0,0,0,0,1,2,2,2,2,1,0,0,0,0}, 329 {0,0,0,0,0,1,1,1,1,0,0,0,0,0}, 330 } 331 332 -- Closed fist cursor (for grab/dragging) 333 -- Hotspot at left knuckle 334 local fist_cursor = { 335 {0,0,1,1,1,1,1,1,1,1,0,0}, 336 {0,1,2,2,1,2,2,1,2,2,1,0}, 337 {0,1,2,2,1,2,2,1,2,2,1,0}, 338 {0,1,2,2,1,2,2,1,2,2,1,0}, 339 {1,1,2,2,2,2,2,2,2,2,1,0}, 340 {1,2,2,2,2,2,2,2,2,2,1,0}, 341 {1,2,2,2,2,2,2,2,2,2,1,0}, 342 {0,1,2,2,2,2,2,2,2,2,1,0}, 343 {0,1,2,2,2,2,2,2,2,1,0,0}, 344 {0,0,1,2,2,2,2,2,2,1,0,0}, 345 {0,0,1,2,2,2,2,2,1,0,0,0}, 346 {0,0,0,1,2,2,2,2,1,0,0,0}, 347 {0,0,0,0,1,1,1,1,0,0,0,0}, 348 } 349 350 local cursor_bitmap 351 local ox, oy 352 353 if state == "grab" then 354 cursor_bitmap = fist_cursor 355 -- Hotspot at left knuckle (top-left of fist) 356 ox = 4 -- Offset so left knuckle is at hotspot 357 oy = 5 358 elseif state == "hover-grab" then 359 cursor_bitmap = open_hand_cursor 360 -- Hotspot at fingertip (top of finger) 361 ox = 2 362 oy = 5 363 else 364 cursor_bitmap = arrow_cursor 365 ox = CURSOR_HOTSPOT_X 366 oy = CURSOR_HOTSPOT_Y 367 end 368 369 -- Determine fill color based on state 370 local fill_color = 0xFFFFFF -- White default 371 if state == "left-click" or state == "right-click" then 372 fill_color = 0xC0C0C0 -- Light gray when clicking 373 elseif state == "loading" then 374 fill_color = 0x00FFFF -- Cyan when loading 375 elseif state == "denied" then 376 fill_color = 0xFF0000 -- Red when denied 377 end 378 379 local fill_r = bit.band(bit.rshift(fill_color, 16), 0xFF) 380 local fill_g = bit.band(bit.rshift(fill_color, 8), 0xFF) 381 local fill_b = bit.band(fill_color, 0xFF) 382 383 -- Draw cursor at hotspot offset 384 for row = 1, #cursor_bitmap do 385 for col = 1, #cursor_bitmap[row] do 386 local pixel = cursor_bitmap[row][col] 387 local px, py = ox + col - 1, oy + row - 1 388 if px >= 0 and px < CURSOR_SIZE and py >= 0 and py < CURSOR_SIZE then 389 if pixel == 1 then 390 _G.cursor_buffer[py + 1][px + 1] = {0, 0, 0} -- Black outline 391 elseif pixel == 2 then 392 _G.cursor_buffer[py + 1][px + 1] = {fill_r, fill_g, fill_b} -- Fill color 393 end 394 end 395 end 396 end 397 end 398 399 -- Regenerate cursor buffer using top of stack (or default) 400 local function regenerateCursorBuffer(state) 401 initCursorBuffer() 402 local gfx = createCursorGfx() 403 404 if #_G.cursor_stack > 0 then 405 -- Use top of stack cursor function 406 local cursor_func = _G.cursor_stack[#_G.cursor_stack] 407 local success, err = pcall(cursor_func, state, gfx) 408 if not success and osprint then 409 osprint("Cursor draw error: " .. tostring(err) .. "\n") 410 -- Fall back to default 411 defaultCursorDraw(state, gfx) 412 end 413 else 414 -- Use default cursor 415 defaultCursorDraw(state, gfx) 416 end 417 418 _G.cursor_buffer_dirty = false 419 _G.cursor_last_mode = state 420 end 421 422 -- Add a cursor drawing function to the stack 423 local function addCursor(draw_func) 424 if type(draw_func) ~= "function" then 425 error("addCursor requires a function") 426 end 427 table.insert(_G.cursor_stack, draw_func) 428 _G.cursor_buffer_dirty = true 429 return #_G.cursor_stack -- Return index for removal 430 end 431 432 -- Remove a cursor drawing function from the stack 433 local function removeCursor(index) 434 if index and index >= 1 and index <= #_G.cursor_stack then 435 table.remove(_G.cursor_stack, index) 436 _G.cursor_buffer_dirty = true 437 return true 438 end 439 return false 440 end 441 442 -- Pop the top cursor from the stack 443 local function popCursor() 444 if #_G.cursor_stack > 0 then 445 table.remove(_G.cursor_stack) 446 _G.cursor_buffer_dirty = true 447 return true 448 end 449 return false 450 end 451 452 -- Set cursor mode 453 local function setCursorMode(mode) 454 local valid_modes = {cursor=true, ["left-click"]=true, ["right-click"]=true, ["hover-grab"]=true, grab=true, loading=true, denied=true} 455 if valid_modes[mode] then 456 if _G.cursor_state.mode ~= mode then 457 _G.cursor_state.mode = mode 458 _G.cursor_buffer_dirty = true 459 end 460 end 461 end 462 463 -- Window cursor overrides (indexed by window reference) 464 _G.window_cursor_stack = _G.window_cursor_stack or {} 465 466 -- Get the active cursor function (window override or global) 467 local function getActiveCursorFunc() 468 -- Check if there's an active window with cursor override 469 if _G.sys and _G.sys.activeWindow then 470 local win_stack = _G.window_cursor_stack[_G.sys.activeWindow] 471 if win_stack and #win_stack > 0 then 472 return win_stack[#win_stack] 473 end 474 end 475 -- Fall back to global cursor stack 476 if #_G.cursor_stack > 0 then 477 return _G.cursor_stack[#_G.cursor_stack] 478 end 479 return nil 480 end 481 482 -- Regenerate cursor buffer using active cursor (window or global) 483 local function regenerateCursorBufferActive(state) 484 initCursorBuffer() 485 local gfx = createCursorGfx() 486 487 local cursor_func = getActiveCursorFunc() 488 if cursor_func then 489 local success, err = pcall(cursor_func, state, gfx) 490 if not success and osprint then 491 osprint("Cursor draw error: " .. tostring(err) .. "\n") 492 defaultCursorDraw(state, gfx) 493 end 494 else 495 defaultCursorDraw(state, gfx) 496 end 497 498 -- Cache the cursor image to C side for fast drawing 499 if VESACacheCursor and _G.cursor_buffer then 500 VESACacheCursor(_G.cursor_buffer, CURSOR_SIZE, CURSOR_SIZE) 501 end 502 503 _G.cursor_buffer_dirty = false 504 _G.cursor_last_mode = state 505 end 506 507 -- Export cursor functions to sys table (will be done after sys is created) 508 _G._cursor_api = { 509 add = addCursor, 510 remove = removeCursor, 511 pop = popCursor, 512 setMode = setCursorMode, 513 regenerate = regenerateCursorBufferActive, 514 -- Window cursor functions 515 windowAdd = function(window, draw_func) 516 if type(draw_func) ~= "function" then 517 error("cursor.add requires a function") 518 end 519 if not _G.window_cursor_stack[window] then 520 _G.window_cursor_stack[window] = {} 521 end 522 table.insert(_G.window_cursor_stack[window], draw_func) 523 _G.cursor_buffer_dirty = true 524 return #_G.window_cursor_stack[window] 525 end, 526 windowRemove = function(window, index) 527 local stack = _G.window_cursor_stack[window] 528 if stack and index and index >= 1 and index <= #stack then 529 table.remove(stack, index) 530 _G.cursor_buffer_dirty = true 531 return true 532 end 533 return false 534 end, 535 windowPop = function(window) 536 local stack = _G.window_cursor_stack[window] 537 if stack and #stack > 0 then 538 table.remove(stack) 539 _G.cursor_buffer_dirty = true 540 return true 541 end 542 return false 543 end, 544 windowClear = function(window) 545 _G.window_cursor_stack[window] = nil 546 _G.cursor_buffer_dirty = true 547 end 548 } 549 550 -- Load Hook Library 551 osprint("\n=== Loading Hook Library ===\n") 552 if CRamdiskOpen then 553 local hook_handle = CRamdiskOpen("/os/libs/Hook.lua", "r") 554 if hook_handle then 555 local hook_code = CRamdiskRead(hook_handle) 556 CRamdiskClose(hook_handle) 557 558 if hook_code then 559 local hook_func, hook_err = load(hook_code, "/os/libs/Hook.lua", "t") 560 if hook_func then 561 _G.Hook = hook_func() 562 if _G.Hook then 563 osprint("Hook Library loaded successfully!\n") 564 else 565 osprint("ERROR: Hook.lua did not return module\n") 566 end 567 else 568 osprint("ERROR: Failed to compile Hook.lua: " .. tostring(hook_err) .. "\n") 569 end 570 else 571 osprint("ERROR: Failed to read Hook.lua\n") 572 end 573 else 574 osprint("ERROR: Failed to open /os/libs/Hook.lua\n") 575 end 576 else 577 osprint("ERROR: CRamdiskOpen not available\n") 578 end 579 580 -- Load System Management Library first 581 osprint("\n=== Loading System Management Library ===\n") 582 local sys = nil 583 584 if CRamdiskOpen then 585 local sys_handle = CRamdiskOpen("/os/libs/Sys.lua", "r") 586 if sys_handle then 587 local sys_code = CRamdiskRead(sys_handle) 588 CRamdiskClose(sys_handle) 589 590 if sys_code then 591 local sys_func, sys_err = load(sys_code, "/os/libs/Sys.lua", "t") 592 if sys_func then 593 sys = sys_func() 594 if sys then 595 osprint("System Management Library loaded successfully!\n") 596 _G.sys = sys 597 598 -- Export KEY_ and MOD_ constants to global namespace 599 for k, v in pairs(sys) do 600 if k:match("^KEY_") or k:match("^MOD_") or k:match("^EVENT_") then 601 _G[k] = v 602 end 603 end 604 else 605 osprint("ERROR: Sys.lua did not return module\n") 606 end 607 else 608 osprint("ERROR: Failed to compile Sys.lua: " .. tostring(sys_err) .. "\n") 609 end 610 else 611 osprint("ERROR: Could not read Sys.lua\n") 612 end 613 else 614 osprint("ERROR: Could not open Sys.lua\n") 615 end 616 else 617 osprint("ERROR: Ramdisk functions not available!\n") 618 end 619 620 -- Load Image Library 621 osprint("\n=== Loading Image Library ===\n") 622 if CRamdiskOpen then 623 local image_handle = CRamdiskOpen("/os/libs/Image.lua", "r") 624 if image_handle then 625 local image_code = CRamdiskRead(image_handle) 626 CRamdiskClose(image_handle) 627 628 if image_code then 629 local image_func, image_err = load(image_code, "/os/libs/Image.lua", "t") 630 if image_func then 631 _G.Image = image_func() 632 if _G.Image then 633 osprint("Image Library loaded successfully!\n") 634 else 635 osprint("ERROR: Image.lua did not return module\n") 636 end 637 else 638 osprint("ERROR: Failed to compile Image.lua: " .. tostring(image_err) .. "\n") 639 end 640 else 641 osprint("ERROR: Failed to read Image.lua\n") 642 end 643 else 644 osprint("ERROR: Failed to open /os/libs/Image.lua\n") 645 end 646 end 647 648 -- Load run module 649 osprint("\n=== Loading Run Module ===\n") 650 if CRamdiskOpen then 651 local run_handle, run_err = CRamdiskOpen("/os/libs/Run.lua", "r") 652 if not run_handle then 653 osprint("ERROR: Could not open Run.lua: " .. tostring(run_err) .. "\n") 654 else 655 local run_code, run_read_err = CRamdiskRead(run_handle) 656 CRamdiskClose(run_handle) 657 658 if not run_code then 659 osprint("ERROR: Could not read Run.lua: " .. tostring(run_read_err) .. "\n") 660 else 661 osprint("Run code loaded, size: " .. tostring(#run_code) .. " bytes\n") 662 local run_func, run_compile_err = load(run_code, "/os/libs/Run.lua", "t") 663 if not run_func then 664 osprint("ERROR: Failed to compile run module: " .. tostring(run_compile_err) .. "\n") 665 else 666 osprint("Run module compiled successfully, executing...\n") 667 local run_success, run_result = pcall(run_func) 668 if not run_success then 669 osprint("ERROR: Failed to execute run module: " .. tostring(run_result) .. "\n") 670 else 671 osprint("Run module loaded successfully!\n") 672 _G.run = run_result 673 end 674 end 675 end 676 end 677 end 678 679 -- Load Timer Library 680 osprint("\n=== Loading Timer Library ===\n") 681 if CRamdiskOpen then 682 local timer_handle = CRamdiskOpen("/os/libs/Timer.lua", "r") 683 if timer_handle then 684 local timer_code = CRamdiskRead(timer_handle) 685 CRamdiskClose(timer_handle) 686 687 if timer_code then 688 local timer_func, timer_err = load(timer_code, "/os/libs/Timer.lua", "t") 689 if timer_func then 690 _G.Timer = timer_func() 691 if _G.Timer then 692 osprint("Timer Library loaded successfully!\n") 693 else 694 osprint("ERROR: Timer.lua did not return module\n") 695 end 696 else 697 osprint("ERROR: Failed to compile Timer.lua: " .. tostring(timer_err) .. "\n") 698 end 699 else 700 osprint("ERROR: Failed to read Timer.lua\n") 701 end 702 else 703 osprint("ERROR: Failed to open /os/libs/Timer.lua\n") 704 end 705 end 706 707 -- Initialize VESA Graphics 708 osprint("\n=== Initializing VESA Graphics ===\n") 709 if VESAInit then 710 local result = VESAInit() 711 if result then 712 osprint("VESA initialized successfully!\n") 713 714 -- Set graphics mode using constants from C 715 local width = DEFAULT_SCREEN_WIDTH or 1200 716 local height = DEFAULT_SCREEN_HEIGHT or 900 717 local bpp = DEFAULT_SCREEN_BPP or 32 718 osprint("Setting VESA mode to " .. width .. "x" .. height .. "x" .. bpp .. "...\n") 719 if VESASetMode then 720 local mode_result = VESASetMode(width, height, bpp) 721 osprint("VESASetMode result: " .. tostring(mode_result) .. "\n") 722 end 723 724 -- Initialize window stack 725 _G.window_stack = {} 726 727 -- Function to rebuild window stack from all apps 728 729 730 731 732 -- Load and run postinit.lua 733 osprint("\n=== Loading postinit.lua ===\n") 734 if CRamdiskOpen then 735 local postinit_handle = CRamdiskOpen("/os/postinit.lua", "r") 736 if postinit_handle then 737 local postinit_code = CRamdiskRead(postinit_handle) 738 CRamdiskClose(postinit_handle) 739 740 if postinit_code then 741 local postinit_func, postinit_err = load(postinit_code, "/os/postinit.lua", "t") 742 if postinit_func then 743 local success, err = pcall(postinit_func) 744 if not success then 745 osprint("ERROR: postinit.lua failed: " .. tostring(err) .. "\n") 746 end 747 else 748 osprint("ERROR: Failed to compile postinit.lua: " .. tostring(postinit_err) .. "\n") 749 end 750 else 751 osprint("ERROR: Could not read postinit.lua\n") 752 end 753 else 754 osprint("WARNING: postinit.lua not found\n") 755 end 756 end 757 758 -- Initial draw 759 osprint("\n=== Drawing Initial Frame ===\n") 760 --drawAllWindows() 761 762 -- Main render loop 763 osprint("\n=== Starting Render Loop ===\n") 764 765 else 766 osprint("ERROR: VESA initialization failed\n") 767 end 768 else 769 osprint("ERROR: VESAInit function not available\n") 770 end 771 772 local function rebuildWindowStack() 773 -- Clear current stack 774 _G.window_stack = {} 775 776 -- Collect all windows from all apps 777 if _G.sys and _G.sys.applications then 778 for pid, app in pairs(_G.sys.applications) do 779 if app.windows then 780 for idx, window in ipairs(app.windows) do 781 if type(window) ~= "table" then 782 osprint("ERROR: app.windows[" .. idx .. "] is a " .. type(window) .. " not a table! pid=" .. tostring(pid) .. " appName=" .. tostring(app.appName) .. "\n") 783 else 784 table.insert(_G.window_stack, window) 785 end 786 end 787 end 788 end 789 end 790 791 -- Sort windows by creation time and always-on-top flag 792 -- Order: background < normal windows (by time) < always-on-top windows (by time) 793 table.sort(_G.window_stack, function(a, b) 794 local a_bg = a.isBackground or false 795 local b_bg = b.isBackground or false 796 local a_top = a.alwaysOnTop or false 797 local b_top = b.alwaysOnTop or false 798 799 -- Background windows always first 800 if a_bg and not b_bg then return true end 801 if b_bg and not a_bg then return false end 802 803 -- Always-on-top windows always last 804 if a_top and not b_top then return false end 805 if b_top and not a_top then return true end 806 807 -- Otherwise sort by creation time 808 local a_time = a.createdAt or 0 809 local b_time = b.createdAt or 0 810 return a_time < b_time 811 end) 812 813 end 814 -- Function to draw all app windows 815 local drawCount = 0 816 local function drawAllWindows() 817 rebuildWindowStack() 818 819 drawCount = drawCount + 1 820 821 -- Check if we're in prompt mode 822 local promptMode = _G.sys and _G.sys.promptMode 823 local inPromptMode = promptMode and promptMode.active 824 local promptWindow = inPromptMode and promptMode.window or nil 825 826 -- Phase 1: Render dirty windows to their buffers 827 if type(_G.window_stack) ~= "table" then 828 osprint("[ERROR] window_stack is not a table, it is: " .. type(_G.window_stack) .. "\n") 829 _G.window_stack = {} 830 end 831 for i, window in ipairs(_G.window_stack) do 832 -- In prompt mode, only draw the prompt window 833 if inPromptMode and window ~= promptWindow then 834 -- Skip all windows except the prompt window 835 goto continue 836 end 837 838 -- Render if window is visible and has either onDraw callback or imperative draw ops 839 local has_draw_content = window.onDraw or (window._draw_ops and #window._draw_ops > 0) 840 if window.visible and has_draw_content then 841 -- Calculate full window dimensions 842 local BORDER_WIDTH = window.BORDER_WIDTH or 2 843 local TITLE_BAR_HEIGHT = window.TITLE_BAR_HEIGHT or 20 844 local buf_w = window.width 845 local buf_h = window.height 846 local is_active = (i == #_G.window_stack) 847 848 if not window.isBorderless then 849 buf_w = buf_w + (BORDER_WIDTH * 2) 850 buf_h = buf_h + TITLE_BAR_HEIGHT + (BORDER_WIDTH * 2) 851 end 852 853 -- Create buffer if needed 854 if not window.buffer then 855 if osprint then 856 osprint(string.format("INIT: Creating window buffer %dx%d\n", buf_w, buf_h)) 857 end 858 window.buffer = VESACreateWindowBuffer(buf_w, buf_h) 859 if osprint then 860 osprint(string.format("INIT: Buffer created: %s\n", tostring(window.buffer))) 861 end 862 window.dirty = true 863 end 864 865 -- Only render to buffer if dirty 866 if window.dirty then 867 window.dirty = false 868 869 -- Set render target to this window's buffer 870 VESASetRenderTarget(window.buffer) 871 872 local draw_ops = {} 873 local function addRect(x, y, w, h, r, g, b) 874 draw_ops[#draw_ops + 1] = {OP_RECT_FILL, x, y, w, h, r, g, b} 875 end 876 local function addText(x, y, text, r, g, b, scale) 877 scale = scale or 1 878 draw_ops[#draw_ops + 1] = {OP_TEXT, x, y, text, r, g, b, scale} 879 end 880 881 -- Helper function to draw window frame (title bar, borders, buttons) 882 -- This will be called AFTER content is drawn so frame appears on top 883 local function drawWindowFrame() 884 if window.isBorderless then return end 885 886 -- Draw title bar 887 addRect(0, 0, buf_w, TITLE_BAR_HEIGHT, 60, 60, 60) 888 889 -- Draw icon if available 890 local title_x_offset = 5 891 if window.appInstance and window.appInstance.iconBuffer then 892 local icon = window.appInstance.iconBuffer 893 local icon_w = window.appInstance.iconWidth 894 local icon_h = window.appInstance.iconHeight 895 896 -- Scale icon to fit title bar (max 16x16) 897 local max_icon_size = TITLE_BAR_HEIGHT - 4 898 local scale = math.min(max_icon_size / icon_w, max_icon_size / icon_h, 1.0) 899 local draw_icon_w = math.floor(icon_w * scale) 900 local draw_icon_h = math.floor(icon_h * scale) 901 local icon_y = math.floor((TITLE_BAR_HEIGHT - draw_icon_h) / 2) 902 903 -- Draw icon pixels 904 if ImageGetPixel then 905 for iy = 0, draw_icon_h - 1 do 906 for ix = 0, draw_icon_w - 1 do 907 -- Sample from source icon 908 local src_x = math.floor(ix / scale) 909 local src_y = math.floor(iy / scale) 910 local r, g, b, a = ImageGetPixel(icon, src_x, src_y) 911 912 -- Only draw if not fully transparent 913 if r and a and a > 128 then 914 draw_ops[#draw_ops + 1] = {OP_PIXEL, 2 + ix, icon_y + iy, r, g, b} 915 end 916 end 917 end 918 end 919 920 title_x_offset = 2 + draw_icon_w + 4 -- Icon + spacing 921 end 922 923 -- Draw window title (if provided) 924 if window.title then 925 addText(title_x_offset, 4, window.title, 255, 255, 255) 926 end 927 928 -- Draw window control buttons (minimize, maximize, close) 929 local btnSize = TITLE_BAR_HEIGHT - 6 930 local btnY = 3 931 local btnSpacing = 2 932 933 -- Close button (light grey, red X on right side) 934 local closeButtonX = buf_w - btnSize - 3 935 addRect(closeButtonX, btnY, btnSize, btnSize, 180, 180, 180) 936 addText(closeButtonX + 3, btnY + 1, "X", 200, 60, 60) 937 938 -- Maximize button (light grey, next to close) 939 local maxButtonX = closeButtonX - btnSize - btnSpacing 940 addRect(maxButtonX, btnY, btnSize, btnSize, 180, 180, 180) 941 -- Draw maximize/restore icon 942 addText(maxButtonX, btnY + 4, "[]", 40, 40, 40) 943 944 -- Minimize button (light grey, next to maximize) 945 local minButtonX = maxButtonX - btnSize - btnSpacing 946 addRect(minButtonX, btnY, btnSize, btnSize, 180, 180, 180) 947 addText(minButtonX + 3, btnY + 1, "_", 40, 40, 40) 948 949 -- Draw borders 950 addRect(0, 0, buf_w, BORDER_WIDTH, 100, 100, 100) 951 addRect(0, buf_h - BORDER_WIDTH, buf_w, BORDER_WIDTH, 100, 100, 100) 952 addRect(0, 0, BORDER_WIDTH, buf_h, 100, 100, 100) 953 addRect(buf_w - BORDER_WIDTH, 0, BORDER_WIDTH, buf_h, 100, 100, 100) 954 end 955 956 -- Call app's draw callback 957 local content_x = window.isBorderless and 0 or BORDER_WIDTH 958 local content_y = window.isBorderless and 0 or (BORDER_WIDTH + TITLE_BAR_HEIGHT) 959 960 -- Build selection ranges BEFORE creating window_gfx so drawText can use them 961 local selectedRanges = {} 962 if window.selection and window.selection.start and window.selection.finish and window.selectableText then 963 local startIdx = window.selection.start.index 964 local startPos = window.selection.start.pos 965 local finishIdx = window.selection.finish.index 966 local finishPos = window.selection.finish.pos 967 968 -- Normalize so start is before finish 969 if startIdx > finishIdx or (startIdx == finishIdx and startPos > finishPos) then 970 startIdx, finishIdx = finishIdx, startIdx 971 startPos, finishPos = finishPos, startPos 972 end 973 974 for i = startIdx, finishIdx do 975 local s = (i == startIdx) and startPos or 1 976 local e = (i == finishIdx) and finishPos or 99999 -- will be clamped 977 selectedRanges[i] = {s = s, e = e} 978 end 979 end 980 981 -- Track text index for selection highlighting 982 local textIndex = 0 983 984 local window_gfx = { 985 fillRect = function(self, x, y, w, h, color) 986 if type(self) == "number" then 987 color = h 988 h = w 989 w = y 990 y = x 991 x = self 992 end 993 local r = bit.band(bit.rshift(color, 16), 0xFF) 994 local g = bit.band(bit.rshift(color, 8), 0xFF) 995 local b = bit.band(color, 0xFF) 996 addRect(content_x + x, content_y + y, w, h, r, g, b) 997 end, 998 drawRect = function(self, x, y, w, h, color) 999 if type(self) == "number" then 1000 color = h 1001 h = w 1002 w = y 1003 y = x 1004 x = self 1005 end 1006 local r = bit.band(bit.rshift(color, 16), 0xFF) 1007 local g = bit.band(bit.rshift(color, 8), 0xFF) 1008 local b = bit.band(color, 0xFF) 1009 -- Draw unfilled rectangle using 4 lines (top, bottom, left, right) 1010 addRect(content_x + x, content_y + y, w, 1, r, g, b) -- Top 1011 addRect(content_x + x, content_y + y + h - 1, w, 1, r, g, b) -- Bottom 1012 addRect(content_x + x, content_y + y, 1, h, r, g, b) -- Left 1013 addRect(content_x + x + w - 1, content_y + y, 1, h, r, g, b) -- Right 1014 end, 1015 drawText = function(self, x, y, text, color, scale) 1016 if type(self) == "string" then 1017 scale = color 1018 color = text 1019 text = y 1020 y = x 1021 x = self 1022 end 1023 scale = scale or 1 1024 textIndex = textIndex + 1 1025 1026 local charWidth = 8 * scale 1027 local charHeight = 12 * scale 1028 local textWidth = #text * charWidth 1029 1030 -- Record text for selection support 1031 table.insert(window.selectableText, { 1032 text = text, 1033 x = x, 1034 y = y, 1035 w = textWidth, 1036 h = charHeight, 1037 color = color, 1038 scale = scale 1039 }) 1040 1041 -- Check if this text has selection highlighting 1042 local selRange = selectedRanges[textIndex] 1043 if selRange then 1044 local r = bit.band(bit.rshift(color, 16), 0xFF) 1045 local g = bit.band(bit.rshift(color, 8), 0xFF) 1046 local b = bit.band(color, 0xFF) 1047 local s = selRange.s 1048 local e = math.min(selRange.e, #text) 1049 1050 -- Draw selection highlight behind text 1051 local hlX = x + (s - 1) * charWidth 1052 local hlW = (e - s + 1) * charWidth 1053 addRect(content_x + hlX, content_y + y - 1, hlW, charHeight, 51, 102, 204) 1054 1055 -- Draw text in 3 parts: before selection, selected (white), after selection 1056 if s > 1 then 1057 addText(content_x + x, content_y + y, text:sub(1, s - 1), r, g, b, scale) 1058 end 1059 -- Selected text (white on blue) 1060 addText(content_x + x + (s - 1) * charWidth, content_y + y, text:sub(s, e), 255, 255, 255, scale) 1061 if e < #text then 1062 addText(content_x + x + e * charWidth, content_y + y, text:sub(e + 1), r, g, b, scale) 1063 end 1064 else 1065 -- No selection, draw normally 1066 local r = bit.band(bit.rshift(color, 16), 0xFF) 1067 local g = bit.band(bit.rshift(color, 8), 0xFF) 1068 local b = bit.band(color, 0xFF) 1069 addText(content_x + x, content_y + y, text, r, g, b, scale) 1070 end 1071 end, 1072 drawUText = function(self, x, y, text, color, scale) 1073 -- Draw unselectable text (not recorded for selection) 1074 if type(self) == "string" then 1075 scale = color 1076 color = text 1077 text = y 1078 y = x 1079 x = self 1080 end 1081 scale = scale or 1 1082 local r = bit.band(bit.rshift(color, 16), 0xFF) 1083 local g = bit.band(bit.rshift(color, 8), 0xFF) 1084 local b = bit.band(color, 0xFF) 1085 addText(content_x + x, content_y + y, text, r, g, b, scale) 1086 end, 1087 drawImage = function(self, image, x, y, w, h) 1088 -- Handle both method and function call syntax 1089 if type(self) == "userdata" then 1090 h = w 1091 w = y 1092 y = x 1093 x = image 1094 image = self 1095 end 1096 -- Add image drawing to buffered operations instead of drawing immediately 1097 -- This ensures images are drawn in the correct order relative to other elements 1098 draw_ops[#draw_ops + 1] = {OP_IMAGE, image, content_x + x, content_y + y, w, h, 1} 1099 end, 1100 drawBuffer = function(self, luaImage, x, y, srcX, srcY, srcW, srcH) 1101 -- Handle both method and function call syntax 1102 if type(self) == "table" and self.buffer then 1103 -- Called as drawBuffer(luaImage, x, y, ...) 1104 srcH = srcW 1105 srcW = srcY 1106 srcY = srcX 1107 srcX = y 1108 y = x 1109 x = luaImage 1110 luaImage = self 1111 end 1112 if not luaImage then return end 1113 1114 -- Get buffer data - support both Image (.buffer) and ImageBuffer (:getData()) 1115 local bufferData 1116 local bufW, bufH 1117 if type(luaImage) == "userdata" then 1118 -- ImageBuffer userdata - use getData() method 1119 bufferData = luaImage:getData() 1120 bufW, bufH = luaImage:getSize() 1121 elseif luaImage.buffer then 1122 -- Image object with .buffer property 1123 bufferData = luaImage.buffer 1124 bufW, bufH = luaImage:getSize() 1125 else 1126 return 1127 end 1128 1129 -- {12, buffer, x, y, bufWidth, bufHeight, srcX, srcY, srcW, srcH} 1130 draw_ops[#draw_ops + 1] = {OP_BUFFER, bufferData, content_x + x, content_y + y, bufW, bufH, srcX or 0, srcY or 0, srcW or 0, srcH or 0} 1131 end, 1132 getWidth = function(self) 1133 return window.width 1134 end, 1135 getHeight = function(self) 1136 return window.height 1137 end 1138 } 1139 1140 -- Call app's onDraw callback if it exists (reactive mode) 1141 if window.onDraw then 1142 -- Clear selectableText before each draw so it reflects current frame 1143 window.selectableText = {} 1144 textIndex = 0 -- Reset text index for this draw 1145 local success, err = pcall(window.onDraw, window_gfx) 1146 if not success then 1147 osprint("ERROR drawing window: " .. tostring(err) .. "\n") 1148 end 1149 end 1150 1151 -- Also add window's imperative draw ops (from window.gfx) 1152 if window._draw_ops and #window._draw_ops > 0 then 1153 local textIndex = 0 1154 for _, op in ipairs(window._draw_ops) do 1155 -- Adjust coordinates to account for window frame 1156 local adjusted_op = {op[1]} 1157 local skipNormalAdd = false 1158 1159 if op[1] == OP_RECT_FILL then 1160 -- {1, x, y, w, h, r, g, b} 1161 adjusted_op[2] = content_x + op[2] 1162 adjusted_op[3] = content_y + op[3] 1163 adjusted_op[4] = op[4] 1164 adjusted_op[5] = op[5] 1165 adjusted_op[6] = op[6] 1166 adjusted_op[7] = op[7] 1167 adjusted_op[8] = op[8] 1168 elseif op[1] == OP_TEXT then 1169 textIndex = textIndex + 1 1170 -- {10, x, y, text, r, g, b, scale} 1171 adjusted_op[2] = content_x + op[2] 1172 adjusted_op[3] = content_y + op[3] 1173 adjusted_op[4] = op[4] 1174 adjusted_op[5] = op[5] 1175 adjusted_op[6] = op[6] 1176 adjusted_op[7] = op[7] 1177 adjusted_op[8] = op[8] or 1 -- scale 1178 1179 -- Check if this text has selection 1180 local selRange = selectedRanges[textIndex] 1181 if selRange then 1182 local text = op[4] 1183 local scale = op[8] or 1 1184 local charWidth = 8 * scale 1185 local charHeight = 12 * scale 1186 local s = selRange.s 1187 local e = math.min(selRange.e, #text) 1188 1189 -- Draw selection highlight behind text 1190 local hlX = op[2] + (s - 1) * charWidth 1191 local hlW = (e - s + 1) * charWidth 1192 draw_ops[#draw_ops + 1] = { 1193 OP_RECT_FILL, 1194 content_x + hlX, 1195 content_y + op[3] - 1, 1196 hlW, 1197 charHeight, 1198 51, 102, 204 -- Selection blue 1199 } 1200 1201 -- Draw text in 3 parts: before selection, selected (white), after selection 1202 if s > 1 then 1203 -- Text before selection (original color) 1204 draw_ops[#draw_ops + 1] = { 1205 OP_TEXT, 1206 content_x + op[2], 1207 content_y + op[3], 1208 text:sub(1, s - 1), 1209 op[5], op[6], op[7], 1210 scale 1211 } 1212 end 1213 1214 -- Selected text (white on blue) 1215 draw_ops[#draw_ops + 1] = { 1216 OP_TEXT, 1217 content_x + op[2] + (s - 1) * charWidth, 1218 content_y + op[3], 1219 text:sub(s, e), 1220 255, 255, 255, -- White text 1221 scale 1222 } 1223 1224 if e < #text then 1225 -- Text after selection (original color) 1226 draw_ops[#draw_ops + 1] = { 1227 OP_TEXT, 1228 content_x + op[2] + e * charWidth, 1229 content_y + op[3], 1230 text:sub(e + 1), 1231 op[5], op[6], op[7], 1232 scale 1233 } 1234 end 1235 skipNormalAdd = true 1236 end 1237 elseif op[1] == OP_PIXEL then 1238 -- {3, x, y, r, g, b} 1239 adjusted_op[2] = content_x + op[2] 1240 adjusted_op[3] = content_y + op[3] 1241 adjusted_op[4] = op[4] 1242 adjusted_op[5] = op[5] 1243 adjusted_op[6] = op[6] 1244 elseif op[1] == OP_IMAGE then 1245 -- {11, image, x, y, w, h, scale} 1246 adjusted_op[2] = op[2] 1247 adjusted_op[3] = content_x + op[3] 1248 adjusted_op[4] = content_y + op[4] 1249 adjusted_op[5] = op[5] 1250 adjusted_op[6] = op[6] 1251 adjusted_op[7] = op[7] 1252 elseif op[1] == OP_BUFFER then 1253 -- {12, buffer, x, y, bufWidth, bufHeight, srcX, srcY, srcW, srcH} 1254 adjusted_op[2] = op[2] -- buffer string 1255 adjusted_op[3] = content_x + op[3] -- x 1256 adjusted_op[4] = content_y + op[4] -- y 1257 adjusted_op[5] = op[5] -- bufWidth 1258 adjusted_op[6] = op[6] -- bufHeight 1259 adjusted_op[7] = op[7] -- srcX 1260 adjusted_op[8] = op[8] -- srcY 1261 adjusted_op[9] = op[9] -- srcW 1262 adjusted_op[10] = op[10] -- srcH 1263 end 1264 1265 if not skipNormalAdd then 1266 draw_ops[#draw_ops + 1] = adjusted_op 1267 end 1268 end 1269 end 1270 1271 -- Draw window frame (title bar, borders) AFTER content 1272 -- so they appear on top and aren't overwritten by app content 1273 drawWindowFrame() 1274 1275 -- Render to buffer 1276 if VESAProcessBufferedDrawOps and #draw_ops > 0 then 1277 VESAProcessBufferedDrawOps(draw_ops) 1278 end 1279 1280 -- Reset render target 1281 VESASetRenderTarget(nil) 1282 end 1283 end 1284 1285 ::continue:: 1286 end 1287 1288 -- Phase 2: Blit all window buffers to screen 1289 for i, window in ipairs(_G.window_stack) do 1290 -- In prompt mode, only blit the prompt window 1291 if inPromptMode and window ~= promptWindow then 1292 goto continue_blit 1293 end 1294 if window.visible and window.buffer then 1295 local BORDER_WIDTH = window.BORDER_WIDTH or 2 1296 local TITLE_BAR_HEIGHT = window.TITLE_BAR_HEIGHT or 20 1297 local blit_x = window.x 1298 local blit_y = window.y 1299 1300 if not window.isBorderless then 1301 blit_x = blit_x - BORDER_WIDTH 1302 blit_y = blit_y - BORDER_WIDTH - TITLE_BAR_HEIGHT 1303 end 1304 1305 VESABlitWindowBuffer(window.buffer, blit_x, blit_y) 1306 end 1307 1308 ::continue_blit:: 1309 end 1310 end 1311 1312 -- Update a specific region of a window's buffer to the screen without full redraw 1313 -- This is useful for paint apps that only need to update the area where the brush was used 1314 -- window: the window object 1315 -- x, y: top-left corner of the region in window content coordinates 1316 -- w, h: width and height of the region to update 1317 function updateWindowRegion(window, x, y, w, h) 1318 if not window or not window.buffer or not window.visible then 1319 return false 1320 end 1321 1322 if not VESABlitWindowBufferRegion then 1323 return false 1324 end 1325 1326 local BORDER_WIDTH = window.BORDER_WIDTH or 2 1327 local TITLE_BAR_HEIGHT = window.TITLE_BAR_HEIGHT or 20 1328 1329 -- Calculate buffer coordinates (accounting for title bar and border in buffer) 1330 local buf_x, buf_y 1331 if window.isBorderless then 1332 buf_x = x 1333 buf_y = y 1334 else 1335 buf_x = BORDER_WIDTH + x 1336 buf_y = BORDER_WIDTH + TITLE_BAR_HEIGHT + y 1337 end 1338 1339 -- Calculate screen coordinates 1340 local screen_x, screen_y 1341 if window.isBorderless then 1342 screen_x = window.x + x 1343 screen_y = window.y + y 1344 else 1345 screen_x = window.x - BORDER_WIDTH + buf_x 1346 screen_y = window.y - BORDER_WIDTH - TITLE_BAR_HEIGHT + buf_y 1347 end 1348 1349 -- Blit just the region 1350 VESABlitWindowBufferRegion(window.buffer, screen_x, screen_y, buf_x, buf_y, w, h) 1351 return true 1352 end 1353 1354 -- Make it globally available 1355 _G.updateWindowRegion = updateWindowRegion 1356 1357 -- Persistent frame counter (outside function to persist across calls) 1358 _G.frame_counter = _G.frame_counter or 0 1359 _G.last_mouse_x = _G.last_mouse_x or _G.cursor_state.x 1360 _G.last_mouse_y = _G.last_mouse_y or _G.cursor_state.y 1361 _G.dragging_window = _G.dragging_window or nil -- Persistent drag state 1362 _G.drag_offset_x = _G.drag_offset_x or 0 -- Persistent drag offset 1363 _G.drag_offset_y = _G.drag_offset_y or 0 -- Persistent drag offset 1364 _G.resizing_window = _G.resizing_window or nil -- Persistent resize state 1365 _G.resize_edge = _G.resize_edge or nil -- Which edge/corner: 'left', 'right', 'top', 'bottom', 'topleft', 'topright', 'bottomleft', 'bottomright' 1366 _G.resize_start_x = _G.resize_start_x or 0 -- Starting mouse position 1367 _G.resize_start_y = _G.resize_start_y or 0 1368 _G.resize_start_win_x = _G.resize_start_win_x or 0 -- Starting window position/size 1369 _G.resize_start_win_y = _G.resize_start_win_y or 0 1370 _G.resize_start_win_w = _G.resize_start_win_w or 0 1371 _G.resize_start_win_h = _G.resize_start_win_h or 0 1372 _G.force_redraw = true -- Force initial redraw 1373 1374 -- Internal MainDraw implementation 1375 local function MainDrawImpl() 1376 local redraw_interval = 1 -- Redraw every frame for smooth mouse cursor 1377 1378 -- Update Timer system 1379 if _G.Timer then 1380 -- Use GetTicks if available, otherwise use frame counter 1381 if GetTicks then 1382 _G.Timer.setTick(GetTicks()) 1383 else 1384 -- Fallback: estimate ~60fps, so each frame is ~16.67ms 1385 _G.Timer.setTick((_G.frame_counter or 0) * 17) 1386 end 1387 _G.Timer.update() 1388 end 1389 1390 -- Poll USB mouse 1391 if USBMousePoll then 1392 USBMousePoll() 1393 end 1394 1395 -- Get mouse state 1396 local mouse_moved = false 1397 local mouse1_down = false 1398 local mouse1_pressed = false 1399 local mouse1_released = false 1400 1401 if MouseGetState then 1402 local mouse = MouseGetState() 1403 if mouse and mouse.initialized then 1404 local old_x = _G.cursor_state.x 1405 local old_y = _G.cursor_state.y 1406 _G.cursor_state.x = mouse.x 1407 _G.cursor_state.y = mouse.y 1408 mouse_moved = (mouse.x ~= _G.last_mouse_x or mouse.y ~= _G.last_mouse_y) 1409 1410 -- Debug: print when position changes 1411 -- Disabled to reduce boot noise 1412 -- if mouse_moved then 1413 -- osprint("L") 1414 -- end 1415 1416 -- Detect button state changes 1417 local current_buttons = mouse.buttons 1418 mouse1_down = bit.band(current_buttons, 1) ~= 0 1419 1420 -- Track previous state for press/release detection 1421 if not _G.last_mouse_buttons then 1422 _G.last_mouse_buttons = 0 1423 end 1424 local last_buttons = _G.last_mouse_buttons 1425 mouse1_pressed = mouse1_down and (bit.band(last_buttons, 1) == 0) 1426 mouse1_released = (not mouse1_down) and (bit.band(last_buttons, 1) ~= 0) 1427 _G.last_mouse_buttons = current_buttons 1428 end 1429 end 1430 1431 -- Helper function to detect which edge/corner the mouse is over 1432 local function getResizeEdge(window, mx, my) 1433 if not window.resizable or window.isBorderless then 1434 return nil 1435 end 1436 1437 local BORDER_WIDTH = window.BORDER_WIDTH or 2 1438 local TITLE_BAR_HEIGHT = window.TITLE_BAR_HEIGHT or 20 1439 1440 -- Window bounds including borders 1441 local win_left = window.x - BORDER_WIDTH 1442 local win_right = window.x + window.width + BORDER_WIDTH 1443 local win_top = window.y - BORDER_WIDTH - TITLE_BAR_HEIGHT 1444 local win_bottom = window.y + window.height + BORDER_WIDTH 1445 1446 -- Check if mouse is within border area 1447 local in_left = mx >= win_left and mx < win_left + BORDER_WIDTH 1448 local in_right = mx >= win_right - BORDER_WIDTH and mx < win_right 1449 local in_top = my >= win_top and my < win_top + BORDER_WIDTH 1450 local in_bottom = my >= win_bottom - BORDER_WIDTH and my < win_bottom 1451 1452 -- Corners first (higher priority) 1453 if in_left and in_top then return 'topleft' end 1454 if in_right and in_top then return 'topright' end 1455 if in_left and in_bottom then return 'bottomleft' end 1456 if in_right and in_bottom then return 'bottomright' end 1457 1458 -- Edges 1459 if in_left then return 'left' end 1460 if in_right then return 'right' end 1461 if in_top then return 'top' end 1462 if in_bottom then return 'bottom' end 1463 1464 return nil 1465 end 1466 1467 -- Handle window dragging and mouse clicks 1468 if mouse1_pressed and not _G.dragging_window and not _G.resizing_window then 1469 -- Check if we're in prompt mode 1470 local promptMode = _G.sys and _G.sys.promptMode 1471 local inPromptMode = promptMode and promptMode.active 1472 local promptWindow = inPromptMode and promptMode.window or nil 1473 1474 -- Check if clicked on a window (from top to bottom) 1475 local clicked_window = nil 1476 local clicked_in_titlebar = false 1477 local clicked_resize_edge = nil 1478 1479 for i = #_G.window_stack, 1, -1 do 1480 local window = _G.window_stack[i] 1481 1482 -- In prompt mode, only allow interaction with the prompt window 1483 if inPromptMode and window ~= promptWindow then 1484 goto skip_window 1485 end 1486 1487 -- Skip background windows (non-focusable) 1488 if window.visible and not window.isBackground then 1489 local TITLE_BAR_HEIGHT = window.TITLE_BAR_HEIGHT or 20 1490 local BORDER_WIDTH = window.BORDER_WIDTH or 2 1491 1492 -- Check for resize edge first (if window is resizable) 1493 local resize_edge = getResizeEdge(window, _G.cursor_state.x, _G.cursor_state.y) 1494 if resize_edge then 1495 clicked_window = window 1496 clicked_resize_edge = resize_edge 1497 break 1498 end 1499 1500 -- Calculate actual screen position of title bar 1501 -- Move grabbable area up by quarter the title bar height 1502 local title_x = window.x - BORDER_WIDTH 1503 local title_y = window.y - BORDER_WIDTH - TITLE_BAR_HEIGHT - (TITLE_BAR_HEIGHT / 4) 1504 local title_w = window.width + (BORDER_WIDTH * 2) 1505 local title_h = TITLE_BAR_HEIGHT 1506 1507 -- Calculate window control button positions (must match drawing code) 1508 local btnSize = TITLE_BAR_HEIGHT - 6 1509 local btnY = window.y - BORDER_WIDTH - TITLE_BAR_HEIGHT + 3 1510 local btnSpacing = 2 1511 local buf_w = window.width + (BORDER_WIDTH * 2) 1512 1513 -- Close button (rightmost) 1514 local closeButtonX = window.x - BORDER_WIDTH + buf_w - btnSize - 3 1515 -- Maximize button (next to close) 1516 local maxButtonX = closeButtonX - btnSize - btnSpacing 1517 -- Minimize button (next to maximize) 1518 local minButtonX = maxButtonX - btnSize - btnSpacing 1519 1520 -- Check if clicked on close button 1521 if not window.isBorderless and 1522 _G.cursor_state.x >= closeButtonX and _G.cursor_state.x < closeButtonX + btnSize and 1523 _G.cursor_state.y >= btnY and _G.cursor_state.y < btnY + btnSize then 1524 -- Close window (respects onClose callback) 1525 if window.close then 1526 window:close() 1527 else 1528 window:hide() 1529 end 1530 window.dirty = false 1531 break 1532 end 1533 1534 -- Check if clicked on maximize button 1535 if not window.isBorderless and 1536 _G.cursor_state.x >= maxButtonX and _G.cursor_state.x < maxButtonX + btnSize and 1537 _G.cursor_state.y >= btnY and _G.cursor_state.y < btnY + btnSize then 1538 if window.maximize then 1539 window:maximize() 1540 end 1541 break 1542 end 1543 1544 -- Check if clicked on minimize button 1545 if not window.isBorderless and 1546 _G.cursor_state.x >= minButtonX and _G.cursor_state.x < minButtonX + btnSize and 1547 _G.cursor_state.y >= btnY and _G.cursor_state.y < btnY + btnSize then 1548 if window.minimize then 1549 window:minimize() 1550 end 1551 break 1552 end 1553 1554 -- Check if clicked in title bar (but not close button) 1555 if not window.isBorderless and 1556 _G.cursor_state.x >= title_x and _G.cursor_state.x < title_x + title_w and 1557 _G.cursor_state.y >= title_y and _G.cursor_state.y < title_y + title_h then 1558 clicked_window = window 1559 clicked_in_titlebar = true 1560 break 1561 end 1562 1563 -- Check if clicked in window content area 1564 if _G.cursor_state.x >= window.x and _G.cursor_state.x < window.x + window.width and 1565 _G.cursor_state.y >= window.y and _G.cursor_state.y < window.y + window.height then 1566 clicked_window = window 1567 clicked_in_titlebar = false 1568 break 1569 end 1570 end 1571 1572 ::skip_window:: 1573 end 1574 1575 if clicked_window then 1576 -- Track the previous focused window 1577 local previousWindow = nil 1578 if _G.window_stack and #_G.window_stack > 0 then 1579 -- Find the currently focused window (last non-background, non-always-on-top) 1580 for i = #_G.window_stack, 1, -1 do 1581 local w = _G.window_stack[i] 1582 if not w.isBackground and not w.alwaysOnTop then 1583 previousWindow = w 1584 break 1585 end 1586 end 1587 end 1588 1589 -- Bring window to front by updating its creation timestamp 1590 -- This makes it appear last in the sorted window_stack (on top) 1591 if not _G._app_timestamp then 1592 _G._app_timestamp = 0 1593 end 1594 _G._app_timestamp = _G._app_timestamp + 1 1595 clicked_window.createdAt = _G._app_timestamp 1596 1597 -- Call onFocusLost on the previous window if it's different 1598 if previousWindow and previousWindow ~= clicked_window and previousWindow.onFocusLost then 1599 local success, err = pcall(previousWindow.onFocusLost) 1600 if not success and osprint then 1601 osprint("[ERROR] onFocusLost callback failed: " .. tostring(err) .. "\n") 1602 end 1603 end 1604 1605 -- Call onFocus on the newly focused window 1606 if clicked_window.onFocus then 1607 local success, err = pcall(clicked_window.onFocus) 1608 if not success and osprint then 1609 osprint("[ERROR] onFocus callback failed: " .. tostring(err) .. "\n") 1610 end 1611 end 1612 1613 -- Set as active window for keyboard input 1614 if _G.sys and _G.sys.setActiveWindow and clicked_window.onInput then 1615 _G.sys.setActiveWindow(clicked_window) 1616 end 1617 1618 if clicked_resize_edge then 1619 -- Start resizing 1620 _G.resizing_window = clicked_window 1621 _G.resize_edge = clicked_resize_edge 1622 _G.resize_start_x = _G.cursor_state.x 1623 _G.resize_start_y = _G.cursor_state.y 1624 _G.resize_start_win_x = clicked_window.x 1625 _G.resize_start_win_y = clicked_window.y 1626 _G.resize_start_win_w = clicked_window.width 1627 _G.resize_start_win_h = clicked_window.height 1628 elseif clicked_in_titlebar then 1629 -- Start dragging - don't mark dirty, just move the existing buffer 1630 _G.dragging_window = clicked_window 1631 _G.drag_offset_x = _G.cursor_state.x - clicked_window.x 1632 _G.drag_offset_y = _G.cursor_state.y - clicked_window.y 1633 1634 -- Call onDragStart callback 1635 if clicked_window.onDragStart then 1636 local success, err = pcall(clicked_window.onDragStart, clicked_window.x, clicked_window.y) 1637 if not success and osprint then 1638 osprint("[ERROR] onDragStart callback failed: " .. tostring(err) .. "\n") 1639 end 1640 end 1641 else 1642 -- Click inside window content - mark dirty and send click event 1643 clicked_window.dirty = true 1644 1645 local click_x = _G.cursor_state.x - clicked_window.x 1646 local click_y = _G.cursor_state.y - clicked_window.y 1647 1648 -- Track this window for mouse move/up events 1649 _G.mouse_capture_window = clicked_window 1650 1651 -- Call onMouseDown first (new event) 1652 local mouseDownHandled = false 1653 if clicked_window.onMouseDown then 1654 local success, result = pcall(clicked_window.onMouseDown, click_x, click_y) 1655 if not success and osprint then 1656 osprint("[ERROR] onMouseDown callback failed: " .. tostring(result) .. "\n") 1657 elseif result == true then 1658 mouseDownHandled = true 1659 end 1660 end 1661 1662 -- Also call onClick for backwards compatibility 1663 if clicked_window.onClick then 1664 local success, result = pcall(clicked_window.onClick, click_x, click_y) 1665 if not success and osprint then 1666 osprint("[ERROR] onClick callback failed: " .. tostring(result) .. "\n") 1667 elseif result == true then 1668 mouseDownHandled = true 1669 end 1670 end 1671 1672 -- If mouse events weren't handled and window is selectable, start text selection 1673 if not mouseDownHandled and clicked_window.selectable ~= false then 1674 local textIdx, charPos = findTextAtPosition(clicked_window, click_x, click_y) 1675 if textIdx then 1676 clicked_window.selection = { 1677 start = { index = textIdx, pos = charPos }, 1678 finish = { index = textIdx, pos = charPos }, 1679 content = "", 1680 type = "text" 1681 } 1682 clicked_window.dirty = true 1683 else 1684 -- Clear selection when clicking outside text 1685 clicked_window.selection = nil 1686 end 1687 end 1688 end 1689 else 1690 -- Clicked on background (no window was clicked) 1691 -- Fire BackgroundFocused hook so apps like taskbar can close menus 1692 if _G.sys and _G.sys.hook then 1693 _G.sys.hook:run("BackgroundFocused") 1694 end 1695 end 1696 end 1697 1698 -- Handle mouse move while button is down (for painting/selection) 1699 if _G.mouse_capture_window and mouse1_down and mouse_moved then 1700 local win = _G.mouse_capture_window 1701 local move_x = _G.cursor_state.x - win.x 1702 local move_y = _G.cursor_state.y - win.y 1703 1704 local mouseMoveHandled = false 1705 if win.onMouseMove then 1706 win.dirty = true 1707 local success, result = pcall(win.onMouseMove, move_x, move_y) 1708 if not success and osprint then 1709 osprint("[ERROR] onMouseMove callback failed: " .. tostring(result) .. "\n") 1710 elseif result == true then 1711 mouseMoveHandled = true 1712 end 1713 end 1714 1715 -- If mouse move wasn't handled and we have an active selection, update it 1716 if not mouseMoveHandled and win.selectable ~= false and win.selection and win.selection.start then 1717 local textIdx, charPos = findTextAtPosition(win, move_x, move_y) 1718 if textIdx then 1719 win.selection.finish = { index = textIdx, pos = charPos } 1720 win.dirty = true 1721 end 1722 end 1723 end 1724 1725 -- Update dragging window position 1726 if _G.dragging_window and mouse1_down and mouse_moved then 1727 local new_x = _G.cursor_state.x - _G.drag_offset_x 1728 local new_y = _G.cursor_state.y - _G.drag_offset_y 1729 1730 -- Calculate window dimensions 1731 local BORDER_WIDTH = _G.dragging_window.BORDER_WIDTH or 2 1732 local TITLE_BAR_HEIGHT = _G.dragging_window.TITLE_BAR_HEIGHT or 20 1733 1734 -- Clamp window position to screen bounds (1024x768) 1735 if new_x - BORDER_WIDTH < 0 then 1736 new_x = BORDER_WIDTH 1737 end 1738 if new_y - BORDER_WIDTH < 0 then 1739 new_y = BORDER_WIDTH 1740 end 1741 if new_x + _G.dragging_window.width > 1024 then 1742 new_x = 1024 - _G.dragging_window.width 1743 end 1744 if new_y + _G.dragging_window.height + TITLE_BAR_HEIGHT > 768 then 1745 new_y = 768 - _G.dragging_window.height - TITLE_BAR_HEIGHT 1746 end 1747 1748 -- Update position 1749 _G.dragging_window.x = new_x 1750 _G.dragging_window.y = new_y 1751 1752 -- Call onDrag callback 1753 if _G.dragging_window.onDrag then 1754 local success, err = pcall(_G.dragging_window.onDrag, new_x, new_y) 1755 if not success and osprint then 1756 osprint("[ERROR] onDrag callback failed: " .. tostring(err) .. "\n") 1757 end 1758 end 1759 end 1760 1761 -- Update resizing window 1762 if _G.resizing_window and mouse1_down and mouse_moved then 1763 local dx = _G.cursor_state.x - _G.resize_start_x 1764 local dy = _G.cursor_state.y - _G.resize_start_y 1765 1766 local new_x = _G.resize_start_win_x 1767 local new_y = _G.resize_start_win_y 1768 local new_w = _G.resize_start_win_w 1769 local new_h = _G.resize_start_win_h 1770 1771 local MIN_WIDTH = 100 1772 local MIN_HEIGHT = 80 1773 1774 -- Apply resize based on edge/corner 1775 local edge = _G.resize_edge 1776 1777 -- Handle left edge 1778 if edge == 'left' or edge == 'topleft' or edge == 'bottomleft' then 1779 new_w = _G.resize_start_win_w - dx 1780 if new_w >= MIN_WIDTH then 1781 new_x = _G.resize_start_win_x + dx 1782 else 1783 new_w = MIN_WIDTH 1784 new_x = _G.resize_start_win_x + _G.resize_start_win_w - MIN_WIDTH 1785 end 1786 end 1787 1788 -- Handle right edge 1789 if edge == 'right' or edge == 'topright' or edge == 'bottomright' then 1790 new_w = _G.resize_start_win_w + dx 1791 if new_w < MIN_WIDTH then 1792 new_w = MIN_WIDTH 1793 end 1794 end 1795 1796 -- Handle top edge 1797 if edge == 'top' or edge == 'topleft' or edge == 'topright' then 1798 new_h = _G.resize_start_win_h - dy 1799 if new_h >= MIN_HEIGHT then 1800 new_y = _G.resize_start_win_y + dy 1801 else 1802 new_h = MIN_HEIGHT 1803 new_y = _G.resize_start_win_y + _G.resize_start_win_h - MIN_HEIGHT 1804 end 1805 end 1806 1807 -- Handle bottom edge 1808 if edge == 'bottom' or edge == 'bottomleft' or edge == 'bottomright' then 1809 new_h = _G.resize_start_win_h + dy 1810 if new_h < MIN_HEIGHT then 1811 new_h = MIN_HEIGHT 1812 end 1813 end 1814 1815 -- Store old dimensions for callback 1816 local old_w = _G.resizing_window.width 1817 local old_h = _G.resizing_window.height 1818 1819 -- Update window 1820 _G.resizing_window.x = new_x 1821 _G.resizing_window.y = new_y 1822 _G.resizing_window.width = new_w 1823 _G.resizing_window.height = new_h 1824 _G.resizing_window.dirty = true 1825 1826 -- Free old buffer and clear to force recreation 1827 if _G.resizing_window.buffer and VESAFreeWindowBuffer then 1828 VESAFreeWindowBuffer(_G.resizing_window.buffer) 1829 end 1830 _G.resizing_window.buffer = nil 1831 1832 -- Call onResize callback if dimensions changed 1833 if (new_w ~= old_w or new_h ~= old_h) and _G.resizing_window.onResize then 1834 local success, err = pcall(_G.resizing_window.onResize, new_w, new_h, old_w, old_h) 1835 if not success and osprint then 1836 osprint("[ERROR] onResize callback failed: " .. tostring(err) .. "\n") 1837 end 1838 end 1839 end 1840 1841 -- Draw all windows (buffers make this fast) 1842 drawAllWindows() 1843 1844 -- Stop dragging when button released 1845 if mouse1_released and _G.dragging_window then 1846 -- Call onDragFinish callback 1847 if _G.dragging_window.onDragFinish then 1848 local success, err = pcall(_G.dragging_window.onDragFinish, _G.dragging_window.x, _G.dragging_window.y) 1849 if not success and osprint then 1850 osprint("[ERROR] onDragFinish callback failed: " .. tostring(err) .. "\n") 1851 end 1852 end 1853 1854 _G.dragging_window = nil 1855 end 1856 1857 -- Stop resizing when button released 1858 if mouse1_released and _G.resizing_window then 1859 _G.resizing_window = nil 1860 _G.resize_edge = nil 1861 end 1862 1863 -- Handle mouse up for captured window 1864 if mouse1_released and _G.mouse_capture_window then 1865 local win = _G.mouse_capture_window 1866 local up_x = _G.cursor_state.x - win.x 1867 local up_y = _G.cursor_state.y - win.y 1868 1869 local mouseUpHandled = false 1870 if win.onMouseUp then 1871 local success, result = pcall(win.onMouseUp, up_x, up_y) 1872 if not success and osprint then 1873 osprint("[ERROR] onMouseUp callback failed: " .. tostring(result) .. "\n") 1874 elseif result == true then 1875 mouseUpHandled = true 1876 end 1877 end 1878 1879 -- Finalize text selection if active 1880 if not mouseUpHandled and win.selectable ~= false and win.selection and win.selection.start then 1881 local textIdx, charPos = findTextAtPosition(win, up_x, up_y) 1882 if textIdx then 1883 win.selection.finish = { index = textIdx, pos = charPos } 1884 end 1885 -- Get the selected text content 1886 win.selection.content = getSelectedText(win) 1887 win.selection.type = "text" 1888 win.dirty = true 1889 end 1890 _G.mouse_capture_window = nil 1891 end 1892 1893 -- Update tracking 1894 _G.last_mouse_x = _G.cursor_state.x 1895 _G.last_mouse_y = _G.cursor_state.y 1896 _G.frame_counter = _G.frame_counter + 1 1897 1898 -- Copy the off-screen buffer to the visible framebuffer 1899 -- Skip the cursor region to prevent flickering 1900 if VESACopyBufferToFramebuffer then 1901 local hotspot_x = 5 1902 local hotspot_y = 5 1903 local cursor_draw_x = _G.cursor_state.x - hotspot_x 1904 local cursor_draw_y = _G.cursor_state.y - hotspot_y 1905 local cursor_w = 30 1906 local cursor_h = 30 1907 1908 -- Pass cursor region to skip during buffer copy 1909 if _G.cursor_state.visible and _G.last_cursor_x then 1910 VESACopyBufferToFramebuffer(cursor_draw_x, cursor_draw_y, cursor_w, cursor_h) 1911 else 1912 VESACopyBufferToFramebuffer() 1913 end 1914 end 1915 1916 -- Draw cursor AFTER copying buffer so it's never saved in the buffer 1917 -- VESADrawCursor handles: restore old background (partial), save new, draw cursor 1918 local mouse_x = _G.cursor_state.x 1919 local mouse_y = _G.cursor_state.y 1920 1921 -- Update cursor mode based on mouse state 1922 local new_mode = "cursor" 1923 if _G.dragging_window then 1924 new_mode = "grab" 1925 elseif _G.resizing_window then 1926 new_mode = "grab" 1927 elseif mouse1_down then 1928 new_mode = "left-click" 1929 elseif _G.mouse2_down then 1930 new_mode = "right-click" 1931 else 1932 -- Check if hovering over a window title bar (hover-grab) 1933 for i = #_G.window_stack, 1, -1 do 1934 local window = _G.window_stack[i] 1935 if window.visible and not window.isBackground and not window.isBorderless then 1936 local TITLE_BAR_HEIGHT = window.TITLE_BAR_HEIGHT or 20 1937 local BORDER_WIDTH = window.BORDER_WIDTH or 2 1938 1939 local title_x = window.x - BORDER_WIDTH 1940 local title_y = window.y - BORDER_WIDTH - TITLE_BAR_HEIGHT 1941 local title_w = window.width + (BORDER_WIDTH * 2) 1942 local title_h = TITLE_BAR_HEIGHT 1943 1944 -- Check if mouse is in title bar area 1945 if mouse_x >= title_x and mouse_x < title_x + title_w and 1946 mouse_y >= title_y and mouse_y < title_y + title_h then 1947 new_mode = "hover-grab" 1948 break 1949 end 1950 1951 -- Check if mouse is in window content area (stop checking lower windows) 1952 if mouse_x >= window.x and mouse_x < window.x + window.width and 1953 mouse_y >= window.y and mouse_y < window.y + window.height then 1954 break 1955 end 1956 end 1957 end 1958 end 1959 1960 -- Only update mode if it changed (and wasn't set programmatically to loading/denied) 1961 if _G.cursor_state.mode ~= "loading" and _G.cursor_state.mode ~= "denied" then 1962 if _G.cursor_state.mode ~= new_mode then 1963 _G.cursor_state.mode = new_mode 1964 _G.cursor_buffer_dirty = true 1965 end 1966 end 1967 1968 -- Regenerate cursor buffer if dirty or mode changed 1969 if _G.cursor_buffer_dirty or _G.cursor_last_mode ~= _G.cursor_state.mode then 1970 if _G._cursor_api and _G._cursor_api.regenerate then 1971 _G._cursor_api.regenerate(_G.cursor_state.mode) 1972 end 1973 end 1974 1975 -- Draw cursor with flicker-free background save/restore 1976 if _G.cursor_state.visible then 1977 -- Calculate cursor draw position (hotspot at 5,5) 1978 local hotspot_x = 5 1979 local hotspot_y = 5 1980 local new_x = mouse_x - hotspot_x 1981 local new_y = mouse_y - hotspot_y 1982 1983 -- Get old cursor position 1984 local old_x = (_G.last_cursor_x or mouse_x) - hotspot_x 1985 local old_y = (_G.last_cursor_y or mouse_y) - hotspot_y 1986 1987 -- Use VESAMoveCursor which takes both old and new positions 1988 -- and only restores the L-shaped region (old minus new) 1989 if VESAMoveCursor and _G.last_cursor_x then 1990 VESAMoveCursor(old_x, old_y, new_x, new_y) 1991 elseif VESADrawCursor then 1992 -- Initial draw (no old position) 1993 VESADrawCursor(new_x, new_y) 1994 elseif VESABlitCursor and _G.cursor_buffer then 1995 -- Fallback to old method 1996 VESABlitCursor(_G.cursor_buffer, new_x, new_y, 30, 30) 1997 end 1998 1999 -- Remember cursor position for next frame 2000 _G.last_cursor_x = mouse_x 2001 _G.last_cursor_y = mouse_y 2002 elseif not _G.cursor_state.visible and VESARestoreCursor then 2003 -- Cursor is hidden, restore the background 2004 VESARestoreCursor() 2005 _G.last_cursor_x = nil 2006 _G.last_cursor_y = nil 2007 end 2008 end 2009 2010 -- Main entry point with error handling 2011 function MainDraw() 2012 local success, err = pcall(MainDrawImpl) 2013 if not success then 2014 -- Print error to serial console 2015 if osprint then 2016 osprint("\n[FATAL ERROR in MainDraw]\n") 2017 osprint(tostring(err) .. "\n") 2018 osprint("Stack trace:\n") 2019 osprint(debug.traceback() .. "\n") 2020 end 2021 -- Try to recover by resetting window_stack if corrupted 2022 if type(_G.window_stack) ~= "table" then 2023 _G.window_stack = {} 2024 end 2025 end 2026 end