luajitos

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

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