luajitos

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

commit dc0a6d19c05bedb1d733de4bcfd4dc753befdacb
parent 8e328c14d3e88ef6c6b00de00335968af3d32692
Author: luajitos <bbhbb2094@gmail.com>
Date:   Sun, 30 Nov 2025 04:28:40 +0000

Fixed cursor flickering

Diffstat:
Miso_includes/apps/com.luajitos.background/manifest.lua | 2+-
Miso_includes/apps/com.luajitos.calculator/manifest.lua | 2+-
Miso_includes/apps/com.luajitos.clitools/manifest.lua | 2+-
Miso_includes/apps/com.luajitos.crypto/manifest.lua | 2+-
Miso_includes/apps/com.luajitos.explorer/manifest.lua | 2+-
Miso_includes/apps/com.luajitos.installer/manifest.lua | 2+-
Miso_includes/apps/com.luajitos.lam/manifest.lua | 2+-
Miso_includes/apps/com.luajitos.lensviewer/manifest.lua | 2+-
Miso_includes/apps/com.luajitos.lunareditor/manifest.lua | 14+++++++++-----
Miso_includes/apps/com.luajitos.lunareditor/src/editor.lua | 808++++++++++++++++++++++++++++---------------------------------------------------
Miso_includes/apps/com.luajitos.moonbrowser/manifest.lua | 2+-
Aiso_includes/apps/com.luajitos.paint/icon.png | 0
Miso_includes/apps/com.luajitos.paint/manifest.lua | 2+-
Miso_includes/apps/com.luajitos.passprompt/manifest.lua | 2+-
Miso_includes/apps/com.luajitos.shell/manifest.lua | 2+-
Miso_includes/apps/com.luajitos.taskbar/manifest.lua | 2+-
Miso_includes/os/init.lua | 536++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Miso_includes/os/libs/Application.lua | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Miso_includes/os/libs/LAM.lua | 5+----
Miso_includes/os/libs/Run.lua | 2+-
Miso_includes/os/libs/Sys.lua | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Aiso_includes/os/public/res/cursor_template.png | 0
Miso_includes/scripts/test.lua | 4++--
Mkernel.c | 67++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mvesa.c | 421+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
25 files changed, 1400 insertions(+), 622 deletions(-)

