commit 6d8e2ce424a94807a0aa2669ef1f1868d9c61d37
parent cf723cc65d7d8ef0f08ac79fcf4a787409445b32
Author: luajitos <bbhbb2094@gmail.com>
Date: Sat, 6 Dec 2025 21:26:19 +0000
Added text selection
Diffstat:
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")