luajitos

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

commit 6d8e2ce424a94807a0aa2669ef1f1868d9c61d37
parent cf723cc65d7d8ef0f08ac79fcf4a787409445b32
Author: luajitos <bbhbb2094@gmail.com>
Date:   Sat,  6 Dec 2025 21:26:19 +0000

Added text selection

Diffstat:
Miso_includes/apps/com.luajitos.lunareditor/src/editor.lua | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Miso_includes/apps/com.luajitos.taskbar/src/init.lua | 5+++++
Miso_includes/os/init.lua | 298+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Miso_includes/os/libs/Application.lua | 40+++++++++++++++++++++++++++++++++++++++-
Miso_includes/os/postinit.lua | 41+++++++++++++++++++++++++++++++++++++++++
5 files changed, 440 insertions(+), 15 deletions(-)

diff --git a/iso_includes/apps/com.luajitos.lunareditor/src/editor.lua b/iso_includes/apps/com.luajitos.lunareditor/src/editor.lua @@ -266,6 +266,13 @@ end window.onInput = function(key, scancode) local baseScancode = scancode % 128 + -- Debug: print key info + if key then + print("Lunar Editor: key='" .. key .. "' byte=" .. tostring(key:byte()) .. " scancode=" .. tostring(scancode) .. " base=" .. tostring(baseScancode)) + else + print("Lunar Editor: key=nil scancode=" .. tostring(scancode) .. " base=" .. tostring(baseScancode)) + end + -- Arrow keys if baseScancode == 72 then -- Up if cursorLine > 1 then @@ -325,6 +332,23 @@ window.onInput = function(key, scancode) ensureCursorVisible() window:markDirty() return + elseif baseScancode == 83 then -- Delete + local line = lines[cursorLine] + if cursorCol <= #line then + -- Delete character at cursor + lines[cursorLine] = line:sub(1, cursorCol - 1) .. line:sub(cursorCol + 1) + modified = true + updateTitle() + elseif cursorLine < #lines then + -- Join with next line + lines[cursorLine] = line .. lines[cursorLine + 1] + table.remove(lines, cursorLine + 1) + modified = true + updateTitle() + end + clampCursor() + window:markDirty() + return end -- Text input @@ -371,6 +395,53 @@ window.onInput = function(key, scancode) end end +-- Paste callback for handling multi-line pastes properly +window.onPaste = function(content, contentType) + if not content or content == "" then return end + + -- Split content into lines + local pasteLines = {} + for line in (content .. "\n"):gmatch("([^\n]*)\n") do + table.insert(pasteLines, line) + end + + if #pasteLines == 0 then return end + + -- Get current line content + local currentLine = lines[cursorLine] or "" + local beforeCursor = currentLine:sub(1, cursorCol - 1) + local afterCursor = currentLine:sub(cursorCol) + + if #pasteLines == 1 then + -- Single line paste - insert inline + lines[cursorLine] = beforeCursor .. pasteLines[1] .. afterCursor + cursorCol = cursorCol + #pasteLines[1] + else + -- Multi-line paste + -- First line: append to current position + lines[cursorLine] = beforeCursor .. pasteLines[1] + + -- Middle lines: insert as new lines + for i = 2, #pasteLines - 1 do + table.insert(lines, cursorLine + i - 1, pasteLines[i]) + end + + -- Last line: prepend to remaining content + local lastPasteLine = pasteLines[#pasteLines] + table.insert(lines, cursorLine + #pasteLines - 1, lastPasteLine .. afterCursor) + + -- Update cursor position + cursorLine = cursorLine + #pasteLines - 1 + cursorCol = #lastPasteLine + 1 + end + + modified = true + updateTitle() + clampCursor() + ensureCursorVisible() + window:markDirty() +end + -- Click callback window.onClick = function(x, y, button) -- Check toolbar diff --git a/iso_includes/apps/com.luajitos.taskbar/src/init.lua b/iso_includes/apps/com.luajitos.taskbar/src/init.lua @@ -507,6 +507,11 @@ local function showStartMenu() break end + -- Check if click is in the gap after this item (before checking item itself) + if my >= yPos + itemHeight and my < yPos + itemHeight + 2 then + return -- Click in gap between items, do nothing + end + if my >= yPos and my < yPos + itemHeight then -- Launch this app if run then diff --git a/iso_includes/os/init.lua b/iso_includes/os/init.lua @@ -50,6 +50,62 @@ local function initCursorBuffer() end initCursorBuffer() +-- Text selection helper: find text at position and return index + character position +local function findTextAtPosition(win, mx, my) + if not win.selectableText then + osprint("[SELECT] findTextAtPosition: no selectableText array\n") + return nil + end + -- Mouse coordinates are already content-relative (window.x/y point to content area) + local content_mx = mx + local content_my = my + + osprint("[SELECT] findTextAtPosition: mx=" .. mx .. " my=" .. my .. " -> content_mx=" .. content_mx .. " content_my=" .. content_my .. " numTexts=" .. #win.selectableText .. " isBorderless=" .. tostring(win.isBorderless) .. "\n") + + for i, textInfo in ipairs(win.selectableText) do + osprint("[SELECT] text[" .. i .. "]: x=" .. textInfo.x .. " y=" .. textInfo.y .. " w=" .. textInfo.w .. " h=" .. textInfo.h .. " text='" .. (textInfo.text:sub(1,20)) .. "'\n") + if content_mx >= textInfo.x and content_mx <= textInfo.x + textInfo.w and + content_my >= textInfo.y and content_my < textInfo.y + textInfo.h then + -- Calculate character position within the text + local charWidth = 8 * (textInfo.scale or 1) + local charPos = math.floor((content_mx - textInfo.x) / charWidth) + 1 + if charPos < 1 then charPos = 1 end + if charPos > #textInfo.text then charPos = #textInfo.text end + return i, charPos + end + end + return nil +end + +-- Text selection helper: get selected text content between start and finish +local function getSelectedText(win) + if not win.selection or not win.selection.start or not win.selection.finish then + return "" + end + local startIdx = win.selection.start.index + local startPos = win.selection.start.pos + local finishIdx = win.selection.finish.index + local finishPos = win.selection.finish.pos + + -- Normalize so start is before finish + if startIdx > finishIdx or (startIdx == finishIdx and startPos > finishPos) then + startIdx, finishIdx = finishIdx, startIdx + startPos, finishPos = finishPos, startPos + end + + local result = {} + for i = startIdx, finishIdx do + local textInfo = win.selectableText[i] + if textInfo then + local text = textInfo.text + local s = (i == startIdx) and startPos or 1 + local e = (i == finishIdx) and finishPos or #text + table.insert(result, text:sub(s, e)) + end + end + return table.concat(result, "\n") +end + -- SafeGfx for cursor drawing (draws to cursor_buffer) local function createCursorGfx() local gfx = {} @@ -808,6 +864,30 @@ local function drawAllWindows() local content_x = window.isBorderless and 0 or BORDER_WIDTH local content_y = window.isBorderless and 0 or (BORDER_WIDTH + TITLE_BAR_HEIGHT) + -- Build selection ranges BEFORE creating window_gfx so drawText can use them + local selectedRanges = {} + if window.selection and window.selection.start and window.selection.finish and window.selectableText then + local startIdx = window.selection.start.index + local startPos = window.selection.start.pos + local finishIdx = window.selection.finish.index + local finishPos = window.selection.finish.pos + + -- Normalize so start is before finish + if startIdx > finishIdx or (startIdx == finishIdx and startPos > finishPos) then + startIdx, finishIdx = finishIdx, startIdx + startPos, finishPos = finishPos, startPos + end + + for i = startIdx, finishIdx do + local s = (i == startIdx) and startPos or 1 + local e = (i == finishIdx) and finishPos or 99999 -- will be clamped + selectedRanges[i] = {s = s, e = e} + end + end + + -- Track text index for selection highlighting + local textIndex = 0 + local window_gfx = { fillRect = function(self, x, y, w, h, color) if type(self) == "number" then @@ -847,10 +927,70 @@ local function drawAllWindows() y = x x = self end + scale = scale or 1 + textIndex = textIndex + 1 + + local charWidth = 8 * scale + local charHeight = 12 * scale + local textWidth = #text * charWidth + + -- Record text for selection support + table.insert(window.selectableText, { + text = text, + x = x, + y = y, + w = textWidth, + h = charHeight, + color = color, + scale = scale + }) + + -- Check if this text has selection highlighting + local selRange = selectedRanges[textIndex] + if selRange then + osprint("[SELECT] Highlighting textIndex=" .. textIndex .. " text='" .. text:sub(1,10) .. "' s=" .. selRange.s .. " e=" .. selRange.e .. " scale=" .. scale .. " charWidth=" .. charWidth .. "\n") + local r = bit.band(bit.rshift(color, 16), 0xFF) + local g = bit.band(bit.rshift(color, 8), 0xFF) + local b = bit.band(color, 0xFF) + local s = selRange.s + local e = math.min(selRange.e, #text) + + -- Draw selection highlight behind text + local hlX = x + (s - 1) * charWidth + local hlW = (e - s + 1) * charWidth + addRect(content_x + hlX, content_y + y - 1, hlW, charHeight, 51, 102, 204) + + -- Draw text in 3 parts: before selection, selected (white), after selection + if s > 1 then + addText(content_x + x, content_y + y, text:sub(1, s - 1), r, g, b, scale) + end + -- Selected text (white on blue) + addText(content_x + x + (s - 1) * charWidth, content_y + y, text:sub(s, e), 255, 255, 255, scale) + if e < #text then + addText(content_x + x + e * charWidth, content_y + y, text:sub(e + 1), r, g, b, scale) + end + else + -- No selection, draw normally + local r = bit.band(bit.rshift(color, 16), 0xFF) + local g = bit.band(bit.rshift(color, 8), 0xFF) + local b = bit.band(color, 0xFF) + addText(content_x + x, content_y + y, text, r, g, b, scale) + end + end, + drawUText = function(self, x, y, text, color, scale) + -- Draw unselectable text (not recorded for selection) + if type(self) == "string" then + scale = color + color = text + text = y + y = x + x = self + end + scale = scale or 1 local r = bit.band(bit.rshift(color, 16), 0xFF) local g = bit.band(bit.rshift(color, 8), 0xFF) local b = bit.band(color, 0xFF) - addText(content_x + x, content_y + y, text, r, g, b, scale or 1) + addText(content_x + x, content_y + y, text, r, g, b, scale) end, drawImage = function(self, image, x, y, w, h) -- Handle both method and function call syntax @@ -875,6 +1015,9 @@ local function drawAllWindows() -- Call app's onDraw callback if it exists (reactive mode) if window.onDraw then + -- Clear selectableText before each draw so it reflects current frame + window.selectableText = {} + textIndex = 0 -- Reset text index for this draw local success, err = pcall(window.onDraw, window_gfx) if not success then osprint("ERROR drawing window: " .. tostring(err) .. "\n") @@ -883,9 +1026,12 @@ local function drawAllWindows() -- Also add window's imperative draw ops (from window.gfx) if window._draw_ops and #window._draw_ops > 0 then + local textIndex = 0 for _, op in ipairs(window._draw_ops) do -- Adjust coordinates to account for window frame local adjusted_op = {op[1]} + local skipNormalAdd = false + if op[1] == OP_RECT_FILL then -- {1, x, y, w, h, r, g, b} adjusted_op[2] = content_x + op[2] @@ -896,6 +1042,7 @@ local function drawAllWindows() adjusted_op[7] = op[7] adjusted_op[8] = op[8] elseif op[1] == OP_TEXT then + textIndex = textIndex + 1 -- {10, x, y, text, r, g, b, scale} adjusted_op[2] = content_x + op[2] adjusted_op[3] = content_y + op[3] @@ -904,6 +1051,65 @@ local function drawAllWindows() adjusted_op[6] = op[6] adjusted_op[7] = op[7] adjusted_op[8] = op[8] or 1 -- scale + + -- Check if this text has selection + local selRange = selectedRanges[textIndex] + if selRange then + local text = op[4] + local scale = op[8] or 1 + local charWidth = 8 * scale + local charHeight = 12 * scale + local s = selRange.s + local e = math.min(selRange.e, #text) + + -- Draw selection highlight behind text + local hlX = op[2] + (s - 1) * charWidth + local hlW = (e - s + 1) * charWidth + draw_ops[#draw_ops + 1] = { + OP_RECT_FILL, + content_x + hlX, + content_y + op[3] - 1, + hlW, + charHeight, + 51, 102, 204 -- Selection blue + } + + -- Draw text in 3 parts: before selection, selected (white), after selection + if s > 1 then + -- Text before selection (original color) + draw_ops[#draw_ops + 1] = { + OP_TEXT, + content_x + op[2], + content_y + op[3], + text:sub(1, s - 1), + op[5], op[6], op[7], + scale + } + end + + -- Selected text (white on blue) + draw_ops[#draw_ops + 1] = { + OP_TEXT, + content_x + op[2] + (s - 1) * charWidth, + content_y + op[3], + text:sub(s, e), + 255, 255, 255, -- White text + scale + } + + if e < #text then + -- Text after selection (original color) + draw_ops[#draw_ops + 1] = { + OP_TEXT, + content_x + op[2] + e * charWidth, + content_y + op[3], + text:sub(e + 1), + op[5], op[6], op[7], + scale + } + end + skipNormalAdd = true + end elseif op[1] == OP_PIXEL then -- {3, x, y, r, g, b} adjusted_op[2] = content_x + op[2] @@ -920,7 +1126,10 @@ local function drawAllWindows() adjusted_op[6] = op[6] adjusted_op[7] = op[7] end - draw_ops[#draw_ops + 1] = adjusted_op + + if not skipNormalAdd then + draw_ops[#draw_ops + 1] = adjusted_op + end end end @@ -1304,19 +1513,43 @@ function MainDraw() _G.mouse_capture_window = clicked_window -- Call onMouseDown first (new event) + local mouseDownHandled = false if clicked_window.onMouseDown then - local success, err = pcall(clicked_window.onMouseDown, click_x, click_y) + local success, result = pcall(clicked_window.onMouseDown, click_x, click_y) if not success and osprint then - osprint("[ERROR] onMouseDown callback failed: " .. tostring(err) .. "\n") + osprint("[ERROR] onMouseDown callback failed: " .. tostring(result) .. "\n") + elseif result == true then + mouseDownHandled = true end end -- Also call onClick for backwards compatibility if clicked_window.onClick then - osprint("Calling onClick for window, relative pos: " .. click_x .. "," .. click_y .. "\n") - local success, err = pcall(clicked_window.onClick, click_x, click_y) + local success, result = pcall(clicked_window.onClick, click_x, click_y) if not success and osprint then - osprint("[ERROR] onClick callback failed: " .. tostring(err) .. "\n") + osprint("[ERROR] onClick callback failed: " .. tostring(result) .. "\n") + elseif result == true then + mouseDownHandled = true + end + end + + -- If mouse events weren't handled and window is selectable, start text selection + if not mouseDownHandled and clicked_window.selectable ~= false then + local textIdx, charPos = findTextAtPosition(clicked_window, click_x, click_y) + osprint("[SELECT] mouseDown at click_x=" .. click_x .. " click_y=" .. click_y .. " selectable=" .. tostring(clicked_window.selectable) .. " selectableText count=" .. #(clicked_window.selectableText or {}) .. "\n") + if textIdx then + osprint("[SELECT] START selection: textIdx=" .. textIdx .. " charPos=" .. charPos .. "\n") + clicked_window.selection = { + start = { index = textIdx, pos = charPos }, + finish = { index = textIdx, pos = charPos }, + content = "", + type = "text" + } + clicked_window.dirty = true + else + -- Clear selection when clicking outside text + osprint("[SELECT] No text found at position, clearing selection\n") + clicked_window.selection = nil end end end @@ -1329,17 +1562,29 @@ function MainDraw() end end - -- Handle mouse move while button is down (for painting) + -- Handle mouse move while button is down (for painting/selection) if _G.mouse_capture_window and mouse1_down and mouse_moved then local win = _G.mouse_capture_window local move_x = _G.cursor_state.x - win.x local move_y = _G.cursor_state.y - win.y + local mouseMoveHandled = false if win.onMouseMove then win.dirty = true - local success, err = pcall(win.onMouseMove, move_x, move_y) + local success, result = pcall(win.onMouseMove, move_x, move_y) if not success and osprint then - osprint("[ERROR] onMouseMove callback failed: " .. tostring(err) .. "\n") + osprint("[ERROR] onMouseMove callback failed: " .. tostring(result) .. "\n") + elseif result == true then + mouseMoveHandled = true + end + end + + -- If mouse move wasn't handled and we have an active selection, update it + if not mouseMoveHandled and win.selectable ~= false and win.selection and win.selection.start then + local textIdx, charPos = findTextAtPosition(win, move_x, move_y) + if textIdx then + win.selection.finish = { index = textIdx, pos = charPos } + win.dirty = true end end end @@ -1483,15 +1728,40 @@ function MainDraw() end -- Handle mouse up for captured window + if mouse1_released then + osprint("[SELECT] mouse1_released, capture_window=" .. tostring(_G.mouse_capture_window ~= nil) .. "\n") + end if mouse1_released and _G.mouse_capture_window then local win = _G.mouse_capture_window + local up_x = _G.cursor_state.x - win.x + local up_y = _G.cursor_state.y - win.y + + osprint("[SELECT] mouseUp: up_x=" .. up_x .. " up_y=" .. up_y .. " hasSelection=" .. tostring(win.selection ~= nil) .. "\n") + + local mouseUpHandled = false if win.onMouseUp then - local up_x = _G.cursor_state.x - win.x - local up_y = _G.cursor_state.y - win.y - local success, err = pcall(win.onMouseUp, up_x, up_y) + local success, result = pcall(win.onMouseUp, up_x, up_y) if not success and osprint then - osprint("[ERROR] onMouseUp callback failed: " .. tostring(err) .. "\n") + osprint("[ERROR] onMouseUp callback failed: " .. tostring(result) .. "\n") + elseif result == true then + mouseUpHandled = true + end + end + + osprint("[SELECT] mouseUpHandled=" .. tostring(mouseUpHandled) .. " selectable=" .. tostring(win.selectable) .. " selection.start=" .. tostring(win.selection and win.selection.start) .. "\n") + + -- Finalize text selection if active + if not mouseUpHandled and win.selectable ~= false and win.selection and win.selection.start then + local textIdx, charPos = findTextAtPosition(win, up_x, up_y) + if textIdx then + win.selection.finish = { index = textIdx, pos = charPos } + osprint("[SELECT] END selection: textIdx=" .. textIdx .. " charPos=" .. charPos .. "\n") end + -- Get the selected text content + win.selection.content = getSelectedText(win) + win.selection.type = "text" + osprint("[SELECT] FINALIZED selection content: '" .. (win.selection.content or "") .. "'\n") + win.dirty = true end _G.mouse_capture_window = nil end diff --git a/iso_includes/os/libs/Application.lua b/iso_includes/os/libs/Application.lua @@ -481,7 +481,11 @@ function Application:newWindow(arg1, arg2, arg3, arg4, arg5, arg6) restoreX = nil, restoreY = nil, restoreWidth = nil, - restoreHeight = nil + restoreHeight = nil, + -- Text selection support + selectable = true, -- Whether text selection is enabled for this window + selectableText = {}, -- Array of {text, x, y, w, h, color, scale} + selection = nil -- {start={index,pos}, finish={index,pos}, content, type} } -- Window cursor API (window.cursor.add, window.cursor.remove, etc.) @@ -525,6 +529,8 @@ function Application:newWindow(arg1, arg2, arg3, arg4, arg5, arg6) -- Window draw method (will be called by LPM drawScreen) function window:draw(safeGfx) + -- Clear selectable text array before each draw + self.selectableText = {} if self.onDraw and self.visible then self.onDraw(safeGfx) end @@ -791,6 +797,38 @@ function Application:newWindow(arg1, arg2, arg3, arg4, arg5, arg6) local b = bit.band(color, 0xFF) scale = scale or 1 table.insert(gfxSelf._window._draw_ops, {10, x, y, text, r, g, b, scale}) + + -- Record text for selection support (6 pixels per char at scale 1, 12 pixels height) + local charWidth = 8 * scale + local charHeight = 12 * scale + local textWidth = #text * charWidth + table.insert(gfxSelf._window.selectableText, { + text = text, + x = x, + y = y, + w = textWidth, + h = charHeight, + color = color, + scale = scale + }) + end, + + drawUText = function(gfxSelf, x, y, text, color, scale) + -- Draw unselectable text (not recorded for selection) + if type(gfxSelf) == "number" then + scale = color + color = text + text = y + y = x + x = gfxSelf + gfxSelf = window.gfx + end + local r = bit.band(bit.rshift(color, 16), 0xFF) + local g = bit.band(bit.rshift(color, 8), 0xFF) + local b = bit.band(color, 0xFF) + scale = scale or 1 + table.insert(gfxSelf._window._draw_ops, {10, x, y, text, r, g, b, scale}) + -- Note: not recording in selectableText end, drawImage = function(gfxSelf, image, x, y, w, h) diff --git a/iso_includes/os/postinit.lua b/iso_includes/os/postinit.lua @@ -316,6 +316,47 @@ if _G.sys and _G.sys.hotkey then end) osprint("DEBUG: Registered hotkey as: " .. tostring(normalized) .. "\n") + -- Initialize global clipboard + _G.clipboard = { + content = nil, + type = nil -- "text" or other types + } + + -- Ctrl+C to copy selection from active window + _G.sys.hotkey.add("ctrl+c", function() + local win = _G.sys.activeWindow + if win and win.selection and win.selection.content and win.selection.content ~= "" then + _G.clipboard.content = win.selection.content + _G.clipboard.type = win.selection.type or "text" + osprint("[CLIPBOARD] Copied: '" .. (_G.clipboard.content:sub(1, 50)) .. "' type=" .. tostring(_G.clipboard.type) .. "\n") + end + end) + + -- Ctrl+V to paste to active window + _G.sys.hotkey.add("ctrl+v", function() + local win = _G.sys.activeWindow + if win and _G.clipboard.content then + -- Try onPaste first + if win.onPaste then + local success, err = pcall(win.onPaste, _G.clipboard.content, _G.clipboard.type) + if not success and osprint then + osprint("[CLIPBOARD] onPaste error: " .. tostring(err) .. "\n") + end + elseif win.onInput then + -- Send each character to onInput (except \n, \t, \r) + for i = 1, #_G.clipboard.content do + local char = _G.clipboard.content:sub(i, i) + if char ~= "\n" and char ~= "\t" and char ~= "\r" then + local success, err = pcall(win.onInput, char, 0, true) -- isPasted = true + if not success and osprint then + osprint("[CLIPBOARD] onInput error: " .. tostring(err) .. "\n") + end + end + end + end + end + end) + -- Check if it was registered local hasIt = _G.sys.hotkey.has("meta+r") osprint("DEBUG: Hotkey 'meta+r' registered: " .. tostring(hasIt) .. "\n")