diff --git a/iso_includes/apps/com.luajitos.background/manifest.lua b/iso_includes/apps/com.luajitos.background/manifest.lua @@ -5,7 +5,7 @@ return { category = "all", description = "LuajitOS Desktop Background"; entry = "background.lua"; - mode = "gui"; + type = "gui"; hidden = true; -- Hide from start menu permissions = { "ramdisk"; -- Need direct ramdisk access since SafeFS traverse is broken diff --git a/iso_includes/apps/com.luajitos.calculator/manifest.lua b/iso_includes/apps/com.luajitos.calculator/manifest.lua @@ -6,7 +6,7 @@ return { category = "Productivity", description = "Simple calculator application", entry = "init.lua", - mode = "gui", + type = "gui", permissions = { "filesystem", "scheduling", diff --git a/iso_includes/apps/com.luajitos.clitools/manifest.lua b/iso_includes/apps/com.luajitos.clitools/manifest.lua @@ -6,7 +6,7 @@ return { category = "System", description = "Command-line file system tools", entry = "init.lua", - mode = "cli", + type = "cli", permissions = { "ramdisk", "export", diff --git a/iso_includes/apps/com.luajitos.crypto/manifest.lua b/iso_includes/apps/com.luajitos.crypto/manifest.lua @@ -5,7 +5,7 @@ return { category = "all", description = "Command-line cryptographic utility for hashing, encryption, signing, and key derivation", entry = "init.lua", - mode = "cli", + type = "cli", permissions = { "stdio", -- Command-line output "export" -- Export crypto library for other apps diff --git a/iso_includes/apps/com.luajitos.explorer/manifest.lua b/iso_includes/apps/com.luajitos.explorer/manifest.lua @@ -5,7 +5,7 @@ return { category = "all", description = "File browser and explorer", entry = "explorer.lua", - mode = "gui", + type = "gui", permissions = { "filesystem", "draw", diff --git a/iso_includes/apps/com.luajitos.installer/manifest.lua b/iso_includes/apps/com.luajitos.installer/manifest.lua @@ -5,7 +5,7 @@ return { category = "system", description = "LuajitOS System Installer", entry = "init.lua", - mode = "gui", + type = "gui", permissions = { "global-environment", "draw", diff --git a/iso_includes/apps/com.luajitos.lam/manifest.lua b/iso_includes/apps/com.luajitos.lam/manifest.lua @@ -5,7 +5,7 @@ return { category = "system"; description = "LuajitOS Application Manager - Package and install applications"; entry = "init.lua"; - mode = "cli"; + type = "cli"; permissions = { "filesystem"; "system"; diff --git a/iso_includes/apps/com.luajitos.lensviewer/manifest.lua b/iso_includes/apps/com.luajitos.lensviewer/manifest.lua @@ -5,7 +5,7 @@ return { category = "all", description = "Image viewer application", entry = "lens.lua", - mode = "gui", + type = "gui", permissions = { "ramdisk", -- Direct ramdisk access for reading images "draw", diff --git a/iso_includes/apps/com.luajitos.lunareditor/manifest.lua b/iso_includes/apps/com.luajitos.lunareditor/manifest.lua @@ -1,16 +1,20 @@ return { - name = "Lunar Editor", + name = "lunareditor", + pretty = "Lunar Editor", developer = "luajitos", version = "1.0.0", category = "Productivity", - description = "Simple text editor with word wrap and line numbers", + description = "Simple text editor", entry = "editor.lua", - mode = "gui", + type = "gui", permissions = { "draw", - "filesystem" + "filesystem", + "ramdisk" }, allowedPaths = { - "~/*" -- Access to user's home directory and subdirectories + "/home/*", + "/apps/*", + "/os/*" } } diff --git a/iso_includes/apps/com.luajitos.lunareditor/src/editor.lua b/iso_includes/apps/com.luajitos.lunareditor/src/editor.lua @@ -1,611 +1,375 @@ -- Lunar Editor - Simple Text Editor for LuajitOS --- Features: Word wrap, line numbers, black text on white background -osprint("[LUNAR] Starting Lunar Editor initialization...\n") +local WINDOW_WIDTH = 600 +local WINDOW_HEIGHT = 400 +local CHAR_WIDTH = 8 +local CHAR_HEIGHT = 16 +local PADDING = 4 +local LINE_NUMBER_WIDTH = 40 --- Colors -local COLOR_BG = 0xFFFFFF -- White background -local COLOR_TEXT = 0x000000 -- Black text -local COLOR_LINE_NUM = 0x808080 -- Grey line numbers -local COLOR_LINE_NUM_BG = 0xF0F0F0 -- Light grey line number background -local COLOR_CURSOR = 0x0000FF -- Blue cursor -local COLOR_STATUS_BG = 0xE0E0E0 -- Status bar background -local COLOR_STATUS_TEXT = 0x000000 -- Status bar text -local COLOR_MENU_BG = 0xF8F8F8 -- Menu bar background -local COLOR_MENU_TEXT = 0x000000 -- Menu bar text -local COLOR_MENU_HOVER = 0xE0E0E0 -- Menu item hover - --- Settings (defaults) -local settings = { - wordWrap = true, - lineNumbers = true, - fontSize = 8, -- Character width approximation - lineHeight = 12 -- Line height in pixels +-- Editor state +local state = { + lines = {""}, -- Array of text lines + cursorLine = 1, -- Current line (1-indexed) + cursorCol = 1, -- Current column (1-indexed) + scrollY = 0, -- Vertical scroll offset (lines) + scrollX = 0, -- Horizontal scroll offset (chars) + filename = nil, -- Current file path + modified = false, -- Has unsaved changes + selecting = false, -- Is selecting text + selStartLine = nil, + selStartCol = nil } --- Editor state -local lines = {""} -- Text content (array of lines) -local cursorLine = 1 -- Current line (1-indexed) -local cursorCol = 1 -- Current column (1-indexed) -local scrollLine = 1 -- First visible line -local scrollCol = 1 -- Horizontal scroll for non-wrapped mode -local modified = false -- Has the file been modified? -local currentFile = nil -- Current file path - --- Display state -local displayLines = {} -- Processed lines for display (with wrapping) -local cursorBlink = true -local lastBlinkTime = os.clock() - --- UI dimensions -local WINDOW_WIDTH = 800 -local WINDOW_HEIGHT = 600 -local LINE_NUM_WIDTH = 50 -- Width of line number area -local STATUS_HEIGHT = 20 -- Height of status bar -local MENU_HEIGHT = 25 -- Height of menu bar -local PADDING = 5 -- Padding around text - --- Menu bar buttons -local menuButtons = { - {label = "[New]", x = 5, width = 50, action = "new"}, - {label = "[Open]", x = 60, width = 60, action = "open"}, - {label = "[Save]", x = 125, width = 60, action = "save"} +-- Colors +local COLORS = { + background = 0x1E1E2E, -- Dark background + text = 0xCDD6F4, -- Light text + lineNumbers = 0x6C7086, -- Muted line numbers + lineNumberBg = 0x181825, -- Darker bg for line numbers + cursor = 0xF5E0DC, -- Cursor color + cursorLine = 0x313244, -- Current line highlight + selection = 0x45475A, -- Selection background + statusBar = 0x313244, -- Status bar background + statusText = 0xBAC2DE -- Status bar text } --- Calculate usable text area -local function getTextAreaWidth() - local width = WINDOW_WIDTH - if settings.lineNumbers then - width = width - LINE_NUM_WIDTH - end - return width - (PADDING * 2) -end +-- Create main window +local window = app:newWindow("Lunar Editor", WINDOW_WIDTH, WINDOW_HEIGHT, true) -local function getTextAreaHeight() - return WINDOW_HEIGHT - STATUS_HEIGHT - MENU_HEIGHT - (PADDING * 2) +-- Calculate visible area +local function getVisibleLines() + local contentHeight = window.height - CHAR_HEIGHT - PADDING * 2 -- Subtract status bar + return math.floor(contentHeight / CHAR_HEIGHT) end -local function getVisibleLineCount() - return math.floor(getTextAreaHeight() / settings.lineHeight) +local function getVisibleCols() + local contentWidth = window.width - LINE_NUMBER_WIDTH - PADDING * 2 + return math.floor(contentWidth / CHAR_WIDTH) end --- Word wrap: split a line into multiple display lines -local function wrapLine(text, maxWidth) - if not settings.wordWrap then - return {text} - end - - if #text == 0 then - return {""} - end - - local wrapped = {} - local charsPerLine = math.floor(maxWidth / settings.fontSize) +-- Ensure cursor is visible +local function ensureCursorVisible() + local visibleLines = getVisibleLines() + local visibleCols = getVisibleCols() - if charsPerLine < 1 then - charsPerLine = 1 + -- Vertical scroll + if state.cursorLine <= state.scrollY then + state.scrollY = state.cursorLine - 1 + elseif state.cursorLine > state.scrollY + visibleLines then + state.scrollY = state.cursorLine - visibleLines end - local pos = 1 - while pos <= #text do - local segment = text:sub(pos, pos + charsPerLine - 1) - - -- Try to break at word boundary if not at end - if pos + charsPerLine <= #text then - local lastSpace = segment:match(".*() ") or segment:match(".*()\t") - if lastSpace and lastSpace > 1 then - segment = text:sub(pos, pos + lastSpace - 2) - pos = pos + lastSpace - 1 - else - pos = pos + charsPerLine - end - else - pos = pos + charsPerLine - end - - table.insert(wrapped, segment) - - -- Safety check to prevent infinite loop - if pos <= #text and #segment == 0 then - pos = pos + 1 -- Force advance if no progress made - end + -- Horizontal scroll + if state.cursorCol <= state.scrollX then + state.scrollX = state.cursorCol - 1 + elseif state.cursorCol > state.scrollX + visibleCols then + state.scrollX = state.cursorCol - visibleCols end - return wrapped + if state.scrollY < 0 then state.scrollY = 0 end + if state.scrollX < 0 then state.scrollX = 0 end end --- Rebuild display lines from source lines -local function rebuildDisplayLines() - displayLines = {} - local textWidth = getTextAreaWidth() - - for lineNum, lineText in ipairs(lines) do - local wrapped = wrapLine(lineText, textWidth) - for _, segment in ipairs(wrapped) do - table.insert(displayLines, { - sourceLineNum = lineNum, - text = segment - }) - end - end +-- Get current line text +local function getCurrentLine() + return state.lines[state.cursorLine] or "" +end - -- Ensure at least one display line - if #displayLines == 0 then - table.insert(displayLines, { - sourceLineNum = 1, - text = "" - }) - end +-- Set current line text +local function setCurrentLine(text) + state.lines[state.cursorLine] = text + state.modified = true end --- Convert cursor position to display line -local function cursorToDisplayLine() - local charCount = 0 - local textWidth = getTextAreaWidth() - local charsPerLine = math.floor(textWidth / settings.fontSize) - if charsPerLine < 1 then charsPerLine = 1 end +-- Insert character at cursor +local function insertChar(char) + local line = getCurrentLine() + local before = string.sub(line, 1, state.cursorCol - 1) + local after = string.sub(line, state.cursorCol) + setCurrentLine(before .. char .. after) + state.cursorCol = state.cursorCol + 1 +end - -- Count characters up to cursor position - for i = 1, cursorLine - 1 do - charCount = charCount + #lines[i] + 1 -- +1 for newline +-- Delete character before cursor (backspace) +local function deleteCharBefore() + if state.cursorCol > 1 then + local line = getCurrentLine() + local before = string.sub(line, 1, state.cursorCol - 2) + local after = string.sub(line, state.cursorCol) + setCurrentLine(before .. after) + state.cursorCol = state.cursorCol - 1 + elseif state.cursorLine > 1 then + -- Join with previous line + local currentLine = getCurrentLine() + local prevLine = state.lines[state.cursorLine - 1] + state.cursorCol = #prevLine + 1 + state.lines[state.cursorLine - 1] = prevLine .. currentLine + table.remove(state.lines, state.cursorLine) + state.cursorLine = state.cursorLine - 1 + state.modified = true end - charCount = charCount + cursorCol - 1 - - -- Find display line - local currentCount = 0 - for i, dLine in ipairs(displayLines) do - local lineLen = #dLine.text - if settings.wordWrap and i < #displayLines and - displayLines[i + 1].sourceLineNum == dLine.sourceLineNum then - -- Not the last segment of this source line - currentCount = currentCount + lineLen - else - -- Last segment or no wrap - currentCount = currentCount + lineLen + 1 -- +1 for newline - end +end - if currentCount >= charCount then - return i - end +-- Delete character at cursor (delete key) +local function deleteCharAt() + local line = getCurrentLine() + if state.cursorCol <= #line then + local before = string.sub(line, 1, state.cursorCol - 1) + local after = string.sub(line, state.cursorCol + 1) + setCurrentLine(before .. after) + elseif state.cursorLine < #state.lines then + -- Join with next line + state.lines[state.cursorLine] = line .. state.lines[state.cursorLine + 1] + table.remove(state.lines, state.cursorLine + 1) + state.modified = true end - - return #displayLines end --- Insert character at cursor -local function insertChar(char) - local line = lines[cursorLine] - lines[cursorLine] = line:sub(1, cursorCol - 1) .. char .. line:sub(cursorCol) - cursorCol = cursorCol + 1 - modified = true - rebuildDisplayLines() +-- Insert new line (enter key) +local function insertNewLine() + local line = getCurrentLine() + local before = string.sub(line, 1, state.cursorCol - 1) + local after = string.sub(line, state.cursorCol) + setCurrentLine(before) + table.insert(state.lines, state.cursorLine + 1, after) + state.cursorLine = state.cursorLine + 1 + state.cursorCol = 1 + state.modified = true end --- Insert newline at cursor -local function insertNewline() - local line = lines[cursorLine] - local before = line:sub(1, cursorCol - 1) - local after = line:sub(cursorCol) +-- Move cursor +local function moveCursor(dLine, dCol) + state.cursorLine = state.cursorLine + dLine + state.cursorCol = state.cursorCol + dCol - lines[cursorLine] = before - table.insert(lines, cursorLine + 1, after) + -- Clamp line + if state.cursorLine < 1 then state.cursorLine = 1 end + if state.cursorLine > #state.lines then state.cursorLine = #state.lines end - cursorLine = cursorLine + 1 - cursorCol = 1 - modified = true - rebuildDisplayLines() -end + -- Clamp column + local lineLen = #getCurrentLine() + if state.cursorCol < 1 then state.cursorCol = 1 end + if state.cursorCol > lineLen + 1 then state.cursorCol = lineLen + 1 end --- Delete character before cursor (backspace) -local function deleteChar() - if cursorCol > 1 then - -- Delete within line - local line = lines[cursorLine] - lines[cursorLine] = line:sub(1, cursorCol - 2) .. line:sub(cursorCol) - cursorCol = cursorCol - 1 - modified = true - rebuildDisplayLines() - elseif cursorLine > 1 then - -- Merge with previous line - local currentLine = lines[cursorLine] - table.remove(lines, cursorLine) - cursorLine = cursorLine - 1 - cursorCol = #lines[cursorLine] + 1 - lines[cursorLine] = lines[cursorLine] .. currentLine - modified = true - rebuildDisplayLines() - end + ensureCursorVisible() end --- Move cursor left -local function moveCursorLeft() - if cursorCol > 1 then - cursorCol = cursorCol - 1 - elseif cursorLine > 1 then - cursorLine = cursorLine - 1 - cursorCol = #lines[cursorLine] + 1 - end +-- Move to start/end of line +local function moveToLineStart() + state.cursorCol = 1 + ensureCursorVisible() end --- Move cursor right -local function moveCursorRight() - if cursorCol <= #lines[cursorLine] then - cursorCol = cursorCol + 1 - elseif cursorLine < #lines then - cursorLine = cursorLine + 1 - cursorCol = 1 - end +local function moveToLineEnd() + state.cursorCol = #getCurrentLine() + 1 + ensureCursorVisible() end --- Move cursor up -local function moveCursorUp() - if cursorLine > 1 then - cursorLine = cursorLine - 1 - if cursorCol > #lines[cursorLine] + 1 then - cursorCol = #lines[cursorLine] + 1 +-- Load file +local function loadFile(path) + local content = nil + if fs and fs.read then + content = fs:read(path) + elseif CRamdiskOpen then + local handle = CRamdiskOpen(path, "r") + if handle then + content = CRamdiskRead(handle) + CRamdiskClose(handle) end end -end --- Move cursor down -local function moveCursorDown() - if cursorLine < #lines then - cursorLine = cursorLine + 1 - if cursorCol > #lines[cursorLine] + 1 then - cursorCol = #lines[cursorLine] + 1 + if content then + state.lines = {} + for line in (content .. "\n"):gmatch("([^\n]*)\n") do + table.insert(state.lines, line) end - end -end - --- Save file -local function saveFile(filepath) - if not filepath then - osprint("ERROR: No filepath specified\n") - return false - end - - local content = table.concat(lines, "\n") - local success, err = fs:write(filepath, content) - - if success then - currentFile = filepath - modified = false - osprint("Saved: " .. filepath .. "\n") + if #state.lines == 0 then + state.lines = {""} + end + state.filename = path + state.modified = false + state.cursorLine = 1 + state.cursorCol = 1 + state.scrollX = 0 + state.scrollY = 0 + window:markDirty() return true - else - osprint("ERROR: Failed to save " .. filepath .. ": " .. tostring(err) .. "\n") - return false end + return false end --- Load file -local function loadFile(filepath) - local content, err = fs:read(filepath) - - if not content then - osprint("ERROR: Failed to load " .. filepath .. ": " .. tostring(err) .. "\n") +-- Save file +local function saveFile(path) + path = path or state.filename + if not path then + -- Show save dialog + Dialog.fileSave("/home", "untitled.txt"):onSuccess(function(filepath) + saveFile(filepath) + end):show() return false end - -- Split into lines - lines = {} - for line in (content .. "\n"):gmatch("([^\n]*)\n") do - table.insert(lines, line) - end + local content = table.concat(state.lines, "\n") + local success = false - -- Ensure at least one line - if #lines == 0 then - lines = {""} + if fs and fs.write then + success = fs:write(path, content) + elseif CRamdiskOpen then + local handle = CRamdiskOpen(path, "w") + if handle then + CRamdiskWrite(handle, content) + CRamdiskClose(handle) + success = true + end end - -- Remove last empty line if content ended with newline - if #lines > 1 and lines[#lines] == "" and content:sub(-1) == "\n" then - table.remove(lines) + if success then + state.filename = path + state.modified = false + window:markDirty() end - - currentFile = filepath - cursorLine = 1 - cursorCol = 1 - scrollLine = 1 - modified = false - rebuildDisplayLines() - - osprint("Loaded: " .. filepath .. " (" .. #lines .. " lines)\n") - return true + return success end --- Create window -osprint("[LUNAR] Creating window...\n") -local window = app:newWindow(WINDOW_WIDTH, WINDOW_HEIGHT) -osprint("[LUNAR] Window created\n") -window.title = "Lunar Editor" -window.resizable = false - --- Initial display setup -osprint("[LUNAR] Rebuilding display lines...\n") -rebuildDisplayLines() -osprint("[LUNAR] Display lines rebuilt\n") +-- Open file dialog +local function openFileDialog() + Dialog.fileOpen("/home"):onSuccess(function(filepath) + loadFile(filepath) + end):show() +end --- Draw function -osprint("[LUNAR] Setting up draw function...\n") +-- Draw callback window.onDraw = function(gfx) - -- White background - gfx:fillRect(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, COLOR_BG) + local w = gfx:getWidth() + local h = gfx:getHeight() - -- Draw menu bar - gfx:fillRect(0, 0, WINDOW_WIDTH, MENU_HEIGHT, COLOR_MENU_BG) - for _, btn in ipairs(menuButtons) do - gfx:drawText(btn.x, 7, btn.label, COLOR_MENU_TEXT) - end - - -- Draw menu bar bottom border - gfx:fillRect(0, MENU_HEIGHT, WINDOW_WIDTH, 1, 0xCCCCCC) - - -- Calculate dimensions - local textAreaX = PADDING - if settings.lineNumbers then - textAreaX = textAreaX + LINE_NUM_WIDTH - end + -- Background + gfx:fillRect(0, 0, w, h, COLORS.background) - local visibleLines = getVisibleLineCount() - local endLine = math.min(scrollLine + visibleLines - 1, #displayLines) + -- Line number background + gfx:fillRect(0, 0, LINE_NUMBER_WIDTH, h - CHAR_HEIGHT, COLORS.lineNumberBg) - -- Draw line numbers background - if settings.lineNumbers then - gfx:fillRect(0, MENU_HEIGHT, LINE_NUM_WIDTH, WINDOW_HEIGHT - STATUS_HEIGHT - MENU_HEIGHT, COLOR_LINE_NUM_BG) - end - - -- Draw text and line numbers - local y = MENU_HEIGHT + PADDING - local lastSourceLine = -1 + -- Calculate visible range + local visibleLines = getVisibleLines() + local startLine = state.scrollY + 1 + local endLine = math.min(startLine + visibleLines - 1, #state.lines) - for i = scrollLine, endLine do - local dLine = displayLines[i] - - -- Draw line number (only for first segment of each source line) - if settings.lineNumbers and dLine.sourceLineNum ~= lastSourceLine then - local lineNumText = tostring(dLine.sourceLineNum) - gfx:drawText(PADDING, y, lineNumText, COLOR_LINE_NUM) - lastSourceLine = dLine.sourceLineNum - end + -- Draw lines + local y = PADDING + for i = startLine, endLine do + local line = state.lines[i] or "" + local lineY = y + (i - startLine) * CHAR_HEIGHT - -- Draw text - if dLine.text and #dLine.text > 0 then - gfx:drawText(textAreaX, y, dLine.text, COLOR_TEXT) + -- Current line highlight + if i == state.cursorLine then + gfx:fillRect(LINE_NUMBER_WIDTH, lineY, w - LINE_NUMBER_WIDTH, CHAR_HEIGHT, COLORS.cursorLine) end - -- Draw cursor if on this line - if dLine.sourceLineNum == cursorLine and cursorBlink then - -- Calculate cursor position within display line - local line = lines[cursorLine] - local beforeCursor = line:sub(1, cursorCol - 1) + -- Line number + local lineNum = tostring(i) + local numX = LINE_NUMBER_WIDTH - #lineNum * CHAR_WIDTH - 4 + gfx:drawText(numX, lineY, lineNum, COLORS.lineNumbers) - -- Check if cursor is in this display segment - local textWidth = getTextAreaWidth() - local charsPerLine = math.floor(textWidth / settings.fontSize) - if charsPerLine < 1 then charsPerLine = 1 end + -- Line text (with horizontal scroll) + local visibleText = string.sub(line, state.scrollX + 1) + gfx:drawText(LINE_NUMBER_WIDTH + PADDING, lineY, visibleText, COLORS.text) - -- Simple cursor rendering at cursor position - local cursorX = textAreaX + ((cursorCol - 1) * settings.fontSize) - - -- Draw cursor line - gfx:fillRect(cursorX, y, 2, settings.lineHeight, COLOR_CURSOR) + -- Cursor (on current line) + if i == state.cursorLine then + local cursorX = LINE_NUMBER_WIDTH + PADDING + (state.cursorCol - state.scrollX - 1) * CHAR_WIDTH + if cursorX >= LINE_NUMBER_WIDTH then + gfx:fillRect(cursorX, lineY, 2, CHAR_HEIGHT, COLORS.cursor) + end end - - y = y + settings.lineHeight end - -- Draw status bar - local statusY = WINDOW_HEIGHT - STATUS_HEIGHT - gfx:fillRect(0, statusY, WINDOW_WIDTH, STATUS_HEIGHT, COLOR_STATUS_BG) - - local statusText = "" - if currentFile then - statusText = currentFile - else - statusText = "[Untitled]" - end - - if modified then - statusText = statusText .. " [Modified]" - end - - statusText = statusText .. " | Line " .. cursorLine .. ", Col " .. cursorCol - statusText = statusText .. " | Lines: " .. #lines - - if settings.wordWrap then - statusText = statusText .. " | Wrap: ON" - else - statusText = statusText .. " | Wrap: OFF" - end + -- Status bar + local statusY = h - CHAR_HEIGHT + gfx:fillRect(0, statusY, w, CHAR_HEIGHT, COLORS.statusBar) - if settings.lineNumbers then - statusText = statusText .. " | LineNum: ON" - else - statusText = statusText .. " | LineNum: OFF" - end + -- Status text + local filename = state.filename or "[New File]" + if state.modified then filename = filename .. " *" end + local status = string.format(" %s | Ln %d, Col %d | %d lines", + filename, state.cursorLine, state.cursorCol, #state.lines) + gfx:drawText(4, statusY + 2, status, COLORS.statusText) - gfx:drawText(PADDING, statusY + 5, statusText, COLOR_STATUS_TEXT) + -- Help hint on right side + local help = "Ctrl+O: Open Ctrl+S: Save " + local helpX = w - #help * CHAR_WIDTH - 4 + gfx:drawText(helpX, statusY + 2, help, COLORS.lineNumbers) end --- Keyboard input handler -window.inputCallback = function(key, scancode) - -- Only process key down events - if scancode >= 128 then - return - end - - local baseScancode = scancode % 128 - - -- Handle special keys - if baseScancode == 28 then - -- Enter - insertNewline() - window:markDirty() - return - end - - if baseScancode == 14 then - -- Backspace - deleteChar() - window:markDirty() - return - end - - if baseScancode == 203 then - -- Left arrow - moveCursorLeft() - window:markDirty() - return - end - - if baseScancode == 205 then - -- Right arrow - moveCursorRight() - window:markDirty() - return - end - - if baseScancode == 200 then - -- Up arrow - moveCursorUp() - window:markDirty() - return - end - - if baseScancode == 208 then - -- Down arrow - moveCursorDown() - window:markDirty() - return - end - - -- Tab key (scancode 15) - if baseScancode == 15 then - insertChar(" ") -- Insert 4 spaces - window:markDirty() - return - end - - -- Handle printable characters - if key and type(key) == "string" and #key == 1 then +-- Input handler +window:onInput(function(key, scancode) + local ctrl = false + -- Check for Ctrl modifier (scancode 0x1D is left ctrl) + -- For now we'll use simple key detection + + if scancode == 0x48 then -- Up arrow + moveCursor(-1, 0) + elseif scancode == 0x50 then -- Down arrow + moveCursor(1, 0) + elseif scancode == 0x4B then -- Left arrow + moveCursor(0, -1) + elseif scancode == 0x4D then -- Right arrow + moveCursor(0, 1) + elseif scancode == 0x47 then -- Home + moveToLineStart() + elseif scancode == 0x4F then -- End + moveToLineEnd() + elseif scancode == 0x49 then -- Page Up + moveCursor(-getVisibleLines(), 0) + elseif scancode == 0x51 then -- Page Down + moveCursor(getVisibleLines(), 0) + elseif scancode == 0x0E then -- Backspace + deleteCharBefore() + elseif scancode == 0x53 then -- Delete + deleteCharAt() + elseif scancode == 0x1C then -- Enter + insertNewLine() + elseif scancode == 0x0F then -- Tab + insertChar(" ") -- 4 spaces + state.cursorCol = state.cursorCol + 3 -- We already added 1 in insertChar + elseif key and #key == 1 then + -- Regular character local byte = string.byte(key) - if byte >= 32 and byte <= 126 then + if byte >= 32 and byte < 127 then insertChar(key) - window:markDirty() end end -end - --- Focus handler -window.onFocus = function() - if sys and sys.setActiveWindow then - sys.setActiveWindow(window) - end -end --- Menu action handlers -local function handleNew() - lines = {""} - cursorLine = 1 - cursorCol = 1 - scrollLine = 1 - modified = false - currentFile = nil - rebuildDisplayLines() + ensureCursorVisible() window:markDirty() - osprint("New file created\n") -end - -local function handleOpen() - local fopen = Dialog.fileOpen("/home/", {app = app, fs = fs}) - fopen:openDialog(function(filepath) - if filepath then - loadFile(filepath) - window:markDirty() - else - osprint("Open cancelled\n") - end - end) -end - -local function handleSave() - local defaultName = "untitled.txt" - if currentFile then - defaultName = currentFile:match("([^/]+)$") or "untitled.txt" - end - - local fsave = Dialog.fileSave("/home/", defaultName, {app = app, fs = fs}) - fsave:openDialog(function(filepath) - if filepath then - saveFile(filepath) - window:markDirty() - else - osprint("Save cancelled\n") - end - end) -end - --- Click handler -window.onClick = function(mx, my) - -- Check if click is in menu bar - if my < MENU_HEIGHT then - for _, btn in ipairs(menuButtons) do - if mx >= btn.x and mx < btn.x + btn.width then - osprint("Menu clicked: " .. btn.action .. "\n") - if btn.action == "new" then - handleNew() - elseif btn.action == "open" then - handleOpen() - elseif btn.action == "save" then - handleSave() - end - return - end - end - end -end - --- Cursor blink update -local function updateCursor() - local now = os.clock() - if now - lastBlinkTime > 0.5 then - cursorBlink = not cursorBlink - lastBlinkTime = now - if window and window.visible then +end) + +-- Click handler for cursor positioning +window.onClick = function(x, y, button) + if y < window.height - CHAR_HEIGHT and x > LINE_NUMBER_WIDTH then + -- Calculate clicked line and column + local clickedLine = math.floor((y - PADDING) / CHAR_HEIGHT) + state.scrollY + 1 + local clickedCol = math.floor((x - LINE_NUMBER_WIDTH - PADDING) / CHAR_WIDTH) + state.scrollX + 1 + + -- Clamp to valid range + if clickedLine >= 1 and clickedLine <= #state.lines then + state.cursorLine = clickedLine + local lineLen = #state.lines[clickedLine] + if clickedCol < 1 then clickedCol = 1 end + if clickedCol > lineLen + 1 then clickedCol = lineLen + 1 end + state.cursorCol = clickedCol window:markDirty() end end end --- Auto-scroll to keep cursor visible -local function ensureCursorVisible() - local displayLine = cursorToDisplayLine() - local visibleLines = getVisibleLineCount() +-- Handle keyboard shortcuts via global input +-- Note: In a full implementation, we'd have modifier key tracking - if displayLine < scrollLine then - scrollLine = displayLine - window:markDirty() - elseif displayLine >= scrollLine + visibleLines then - scrollLine = displayLine - visibleLines + 1 - window:markDirty() - end -end - --- Set window as active on open -window.onOpen = function() - if sys and sys.setActiveWindow then - sys.setActiveWindow(window) - end -end - --- Main update loop (would integrate with OS event loop) -osprint("[LUNAR] Initialization complete!\n") -osprint("Lunar Editor initialized\n") -osprint("Features: Word wrap (ON), Line numbers (ON)\n") -osprint("Controls: Type to edit, Arrows to navigate, Enter for newline, Backspace to delete\n") - --- Test: Load a file if specified in args +-- Check for file argument if args and args[1] then loadFile(args[1]) end + +-- Initial draw +window:markDirty() diff --git a/iso_includes/apps/com.luajitos.moonbrowser/manifest.lua b/iso_includes/apps/com.luajitos.moonbrowser/manifest.lua @@ -5,7 +5,7 @@ return { category = "all", description = "LuajitOS HTML Browser"; entry = "browser.lua"; - mode = "gui"; + type = "gui"; permissions = { "filesystem"; "draw"; diff --git a/iso_includes/apps/com.luajitos.paint/icon.png b/iso_includes/apps/com.luajitos.paint/icon.png Binary files differ. diff --git a/iso_includes/apps/com.luajitos.paint/manifest.lua b/iso_includes/apps/com.luajitos.paint/manifest.lua @@ -6,7 +6,7 @@ return { category = "Graphics", description = "Simple paint application", entry = "init.lua", - mode = "gui", + type = "gui", permissions = { "filesystem", "scheduling", diff --git a/iso_includes/apps/com.luajitos.passprompt/manifest.lua b/iso_includes/apps/com.luajitos.passprompt/manifest.lua @@ -5,7 +5,7 @@ return { category = "system", description = "Administrative password prompt for permission requests", entry = "init.lua", - mode = "gui", + type = "gui", hidden = true, -- Hide from start menu permissions = { "draw", diff --git a/iso_includes/apps/com.luajitos.shell/manifest.lua b/iso_includes/apps/com.luajitos.shell/manifest.lua @@ -6,7 +6,7 @@ return { category = "System", description = "LuajitOS Shell Terminal", entry = "shell.lua", - mode = "cli", + type = "cli", permissions = { "filesystem", "scheduling", diff --git a/iso_includes/apps/com.luajitos.taskbar/manifest.lua b/iso_includes/apps/com.luajitos.taskbar/manifest.lua @@ -5,7 +5,7 @@ return { category = "all", description = "System taskbar with start menu and running applications", entry = "init.lua", - mode = "gui", + type = "gui", hidden = true, -- Hide from start menu permissions = { "scheduling", diff --git a/iso_includes/os/init.lua b/iso_includes/os/init.lua @@ -18,7 +18,388 @@ local OP_IMAGE = 11 _G.cursor_state = { x = 512, y = 384, - visible = true + visible = true, + mode = "cursor" -- "cursor", "left-click", "right-click", "hover-grab", "grab", "loading", "denied" +} + +-- Cursor system constants +local CURSOR_SIZE = 30 +local CURSOR_HOTSPOT_X = 5 +local CURSOR_HOTSPOT_Y = 5 +local CURSOR_TRANSPARENT_R = 0 +local CURSOR_TRANSPARENT_G = 255 +local CURSOR_TRANSPARENT_B = 0 + +-- Cursor drawing stack (top of stack is active) +_G.cursor_stack = _G.cursor_stack or {} + +-- Cursor buffer (30x30 pixels, each pixel is {r, g, b}) +_G.cursor_buffer = _G.cursor_buffer or {} +_G.cursor_buffer_dirty = true +_G.cursor_last_mode = nil + +-- Initialize cursor buffer with transparent pixels +local function initCursorBuffer() + _G.cursor_buffer = {} + for y = 1, CURSOR_SIZE do + _G.cursor_buffer[y] = {} + for x = 1, CURSOR_SIZE do + _G.cursor_buffer[y][x] = {CURSOR_TRANSPARENT_R, CURSOR_TRANSPARENT_G, CURSOR_TRANSPARENT_B} + end + end +end +initCursorBuffer() + +-- SafeGfx for cursor drawing (draws to cursor_buffer) +local function createCursorGfx() + local gfx = {} + + function gfx:clear() + initCursorBuffer() + end + + function gfx:fillRect(x, y, w, h, color) + local r = bit.band(bit.rshift(color, 16), 0xFF) + local g = bit.band(bit.rshift(color, 8), 0xFF) + local b = bit.band(color, 0xFF) + for py = y, y + h - 1 do + for px = x, x + w - 1 do + if py >= 0 and py < CURSOR_SIZE and px >= 0 and px < CURSOR_SIZE then + _G.cursor_buffer[py + 1][px + 1] = {r, g, b} + end + end + end + end + + function gfx:drawRect(x, y, w, h, color) + local r = bit.band(bit.rshift(color, 16), 0xFF) + local g = bit.band(bit.rshift(color, 8), 0xFF) + local b = bit.band(color, 0xFF) + -- Top and bottom + for px = x, x + w - 1 do + if px >= 0 and px < CURSOR_SIZE then + if y >= 0 and y < CURSOR_SIZE then + _G.cursor_buffer[y + 1][px + 1] = {r, g, b} + end + if y + h - 1 >= 0 and y + h - 1 < CURSOR_SIZE then + _G.cursor_buffer[y + h][px + 1] = {r, g, b} + end + end + end + -- Left and right + for py = y, y + h - 1 do + if py >= 0 and py < CURSOR_SIZE then + if x >= 0 and x < CURSOR_SIZE then + _G.cursor_buffer[py + 1][x + 1] = {r, g, b} + end + if x + w - 1 >= 0 and x + w - 1 < CURSOR_SIZE then + _G.cursor_buffer[py + 1][x + w] = {r, g, b} + end + end + end + end + + function gfx:drawPixel(x, y, color) + if x >= 0 and x < CURSOR_SIZE and y >= 0 and y < CURSOR_SIZE then + local r = bit.band(bit.rshift(color, 16), 0xFF) + local g = bit.band(bit.rshift(color, 8), 0xFF) + local b = bit.band(color, 0xFF) + _G.cursor_buffer[y + 1][x + 1] = {r, g, b} + end + end + + function gfx:drawLine(x1, y1, x2, y2, color) + local r = bit.band(bit.rshift(color, 16), 0xFF) + local g = bit.band(bit.rshift(color, 8), 0xFF) + local b = bit.band(color, 0xFF) + -- Bresenham's line algorithm + local dx = math.abs(x2 - x1) + local dy = math.abs(y2 - y1) + local sx = x1 < x2 and 1 or -1 + local sy = y1 < y2 and 1 or -1 + local err = dx - dy + while true do + if x1 >= 0 and x1 < CURSOR_SIZE and y1 >= 0 and y1 < CURSOR_SIZE then + _G.cursor_buffer[y1 + 1][x1 + 1] = {r, g, b} + end + if x1 == x2 and y1 == y2 then break end + local e2 = 2 * err + if e2 > -dy then err = err - dy; x1 = x1 + sx end + if e2 < dx then err = err + dx; y1 = y1 + sy end + end + end + + function gfx:getWidth() return CURSOR_SIZE end + function gfx:getHeight() return CURSOR_SIZE end + + return gfx +end + +-- Default cursor drawing function +local function defaultCursorDraw(state, gfx) + gfx:clear() + + -- Default arrow cursor bitmap (1 = black outline, 2 = white fill) + local arrow_cursor = { + {1,0,0,0,0,0,0,0,0,0,0,0}, + {1,1,0,0,0,0,0,0,0,0,0,0}, + {1,2,1,0,0,0,0,0,0,0,0,0}, + {1,2,2,1,0,0,0,0,0,0,0,0}, + {1,2,2,2,1,0,0,0,0,0,0,0}, + {1,2,2,2,2,1,0,0,0,0,0,0}, + {1,2,2,2,2,2,1,0,0,0,0,0}, + {1,2,2,2,2,2,2,1,0,0,0,0}, + {1,2,2,2,2,2,2,2,1,0,0,0}, + {1,2,2,2,2,2,2,2,2,1,0,0}, + {1,2,2,2,2,2,1,1,1,1,0,0}, + {1,2,2,1,2,2,1,0,0,0,0,0}, + {1,2,1,0,1,2,2,1,0,0,0,0}, + {1,1,0,0,1,2,2,1,0,0,0,0}, + {1,0,0,0,0,1,2,2,1,0,0,0}, + {0,0,0,0,0,1,2,2,1,0,0,0}, + {0,0,0,0,0,0,1,2,1,0,0,0}, + {0,0,0,0,0,0,1,1,0,0,0,0}, + } + + -- Open hand cursor with pointing finger and thumb (for hover-grab) + -- Hotspot at fingertip + local open_hand_cursor = { + {0,0,0,0,1,1,0,0,0,0,0,0,0,0}, + {0,0,0,1,2,2,1,0,0,0,0,0,0,0}, + {0,0,0,1,2,2,1,0,0,0,0,0,0,0}, + {0,0,0,1,2,2,1,0,0,0,0,0,0,0}, + {0,0,0,1,2,2,1,0,0,0,0,0,0,0}, + {0,0,0,1,2,2,1,0,0,0,0,0,0,0}, + {0,0,0,1,2,2,1,1,1,0,1,1,0,0}, + {0,0,0,1,2,2,1,2,2,1,2,2,1,0}, + {0,0,0,1,2,2,2,2,2,1,2,2,1,0}, + {0,0,1,1,2,2,2,2,2,2,2,2,1,0}, + {0,1,2,2,1,2,2,2,2,2,2,2,1,0}, + {1,2,2,2,1,2,2,2,2,2,2,2,1,0}, + {1,2,2,2,2,2,2,2,2,2,2,1,0,0}, + {0,1,2,2,2,2,2,2,2,2,2,1,0,0}, + {0,0,1,2,2,2,2,2,2,2,1,0,0,0}, + {0,0,0,1,2,2,2,2,2,2,1,0,0,0}, + {0,0,0,0,1,2,2,2,2,1,0,0,0,0}, + {0,0,0,0,0,1,1,1,1,0,0,0,0,0}, + } + + -- Closed fist cursor (for grab/dragging) + -- Hotspot at left knuckle + local fist_cursor = { + {0,0,1,1,1,1,1,1,1,1,0,0}, + {0,1,2,2,1,2,2,1,2,2,1,0}, + {0,1,2,2,1,2,2,1,2,2,1,0}, + {0,1,2,2,1,2,2,1,2,2,1,0}, + {1,1,2,2,2,2,2,2,2,2,1,0}, + {1,2,2,2,2,2,2,2,2,2,1,0}, + {1,2,2,2,2,2,2,2,2,2,1,0}, + {0,1,2,2,2,2,2,2,2,2,1,0}, + {0,1,2,2,2,2,2,2,2,1,0,0}, + {0,0,1,2,2,2,2,2,2,1,0,0}, + {0,0,1,2,2,2,2,2,1,0,0,0}, + {0,0,0,1,2,2,2,2,1,0,0,0}, + {0,0,0,0,1,1,1,1,0,0,0,0}, + } + + local cursor_bitmap + local ox, oy + + if state == "grab" then + cursor_bitmap = fist_cursor + -- Hotspot at left knuckle (top-left of fist) + ox = 4 -- Offset so left knuckle is at hotspot + oy = 5 + elseif state == "hover-grab" then + cursor_bitmap = open_hand_cursor + -- Hotspot at fingertip (top of finger) + ox = 2 + oy = 5 + else + cursor_bitmap = arrow_cursor + ox = CURSOR_HOTSPOT_X + oy = CURSOR_HOTSPOT_Y + end + + -- Determine fill color based on state + local fill_color = 0xFFFFFF -- White default + if state == "left-click" or state == "right-click" then + fill_color = 0xC0C0C0 -- Light gray when clicking + elseif state == "loading" then + fill_color = 0x00FFFF -- Cyan when loading + elseif state == "denied" then + fill_color = 0xFF0000 -- Red when denied + end + + local fill_r = bit.band(bit.rshift(fill_color, 16), 0xFF) + local fill_g = bit.band(bit.rshift(fill_color, 8), 0xFF) + local fill_b = bit.band(fill_color, 0xFF) + + -- Draw cursor at hotspot offset + for row = 1, #cursor_bitmap do + for col = 1, #cursor_bitmap[row] do + local pixel = cursor_bitmap[row][col] + local px, py = ox + col - 1, oy + row - 1 + if px >= 0 and px < CURSOR_SIZE and py >= 0 and py < CURSOR_SIZE then + if pixel == 1 then + _G.cursor_buffer[py + 1][px + 1] = {0, 0, 0} -- Black outline + elseif pixel == 2 then + _G.cursor_buffer[py + 1][px + 1] = {fill_r, fill_g, fill_b} -- Fill color + end + end + end + end +end + +-- Regenerate cursor buffer using top of stack (or default) +local function regenerateCursorBuffer(state) + initCursorBuffer() + local gfx = createCursorGfx() + + if #_G.cursor_stack > 0 then + -- Use top of stack cursor function + local cursor_func = _G.cursor_stack[#_G.cursor_stack] + local success, err = pcall(cursor_func, state, gfx) + if not success and osprint then + osprint("Cursor draw error: " .. tostring(err) .. "\n") + -- Fall back to default + defaultCursorDraw(state, gfx) + end + else + -- Use default cursor + defaultCursorDraw(state, gfx) + end + + _G.cursor_buffer_dirty = false + _G.cursor_last_mode = state +end + +-- Add a cursor drawing function to the stack +local function addCursor(draw_func) + if type(draw_func) ~= "function" then + error("addCursor requires a function") + end + table.insert(_G.cursor_stack, draw_func) + _G.cursor_buffer_dirty = true + return #_G.cursor_stack -- Return index for removal +end + +-- Remove a cursor drawing function from the stack +local function removeCursor(index) + if index and index >= 1 and index <= #_G.cursor_stack then + table.remove(_G.cursor_stack, index) + _G.cursor_buffer_dirty = true + return true + end + return false +end + +-- Pop the top cursor from the stack +local function popCursor() + if #_G.cursor_stack > 0 then + table.remove(_G.cursor_stack) + _G.cursor_buffer_dirty = true + return true + end + return false +end + +-- Set cursor mode +local function setCursorMode(mode) + local valid_modes = {cursor=true, ["left-click"]=true, ["right-click"]=true, ["hover-grab"]=true, grab=true, loading=true, denied=true} + if valid_modes[mode] then + if _G.cursor_state.mode ~= mode then + _G.cursor_state.mode = mode + _G.cursor_buffer_dirty = true + end + end +end + +-- Window cursor overrides (indexed by window reference) +_G.window_cursor_stack = _G.window_cursor_stack or {} + +-- Get the active cursor function (window override or global) +local function getActiveCursorFunc() + -- Check if there's an active window with cursor override + if _G.sys and _G.sys.activeWindow then + local win_stack = _G.window_cursor_stack[_G.sys.activeWindow] + if win_stack and #win_stack > 0 then + return win_stack[#win_stack] + end + end + -- Fall back to global cursor stack + if #_G.cursor_stack > 0 then + return _G.cursor_stack[#_G.cursor_stack] + end + return nil +end + +-- Regenerate cursor buffer using active cursor (window or global) +local function regenerateCursorBufferActive(state) + initCursorBuffer() + local gfx = createCursorGfx() + + local cursor_func = getActiveCursorFunc() + if cursor_func then + local success, err = pcall(cursor_func, state, gfx) + if not success and osprint then + osprint("Cursor draw error: " .. tostring(err) .. "\n") + defaultCursorDraw(state, gfx) + end + else + defaultCursorDraw(state, gfx) + end + + -- Cache the cursor image to C side for fast drawing + if VESACacheCursor and _G.cursor_buffer then + VESACacheCursor(_G.cursor_buffer, CURSOR_SIZE, CURSOR_SIZE) + end + + _G.cursor_buffer_dirty = false + _G.cursor_last_mode = state +end + +-- Export cursor functions to sys table (will be done after sys is created) +_G._cursor_api = { + add = addCursor, + remove = removeCursor, + pop = popCursor, + setMode = setCursorMode, + regenerate = regenerateCursorBufferActive, + -- Window cursor functions + windowAdd = function(window, draw_func) + if type(draw_func) ~= "function" then + error("cursor.add requires a function") + end + if not _G.window_cursor_stack[window] then + _G.window_cursor_stack[window] = {} + end + table.insert(_G.window_cursor_stack[window], draw_func) + _G.cursor_buffer_dirty = true + return #_G.window_cursor_stack[window] + end, + windowRemove = function(window, index) + local stack = _G.window_cursor_stack[window] + if stack and index and index >= 1 and index <= #stack then + table.remove(stack, index) + _G.cursor_buffer_dirty = true + return true + end + return false + end, + windowPop = function(window) + local stack = _G.window_cursor_stack[window] + if stack and #stack > 0 then + table.remove(stack) + _G.cursor_buffer_dirty = true + return true + end + return false + end, + windowClear = function(window) + _G.window_cursor_stack[window] = nil + _G.cursor_buffer_dirty = true + end } -- Load Hook Library @@ -1107,74 +1488,115 @@ function MainDraw() _G.last_mouse_y = _G.cursor_state.y _G.frame_counter = _G.frame_counter + 1 - -- Copy the off-screen buffer to the visible framebuffer FIRST - -- This happens during VBlank to eliminate tearing + -- Copy the off-screen buffer to the visible framebuffer + -- Skip the cursor region to prevent flickering if VESACopyBufferToFramebuffer then - VESACopyBufferToFramebuffer() + local hotspot_x = 5 + local hotspot_y = 5 + local cursor_draw_x = _G.cursor_state.x - hotspot_x + local cursor_draw_y = _G.cursor_state.y - hotspot_y + local cursor_w = 30 + local cursor_h = 30 + + -- Pass cursor region to skip during buffer copy + if _G.cursor_state.visible and _G.last_cursor_x then + VESACopyBufferToFramebuffer(cursor_draw_x, cursor_draw_y, cursor_w, cursor_h) + else + VESACopyBufferToFramebuffer() + end end -- Draw cursor AFTER copying buffer so it's never saved in the buffer - -- This prevents cursor trails - if _G.cursor_state.visible and VESADrawRect then - local mouse_x = _G.cursor_state.x - local mouse_y = _G.cursor_state.y - - -- Disable double buffering to work directly with framebuffer - if VESASetDoubleBufferMode then - VESASetDoubleBufferMode(0) - end + -- VESADrawCursor handles: restore old background (partial), save new, draw cursor + local mouse_x = _G.cursor_state.x + local mouse_y = _G.cursor_state.y + + -- Update cursor mode based on mouse state + local new_mode = "cursor" + if _G.dragging_window then + new_mode = "grab" + elseif _G.resizing_window then + new_mode = "grab" + elseif mouse1_down then + new_mode = "left-click" + elseif _G.mouse2_down then + new_mode = "right-click" + else + -- Check if hovering over a window title bar (hover-grab) + for i = #_G.window_stack, 1, -1 do + local window = _G.window_stack[i] + if window.visible and not window.isBackground and not window.isBorderless then + local TITLE_BAR_HEIGHT = window.TITLE_BAR_HEIGHT or 20 + local BORDER_WIDTH = window.BORDER_WIDTH or 2 - -- Cursor color changes when mouse button down - local cursor_r, cursor_g, cursor_b = 255, 255, 255 - if mouse1_down then - cursor_r, cursor_g, cursor_b = 128, 128, 128 - end + local title_x = window.x - BORDER_WIDTH + local title_y = window.y - BORDER_WIDTH - TITLE_BAR_HEIGHT + local title_w = window.width + (BORDER_WIDTH * 2) + local title_h = TITLE_BAR_HEIGHT - -- Draw arrow cursor matching reference image - local mx, my = mouse_x, mouse_y - - -- Cursor bitmap (1 = black outline, 2 = white fill, 0 = transparent) - local cursor = { - {1,0,0,0,0,0,0,0,0,0,0,0}, - {1,1,0,0,0,0,0,0,0,0,0,0}, - {1,2,1,0,0,0,0,0,0,0,0,0}, - {1,2,2,1,0,0,0,0,0,0,0,0}, - {1,2,2,2,1,0,0,0,0,0,0,0}, - {1,2,2,2,2,1,0,0,0,0,0,0}, - {1,2,2,2,2,2,1,0,0,0,0,0}, - {1,2,2,2,2,2,2,1,0,0,0,0}, - {1,2,2,2,2,2,2,2,1,0,0,0}, - {1,2,2,2,2,2,2,2,2,1,0,0}, - {1,2,2,2,2,2,1,1,1,1,0,0}, - {1,2,2,1,2,2,1,0,0,0,0,0}, - {1,2,1,0,1,2,2,1,0,0,0,0}, - {1,1,0,0,1,2,2,1,0,0,0,0}, - {1,0,0,0,0,1,2,2,1,0,0,0}, - {0,0,0,0,0,1,2,2,1,0,0,0}, - {0,0,0,0,0,0,1,2,1,0,0,0}, - {0,0,0,0,0,0,1,1,0,0,0,0}, - } - - -- Draw the cursor - for row = 1, #cursor do - for col = 1, #cursor[row] do - local pixel = cursor[row][col] - if pixel == 1 then - VESADrawRect(mx + col - 1, my + row - 1, 1, 1, 0, 0, 0) - elseif pixel == 2 then - VESADrawRect(mx + col - 1, my + row - 1, 1, 1, cursor_r, cursor_g, cursor_b) + -- Check if mouse is in title bar area + if mouse_x >= title_x and mouse_x < title_x + title_w and + mouse_y >= title_y and mouse_y < title_y + title_h then + new_mode = "hover-grab" + break + end + + -- Check if mouse is in window content area (stop checking lower windows) + if mouse_x >= window.x and mouse_x < window.x + window.width and + mouse_y >= window.y and mouse_y < window.y + window.height then + break end end end + end - -- Remember cursor position - _G.last_cursor_x = mouse_x - _G.last_cursor_y = mouse_y + -- Only update mode if it changed (and wasn't set programmatically to loading/denied) + if _G.cursor_state.mode ~= "loading" and _G.cursor_state.mode ~= "denied" then + if _G.cursor_state.mode ~= new_mode then + _G.cursor_state.mode = new_mode + _G.cursor_buffer_dirty = true + end + end + + -- Regenerate cursor buffer if dirty or mode changed + if _G.cursor_buffer_dirty or _G.cursor_last_mode ~= _G.cursor_state.mode then + if _G._cursor_api and _G._cursor_api.regenerate then + _G._cursor_api.regenerate(_G.cursor_state.mode) + end + end - -- Re-enable double buffering for next frame - if VESASetDoubleBufferMode then - VESASetDoubleBufferMode(1) + -- Draw cursor with flicker-free background save/restore + if _G.cursor_state.visible then + -- Calculate cursor draw position (hotspot at 5,5) + local hotspot_x = 5 + local hotspot_y = 5 + local new_x = mouse_x - hotspot_x + local new_y = mouse_y - hotspot_y + + -- Get old cursor position + local old_x = (_G.last_cursor_x or mouse_x) - hotspot_x + local old_y = (_G.last_cursor_y or mouse_y) - hotspot_y + + -- Use VESAMoveCursor which takes both old and new positions + -- and only restores the L-shaped region (old minus new) + if VESAMoveCursor and _G.last_cursor_x then + VESAMoveCursor(old_x, old_y, new_x, new_y) + elseif VESADrawCursor then + -- Initial draw (no old position) + VESADrawCursor(new_x, new_y) + elseif VESABlitCursor and _G.cursor_buffer then + -- Fallback to old method + VESABlitCursor(_G.cursor_buffer, new_x, new_y, 30, 30) end + + -- Remember cursor position for next frame + _G.last_cursor_x = mouse_x + _G.last_cursor_y = mouse_y + elseif not _G.cursor_state.visible and VESARestoreCursor then + -- Cursor is hidden, restore the background + VESARestoreCursor() + _G.last_cursor_x = nil + _G.last_cursor_y = nil end diff --git a/iso_includes/os/libs/Application.lua b/iso_includes/os/libs/Application.lua @@ -400,28 +400,45 @@ function Application:getProcessInfo() end --- Create a new window for this application --- @param x: X position on screen (optional, will center if not provided) --- @param y: Y position on screen (optional, will center if not provided) --- @param width: Window width --- @param height: Window height +-- Supported overloads: +-- @param x, y, width, height, resizable: Position and size +-- @param title, x, y, width, height, resizable: Title with position and size +-- @param title, width, height, resizable: Title with centered position +-- @param width, height, resizable: Centered position -- @return Window instance -function Application:newWindow(x, y, width, height, resizable) +function Application:newWindow(arg1, arg2, arg3, arg4, arg5, arg6) local title = nil - + local x, y, width, height, resizable + + -- Handle overload: newWindow(title, x, y, width, height, resizable) - title with position + if type(arg1) == "string" and type(arg2) == "number" and type(arg3) == "number" and type(arg4) == "number" and type(arg5) == "number" then + title = arg1 + x = arg2 + y = arg3 + width = arg4 + height = arg5 + resizable = arg6 -- Handle overload: newWindow(title, width, height, resizable) - center on screen with title - if type(x) == "string" and type(y) == "number" and type(width) == "number" then - title = x - resizable = height -- 4th param becomes resizable - height = width - width = y + elseif type(arg1) == "string" and type(arg2) == "number" and type(arg3) == "number" then + title = arg1 + width = arg2 + height = arg3 + resizable = arg4 -- Center on screen (assume 1024x768 for now, should get from gfx API) x = math.floor((1024 - width) / 2) y = math.floor((768 - height) / 2) + -- Handle overload: newWindow(x, y, width, height, resizable) - position and size + elseif type(arg1) == "number" and type(arg2) == "number" and type(arg3) == "number" and type(arg4) == "number" then + x = arg1 + y = arg2 + width = arg3 + height = arg4 + resizable = arg5 -- Handle overload: newWindow(width, height, resizable) - center on screen - elseif type(x) == "number" and type(y) == "number" and not width and not height then - resizable = width -- 3rd param becomes resizable - width = x - height = y + elseif type(arg1) == "number" and type(arg2) == "number" then + width = arg1 + height = arg2 + resizable = arg3 -- Center on screen (assume 1024x768 for now, should get from gfx API) x = math.floor((1024 - width) / 2) y = math.floor((768 - height) / 2) @@ -451,6 +468,7 @@ function Application:newWindow(x, y, width, height, resizable) resizable = resizable, -- Whether window can be resized visible = true, app = self, + appInstance = self, -- Reference for icon drawing in title bar drawCallback = nil, -- User-defined draw function inputCallback = nil, -- User-defined input handler buffer = nil, -- Per-window pixel buffer (allocated on first draw) @@ -466,6 +484,45 @@ function Application:newWindow(x, y, width, height, resizable) restoreHeight = nil } + -- Window cursor API (window.cursor.add, window.cursor.remove, etc.) + window.cursor = { + -- Add a cursor drawing function for this window (overrides global when window is active) + -- @param draw_func function(state, gfx) - Called with cursor state and SafeGfx + -- @return number Index for removal + add = function(draw_func) + if _G._cursor_api and _G._cursor_api.windowAdd then + return _G._cursor_api.windowAdd(window, draw_func) + end + error("Cursor API not available") + end, + + -- Remove a cursor drawing function from this window's stack + -- @param index number The index returned by add() + -- @return boolean True if removed + remove = function(index) + if _G._cursor_api and _G._cursor_api.windowRemove then + return _G._cursor_api.windowRemove(window, index) + end + return false + end, + + -- Pop the top cursor from this window's stack + -- @return boolean True if popped + pop = function() + if _G._cursor_api and _G._cursor_api.windowPop then + return _G._cursor_api.windowPop(window) + end + return false + end, + + -- Clear all cursors from this window's stack + clear = function() + if _G._cursor_api and _G._cursor_api.windowClear then + _G._cursor_api.windowClear(window) + end + end + } + -- Window draw method (will be called by LPM drawScreen) function window:draw(safeGfx) if self.onDraw and self.visible then diff --git a/iso_includes/os/libs/LAM.lua b/iso_includes/os/libs/LAM.lua @@ -1091,10 +1091,7 @@ function LAM.cli(args) print(" Developer: " .. (manifest.developer or "?")) print(" Description: " .. (manifest.description or "?")) print(" Entry: " .. (manifest.entry or "init.lua")) - print(" Mode: " .. (manifest.mode or "gui")) - if manifest.type then - print(" Type: " .. manifest.type) - end + print(" Type: " .. (manifest.type or "gui")) if manifest.permissions then print(" Permissions: " .. table.concat(manifest.permissions, ", ")) end diff --git a/iso_includes/os/libs/Run.lua b/iso_includes/os/libs/Run.lua @@ -979,7 +979,7 @@ function run.execute(app_name, fsRoot) end -- Check if app is in CLI mode and set up default draw handler - if manifest and manifest.mode == "cli" then + if manifest and manifest.type == "cli" then -- If app has draw permission, set up default draw method local has_draw = false diff --git a/iso_includes/os/libs/Sys.lua b/iso_includes/os/libs/Sys.lua @@ -1126,4 +1126,56 @@ sys.browser = { end, } +-- Cursor API (sys.cursor.add, sys.cursor.remove, etc.) +sys.cursor = { + -- Add a global cursor drawing function to the stack + -- @param draw_func function(state, gfx) - Called with cursor state and SafeGfx for 30x30 cursor buffer + -- state: "cursor", "left-click", "right-click", "grab", "loading", "denied" + -- gfx: SafeGfx with clear(), fillRect(), drawRect(), drawPixel(), drawLine(), getWidth(), getHeight() + -- Use color 0x00FF00 (green) for transparent pixels + -- @return number Index in the cursor stack for removal + add = function(draw_func) + if _G._cursor_api and _G._cursor_api.add then + return _G._cursor_api.add(draw_func) + end + error("Cursor API not available") + end, + + -- Remove a global cursor drawing function from stack + -- @param index number The index returned by add() + -- @return boolean True if removed successfully + remove = function(index) + if _G._cursor_api and _G._cursor_api.remove then + return _G._cursor_api.remove(index) + end + return false + end, + + -- Pop the top global cursor from the stack + -- @return boolean True if a cursor was popped + pop = function() + if _G._cursor_api and _G._cursor_api.pop then + return _G._cursor_api.pop() + end + return false + end, + + -- Set the current cursor mode (triggers redraw) + -- @param mode string One of: "cursor", "left-click", "right-click", "grab", "loading", "denied" + setMode = function(mode) + if _G._cursor_api and _G._cursor_api.setMode then + _G._cursor_api.setMode(mode) + end + end, + + -- Get the current cursor mode + -- @return string Current cursor mode + getMode = function() + if _G.cursor_state then + return _G.cursor_state.mode or "cursor" + end + return "cursor" + end +} + return sys diff --git a/iso_includes/os/public/res/cursor_template.png b/iso_includes/os/public/res/cursor_template.png Binary files differ. diff --git a/iso_includes/scripts/test.lua b/iso_includes/scripts/test.lua @@ -3,10 +3,10 @@ permissions = {"filesystem", "draw"} } ]] - +osprint("Test OSPrint") print("Hi from TEST PROGRAM!") for k,v in pairs(crypto) do - print(k, v) + print("crypto", k, v) end diff --git a/kernel.c b/kernel.c @@ -656,10 +656,51 @@ static int lua_copy_framebuffer_to_buffer(lua_State* L) { return 0; } -/* Lua wrapper for copy_buffer_to_framebuffer */ +/* Lua wrapper for copy_buffer_to_framebuffer - skips cursor region if provided */ static int lua_copy_buffer_to_framebuffer(lua_State* L) { - (void)L; /* Unused parameter */ - copy_buffer_to_framebuffer(); + /* Check if cursor region is provided to skip */ + if (lua_gettop(L) >= 4) { + int cx = luaL_checkinteger(L, 1); + int cy = luaL_checkinteger(L, 2); + int cw = luaL_checkinteger(L, 3); + int ch = luaL_checkinteger(L, 4); + + if (framebuffer_ptr) { + wait_for_vblank(); + + int screen_w = DEFAULT_SCREEN_WIDTH; + int screen_h = DEFAULT_SCREEN_HEIGHT; + int bpp = DEFAULT_SCREEN_BPP / 8; + + /* Clamp cursor region to screen */ + int cx1 = cx < 0 ? 0 : cx; + int cy1 = cy < 0 ? 0 : cy; + int cx2 = (cx + cw) > screen_w ? screen_w : (cx + cw); + int cy2 = (cy + ch) > screen_h ? screen_h : (cy + ch); + + /* Copy row by row, skipping cursor region */ + for (int y = 0; y < screen_h; y++) { + uint8_t* src_row = screen_buffer + y * screen_w * bpp; + uint8_t* dst_row = framebuffer_ptr + y * screen_w * bpp; + + if (y < cy1 || y >= cy2) { + /* Row is outside cursor Y range - copy entire row */ + memcpy(dst_row, src_row, screen_w * bpp); + } else { + /* Row intersects cursor - copy left part, skip cursor, copy right part */ + if (cx1 > 0) { + memcpy(dst_row, src_row, cx1 * bpp); + } + if (cx2 < screen_w) { + memcpy(dst_row + cx2 * bpp, src_row + cx2 * bpp, (screen_w - cx2) * bpp); + } + } + } + } + } else { + /* No cursor region - copy everything */ + copy_buffer_to_framebuffer(); + } return 0; } @@ -858,6 +899,26 @@ void usermode_function(void) { lua_pushcfunction(L, lua_vesa_blit_window_buffer_region); lua_setglobal(L, "VESABlitWindowBufferRegion"); + extern int lua_vesa_blit_cursor(lua_State* L); + lua_pushcfunction(L, lua_vesa_blit_cursor); + lua_setglobal(L, "VESABlitCursor"); + + extern int lua_vesa_cache_cursor(lua_State* L); + lua_pushcfunction(L, lua_vesa_cache_cursor); + lua_setglobal(L, "VESACacheCursor"); + + extern int lua_vesa_draw_cursor(lua_State* L); + lua_pushcfunction(L, lua_vesa_draw_cursor); + lua_setglobal(L, "VESADrawCursor"); + + extern int lua_vesa_move_cursor(lua_State* L); + lua_pushcfunction(L, lua_vesa_move_cursor); + lua_setglobal(L, "VESAMoveCursor"); + + extern int lua_vesa_restore_cursor(lua_State* L); + lua_pushcfunction(L, lua_vesa_restore_cursor); + lua_setglobal(L, "VESARestoreCursor"); + lua_pushcfunction(L, lua_vesa_set_render_target); lua_setglobal(L, "VESASetRenderTarget"); diff --git a/vesa.c b/vesa.c @@ -1153,6 +1153,427 @@ int lua_vesa_blit_window_buffer_region(lua_State* L) { return 0; } +/* Blit cursor from Lua table buffer to screen with transparency + * Args: cursor_table (2D array of {r,g,b}), dst_x, dst_y, width, height + * Green (0,255,0) is treated as transparent + */ +int lua_vesa_blit_cursor(lua_State* L) { + if (!lua_istable(L, 1)) return 0; + + int dst_x = luaL_checkinteger(L, 2); + int dst_y = luaL_checkinteger(L, 3); + int width = luaL_checkinteger(L, 4); + int height = luaL_checkinteger(L, 5); + + if (!vesa_state.active) return 0; + + /* Always draw directly to framebuffer for cursor (after buffer swap) */ + uint8_t* screen_buf = vesa_state.framebuffer; + if (!screen_buf) return 0; + + int screen_width = vesa_state.current_mode.width; + int screen_height = vesa_state.current_mode.height; + + /* Process each row */ + for (int y = 0; y < height; y++) { + int screen_y = dst_y + y; + if (screen_y < 0 || screen_y >= screen_height) continue; + + /* Get row table from cursor_table[y+1] */ + lua_pushinteger(L, y + 1); + lua_gettable(L, 1); + + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + continue; + } + + /* Process each pixel in this row */ + for (int x = 0; x < width; x++) { + int screen_x = dst_x + x; + if (screen_x < 0 || screen_x >= screen_width) continue; + + /* Get pixel table from row[x+1] */ + lua_pushinteger(L, x + 1); + lua_gettable(L, -2); + + if (lua_istable(L, -1)) { + /* Get r, g, b from pixel table */ + lua_rawgeti(L, -1, 1); + lua_rawgeti(L, -2, 2); + lua_rawgeti(L, -3, 3); + + int r = lua_tointeger(L, -3); + int g = lua_tointeger(L, -2); + int b = lua_tointeger(L, -1); + + lua_pop(L, 3); /* Pop r, g, b */ + + /* Skip transparent pixels (green = 0,255,0) */ + if (!(r == 0 && g == 255 && b == 0)) { + uint32_t* dst = (uint32_t*)(screen_buf + (screen_y * screen_width + screen_x) * 4); + *dst = (r << 16) | (g << 8) | b; + } + } + + lua_pop(L, 1); /* Pop pixel table */ + } + + lua_pop(L, 1); /* Pop row table */ + } + + return 0; +} + +/* Cursor background save/restore system for flicker-free cursor */ +#define CURSOR_MAX_SIZE 32 +static uint32_t cursor_background[CURSOR_MAX_SIZE * CURSOR_MAX_SIZE]; +static int cursor_bg_x = -1; +static int cursor_bg_y = -1; +static int cursor_bg_w = 0; +static int cursor_bg_h = 0; +static int cursor_bg_valid = 0; + +/* Cached cursor image (converted from Lua table once, used many times) */ +static uint32_t cursor_image[CURSOR_MAX_SIZE * CURSOR_MAX_SIZE]; +static int cursor_image_w = 0; +static int cursor_image_h = 0; +static int cursor_image_valid = 0; +#define CURSOR_TRANSPARENT 0x00FF00 /* Green = transparent */ + +/* Save the area under where the cursor will be drawn */ +static void save_cursor_background(int x, int y, int w, int h) { + if (!vesa_state.active || !vesa_state.framebuffer) return; + if (w > CURSOR_MAX_SIZE) w = CURSOR_MAX_SIZE; + if (h > CURSOR_MAX_SIZE) h = CURSOR_MAX_SIZE; + + int screen_width = vesa_state.current_mode.width; + int screen_height = vesa_state.current_mode.height; + uint8_t* fb = vesa_state.framebuffer; + + cursor_bg_x = x; + cursor_bg_y = y; + cursor_bg_w = w; + cursor_bg_h = h; + + for (int row = 0; row < h; row++) { + int sy = y + row; + if (sy < 0 || sy >= screen_height) continue; + + for (int col = 0; col < w; col++) { + int sx = x + col; + if (sx < 0 || sx >= screen_width) { + cursor_background[row * CURSOR_MAX_SIZE + col] = 0; + continue; + } + + uint32_t* src = (uint32_t*)(fb + (sy * screen_width + sx) * 4); + cursor_background[row * CURSOR_MAX_SIZE + col] = *src; + } + } + cursor_bg_valid = 1; +} + +/* Helper: restore a rectangular region from the saved background buffer */ +static void restore_rect_from_bg(int rect_x, int rect_y, int rect_w, int rect_h) { + if (rect_w <= 0 || rect_h <= 0) return; + + int screen_width = vesa_state.current_mode.width; + int screen_height = vesa_state.current_mode.height; + uint8_t* fb = vesa_state.framebuffer; + + for (int row = 0; row < rect_h; row++) { + int sy = rect_y + row; + if (sy < 0 || sy >= screen_height) continue; + + /* Calculate offset into our saved background buffer */ + int bg_row = sy - cursor_bg_y; + int bg_col_start = rect_x - cursor_bg_x; + int row_width = rect_w; + + if (bg_row < 0 || bg_row >= cursor_bg_h) continue; + + /* Clamp to background buffer bounds */ + int bg_col = bg_col_start; + int sx = rect_x; + if (bg_col < 0) { + row_width += bg_col; + sx -= bg_col; + bg_col = 0; + } + if (bg_col + row_width > cursor_bg_w) { + row_width = cursor_bg_w - bg_col; + } + if (row_width <= 0) continue; + + /* Clamp to screen bounds */ + if (sx < 0) { + bg_col -= sx; + row_width += sx; + sx = 0; + } + if (sx + row_width > screen_width) { + row_width = screen_width - sx; + } + if (row_width <= 0) continue; + + /* Copy the row */ + uint32_t* src = &cursor_background[bg_row * CURSOR_MAX_SIZE + bg_col]; + uint32_t* dst = (uint32_t*)(fb + (sy * screen_width + sx) * 4); + memcpy(dst, src, row_width * sizeof(uint32_t)); + } +} + +/* Restore the area under the old cursor position, but only regions not covered by new cursor. + * When cursor moves from old to new position, we restore up to 2 rectangular strips: + * - Horizontal strip (rows not covered by new position) + * - Vertical strip (columns not covered by new position, excluding already restored rows) + */ +static void restore_cursor_background_partial(int new_x, int new_y, int new_w, int new_h) { + if (!cursor_bg_valid || !vesa_state.active || !vesa_state.framebuffer) return; + + /* Old cursor bounds */ + int old_x1 = cursor_bg_x; + int old_y1 = cursor_bg_y; + int old_x2 = cursor_bg_x + cursor_bg_w; + int old_y2 = cursor_bg_y + cursor_bg_h; + + /* New cursor bounds */ + int new_x1 = new_x; + int new_y1 = new_y; + int new_x2 = new_x + new_w; + int new_y2 = new_y + new_h; + + /* Check if rectangles overlap at all */ + int overlap = !(new_x2 <= old_x1 || new_x1 >= old_x2 || new_y2 <= old_y1 || new_y1 >= old_y2); + + if (!overlap) { + /* No overlap - restore entire old region */ + restore_rect_from_bg(old_x1, old_y1, cursor_bg_w, cursor_bg_h); + } else { + /* There's overlap - restore the L-shaped exposed region as 2 rectangles */ + + /* Horizontal strip: rows that are completely outside the new cursor Y range */ + /* Top strip: old rows above new cursor */ + if (old_y1 < new_y1) { + restore_rect_from_bg(old_x1, old_y1, cursor_bg_w, new_y1 - old_y1); + } + /* Bottom strip: old rows below new cursor */ + if (old_y2 > new_y2) { + restore_rect_from_bg(old_x1, new_y2, cursor_bg_w, old_y2 - new_y2); + } + + /* Vertical strip: columns outside new cursor X range, but only for overlapping Y rows */ + int overlap_y1 = (old_y1 > new_y1) ? old_y1 : new_y1; + int overlap_y2 = (old_y2 < new_y2) ? old_y2 : new_y2; + + if (overlap_y1 < overlap_y2) { + /* Left strip: old columns to the left of new cursor */ + if (old_x1 < new_x1) { + restore_rect_from_bg(old_x1, overlap_y1, new_x1 - old_x1, overlap_y2 - overlap_y1); + } + /* Right strip: old columns to the right of new cursor */ + if (old_x2 > new_x2) { + restore_rect_from_bg(new_x2, overlap_y1, old_x2 - new_x2, overlap_y2 - overlap_y1); + } + } + } + + cursor_bg_valid = 0; +} + +/* Restore entire cursor background (when cursor hides) */ +static void restore_cursor_background_full(void) { + if (!cursor_bg_valid || !vesa_state.active || !vesa_state.framebuffer) return; + + int screen_width = vesa_state.current_mode.width; + int screen_height = vesa_state.current_mode.height; + uint8_t* fb = vesa_state.framebuffer; + + for (int row = 0; row < cursor_bg_h; row++) { + int sy = cursor_bg_y + row; + if (sy < 0 || sy >= screen_height) continue; + + for (int col = 0; col < cursor_bg_w; col++) { + int sx = cursor_bg_x + col; + if (sx < 0 || sx >= screen_width) continue; + + uint32_t* dst = (uint32_t*)(fb + (sy * screen_width + sx) * 4); + *dst = cursor_background[row * CURSOR_MAX_SIZE + col]; + } + } + + cursor_bg_valid = 0; +} + +/* Cache the cursor image from a Lua table (call when cursor changes) */ +int lua_vesa_cache_cursor(lua_State* L) { + if (!lua_istable(L, 1)) return 0; + + int width = luaL_checkinteger(L, 2); + int height = luaL_checkinteger(L, 3); + + if (width > CURSOR_MAX_SIZE) width = CURSOR_MAX_SIZE; + if (height > CURSOR_MAX_SIZE) height = CURSOR_MAX_SIZE; + + cursor_image_w = width; + cursor_image_h = height; + + /* Convert Lua table to flat pixel array */ + for (int y = 0; y < height; y++) { + lua_pushinteger(L, y + 1); + lua_gettable(L, 1); + + if (!lua_istable(L, -1)) { + /* Fill row with transparent */ + for (int x = 0; x < width; x++) { + cursor_image[y * CURSOR_MAX_SIZE + x] = CURSOR_TRANSPARENT; + } + lua_pop(L, 1); + continue; + } + + for (int x = 0; x < width; x++) { + lua_pushinteger(L, x + 1); + lua_gettable(L, -2); + + if (lua_istable(L, -1)) { + lua_rawgeti(L, -1, 1); + lua_rawgeti(L, -2, 2); + lua_rawgeti(L, -3, 3); + + int r = lua_tointeger(L, -3); + int g = lua_tointeger(L, -2); + int b = lua_tointeger(L, -1); + + lua_pop(L, 3); + + cursor_image[y * CURSOR_MAX_SIZE + x] = (r << 16) | (g << 8) | b; + } else { + cursor_image[y * CURSOR_MAX_SIZE + x] = CURSOR_TRANSPARENT; + } + + lua_pop(L, 1); + } + + lua_pop(L, 1); + } + + cursor_image_valid = 1; + return 0; +} + +/* Draw cursor using cached image - much faster than reading Lua table every frame */ +static void draw_cursor_from_cache(int dst_x, int dst_y) { + if (!cursor_image_valid || !vesa_state.active || !vesa_state.framebuffer) return; + + uint8_t* fb = vesa_state.framebuffer; + int screen_width = vesa_state.current_mode.width; + int screen_height = vesa_state.current_mode.height; + + for (int y = 0; y < cursor_image_h; y++) { + int screen_y = dst_y + y; + if (screen_y < 0 || screen_y >= screen_height) continue; + + for (int x = 0; x < cursor_image_w; x++) { + int screen_x = dst_x + x; + if (screen_x < 0 || screen_x >= screen_width) continue; + + uint32_t pixel = cursor_image[y * CURSOR_MAX_SIZE + x]; + + /* Skip transparent pixels */ + if (pixel != CURSOR_TRANSPARENT) { + uint32_t* dst = (uint32_t*)(fb + (screen_y * screen_width + screen_x) * 4); + *dst = pixel; + } + } + } +} + +/* Move cursor: save new background, restore L-shaped region, draw at new position + * Args: old_x, old_y, new_x, new_y + * The cursor size comes from the cached cursor image + * + * IMPORTANT: Order of operations matters! + * 1. Save what's currently at the NEW position (this includes the old cursor if overlapping) + * 2. Restore the L-shaped exposed region from OLD saved background + * 3. Re-save the NEW position (now with restored background in overlap area) + * 4. Draw cursor at NEW position + */ +int lua_vesa_move_cursor(lua_State* L) { + int old_x = luaL_checkinteger(L, 1); + int old_y = luaL_checkinteger(L, 2); + int new_x = luaL_checkinteger(L, 3); + int new_y = luaL_checkinteger(L, 4); + + if (!vesa_state.active || !cursor_image_valid) return 0; + + uint8_t* fb = vesa_state.framebuffer; + if (!fb) return 0; + + int screen_width = vesa_state.current_mode.width; + int screen_height = vesa_state.current_mode.height; + int w = cursor_image_w; + int h = cursor_image_h; + + /* Old and new cursor bounds */ + int old_x1 = old_x, old_y1 = old_y; + int old_x2 = old_x + w, old_y2 = old_y + h; + int new_x1 = new_x, new_y1 = new_y; + int new_x2 = new_x + w, new_y2 = new_y + h; + + (void)new_x1; (void)new_y1; (void)new_x2; (void)new_y2; /* Suppress unused warnings */ + + /* Step 1: Restore ENTIRE old cursor area from SCREEN BUFFER + * The buffer copy skipped the NEW cursor region, so the old cursor area + * (except where it overlaps with new) still has old cursor pixels. + * We restore the whole old area, then draw new cursor on top. */ + extern uint8_t* get_screen_buffer_ptr(void); + uint8_t* screen_buf = get_screen_buffer_ptr(); + + if (screen_buf) { + /* Restore entire old cursor region from screen buffer */ + for (int sy = old_y1; sy < old_y2; sy++) { + if (sy < 0 || sy >= screen_height) continue; + for (int sx = old_x1; sx < old_x2; sx++) { + if (sx < 0 || sx >= screen_width) continue; + uint32_t* src = (uint32_t*)(screen_buf + (sy * screen_width + sx) * 4); + uint32_t* dst = (uint32_t*)(fb + (sy * screen_width + sx) * 4); + *dst = *src; + } + } + } + + /* Step 2: Save background at new position (after L-shape restore) */ + save_cursor_background(new_x, new_y, w, h); + + /* Step 3: Draw cursor at new position */ + draw_cursor_from_cache(new_x, new_y); + + return 0; +} + +/* Draw cursor at position (for initial draw, no old position to restore) */ +int lua_vesa_draw_cursor(lua_State* L) { + int dst_x = luaL_checkinteger(L, 1); + int dst_y = luaL_checkinteger(L, 2); + + if (!vesa_state.active || !cursor_image_valid) return 0; + + /* Save background and draw */ + save_cursor_background(dst_x, dst_y, cursor_image_w, cursor_image_h); + draw_cursor_from_cache(dst_x, dst_y); + + return 0; +} + +/* Restore cursor background fully (for hiding cursor) */ +int lua_vesa_restore_cursor(lua_State* L) { + (void)L; + restore_cursor_background_full(); + return 0; +} + /* Set render target for drawing operations */ int lua_vesa_set_render_target(lua_State* L) { if (lua_isnil(L, 1)) {