commit cbbaa8fda857e5bfc3642f732bb56a365bf45bbb
parent 6d8e2ce424a94807a0aa2669ef1f1868d9c61d37
Author: luajitos <bbhbb2094@gmail.com>
Date: Sat, 6 Dec 2025 22:20:04 +0000
Text selection now replaceable/pastable
Diffstat:
5 files changed, 287 insertions(+), 37 deletions(-)
diff --git a/iso_includes/apps/com.luajitos.lunareditor/src/editor.lua b/iso_includes/apps/com.luajitos.lunareditor/src/editor.lua
@@ -264,14 +264,7 @@ end
-- Input callback
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
+ local baseScancode = (scancode or 0) % 128
-- Arrow keys
if baseScancode == 72 then -- Up
@@ -442,6 +435,91 @@ window.onPaste = function(content, contentType)
window:markDirty()
end
+-- Selection edit callback for multi-line selection editing
+-- point1 and point2 are {x, y} coordinates in the content area
+window.onSelectionEditted = function(point1, point2, newContent)
+ -- Convert y coordinates to line numbers
+ -- Text starts at y = toolbarHeight + 2, each line is lineHeight pixels
+ local textStartY = toolbarHeight + 2
+
+ local startLine = scrollY + math.floor((point1.y - textStartY) / lineHeight) + 1
+ local endLine = scrollY + math.floor((point2.y - textStartY) / lineHeight) + 1
+
+ -- Convert x coordinates to column positions
+ -- Text starts at x = 5, each character is charWidth pixels
+ local startCol = math.floor((point1.x - 5) / charWidth) + 1
+ local endCol = math.floor((point2.x - 5) / charWidth) + 1
+
+ -- Normalize so start is before end
+ if startLine > endLine or (startLine == endLine and startCol > endCol) then
+ startLine, endLine = endLine, startLine
+ startCol, endCol = endCol, startCol
+ end
+
+ -- Validate line numbers
+ if startLine < 1 then startLine = 1 end
+ if endLine > #lines then endLine = #lines end
+ if startLine > #lines then return end
+
+ -- Clamp column positions
+ if startCol < 1 then startCol = 1 end
+ if endCol < 1 then endCol = 1 end
+ if startCol > #lines[startLine] + 1 then startCol = #lines[startLine] + 1 end
+ if endCol > #lines[endLine] then endCol = #lines[endLine] end
+
+ -- Get the text before and after the selection
+ local beforeText = lines[startLine]:sub(1, startCol - 1)
+ local afterText = lines[endLine]:sub(endCol + 1)
+
+ -- Split newContent into lines
+ local newLines = {}
+ for line in ((newContent or "") .. "\n"):gmatch("([^\n]*)\n") do
+ table.insert(newLines, line)
+ end
+ -- Remove the trailing empty line added by the pattern
+ if #newLines > 0 and newLines[#newLines] == "" and not (newContent or ""):match("\n$") then
+ table.remove(newLines)
+ end
+ if #newLines == 0 then
+ newLines = {""}
+ end
+
+ -- Remove all lines in the selection range
+ for i = endLine, startLine + 1, -1 do
+ table.remove(lines, i)
+ end
+
+ -- Build the replacement
+ if #newLines == 1 then
+ -- Single line replacement
+ lines[startLine] = beforeText .. newLines[1] .. afterText
+ cursorLine = startLine
+ cursorCol = #beforeText + #newLines[1] + 1
+ else
+ -- Multi-line replacement
+ -- First line: beforeText + first new line
+ lines[startLine] = beforeText .. newLines[1]
+
+ -- Middle lines: insert as new lines
+ for i = 2, #newLines - 1 do
+ table.insert(lines, startLine + i - 1, newLines[i])
+ end
+
+ -- Last line: last new line + afterText
+ local lastNewLine = newLines[#newLines]
+ table.insert(lines, startLine + #newLines - 1, lastNewLine .. afterText)
+
+ cursorLine = startLine + #newLines - 1
+ cursorCol = #lastNewLine + 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
@@ -187,7 +187,7 @@ local function showWindowPopup(appInfo, buttonX)
gfx:drawRect(0, yPos, popupWidth, appButtonHeight, 0x606060)
-- Draw text
- gfx:drawText(5, yPos + 13, title, 0xFFFFFF)
+ gfx:drawUText(5, yPos + 13, title, 0xFFFFFF)
end
end
@@ -393,7 +393,7 @@ local function showStartMenu()
-- Title bar
gfx:fillRect(0, 0, menuWidth, 30, 0x0066CC)
- gfx:drawText(10, 8, "Applications", 0xFFFFFF)
+ gfx:drawUText(10, 8, "Applications", 0xFFFFFF)
-- List available apps by category
local yPos = 40
@@ -408,7 +408,7 @@ local function showStartMenu()
end
if not hasApps then
- gfx:drawText(10, yPos, "No applications found", 0x888888)
+ gfx:drawUText(10, yPos, "No applications found", 0x888888)
else
-- Sort categories alphabetically
local sortedCategories = {}
@@ -427,7 +427,7 @@ local function showStartMenu()
-- Draw category header
gfx:fillRect(5, yPos, menuWidth - 10, categoryHeaderHeight, 0x1A1A1A)
- gfx:drawText(10, yPos + 6, category, 0xCCCCCC)
+ gfx:drawUText(10, yPos + 6, category, 0xCCCCCC)
yPos = yPos + categoryHeaderHeight + 2
-- Draw apps in this category
@@ -463,7 +463,7 @@ local function showStartMenu()
end
-- Draw app name
- gfx:drawText(text_x_offset, yPos + 8, appInfo.name, 0xFFFFFF)
+ gfx:drawUText(text_x_offset, yPos + 8, appInfo.name, 0xFFFFFF)
yPos = yPos + itemHeight + 2
end
@@ -619,7 +619,7 @@ taskbar.onDraw = function(gfx)
gfx:drawRect(startButtonX, startButtonY, startButtonWidth, startButtonHeight, 0x0088FF)
-- Start button text
- gfx:drawText(startButtonX + 20, startButtonY + 13, "Start", 0xFFFFFF)
+ gfx:drawUText(startButtonX + 20, startButtonY + 13, "Start", 0xFFFFFF)
-- Draw application buttons
local apps = getRunningApplications()
@@ -672,7 +672,7 @@ taskbar.onDraw = function(gfx)
-- Draw text (dimmed if minimized)
local textColor = appInfo.hasMinimized and 0xAAAAAA or 0xFFFFFF
- gfx:drawText(currentX + text_x_offset, appButtonY + 13, appName, textColor)
+ gfx:drawUText(currentX + text_x_offset, appButtonY + 13, appName, textColor)
currentX = currentX + appButtonWidth + 5
end
@@ -681,7 +681,7 @@ taskbar.onDraw = function(gfx)
local timeText = formatTime()
local timeX = screenWidth - 100 -- Position near right edge
local timeY = 15 -- Vertically centered
- gfx:drawText(timeX, timeY, timeText, 0xFFFFFF)
+ gfx:drawUText(timeX, timeY, timeText, 0xFFFFFF)
-- Draw separator line at top
gfx:fillRect(0, 0, screenWidth, 1, 0x444444)
diff --git a/iso_includes/os/init.lua b/iso_includes/os/init.lua
@@ -53,17 +53,13 @@ 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
@@ -106,6 +102,98 @@ local function getSelectedText(win)
return table.concat(result, "\n")
end
+-- Calculate edited text boxes after selection replacement
+-- textBoxes: array of {text, x, y, w, h, scale, ...}
+-- point1, point2: selection endpoints {x, y} in content coordinates
+-- newContent: string to replace selection with
+-- Returns: new array of text boxes with selection replaced
+local function calcSelectionEditted(textBoxes, point1, point2, newContent)
+ if not textBoxes or #textBoxes == 0 then
+ return textBoxes
+ end
+
+ -- Find which text boxes contain the selection points
+ local startIdx, startPos, finishIdx, finishPos
+
+ for i, box in ipairs(textBoxes) do
+ local charWidth = 8 * (box.scale or 1)
+ local charHeight = 12 * (box.scale or 1)
+
+ -- Check if point1 is in this box
+ if not startIdx then
+ if point1.y >= box.y and point1.y < box.y + charHeight and
+ point1.x >= box.x and point1.x <= box.x + box.w then
+ startIdx = i
+ startPos = math.floor((point1.x - box.x) / charWidth) + 1
+ if startPos < 1 then startPos = 1 end
+ if startPos > #box.text then startPos = #box.text end
+ end
+ end
+
+ -- Check if point2 is in this box
+ if not finishIdx then
+ if point2.y >= box.y and point2.y < box.y + charHeight and
+ point2.x >= box.x and point2.x <= box.x + box.w then
+ finishIdx = i
+ finishPos = math.floor((point2.x - box.x) / charWidth) + 1
+ if finishPos < 1 then finishPos = 1 end
+ if finishPos > #box.text then finishPos = #box.text end
+ end
+ end
+ end
+
+ if not startIdx or not finishIdx then
+ return textBoxes
+ end
+
+ -- 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
+
+ -- Build new text boxes array
+ local result = {}
+
+ -- Copy boxes before selection
+ for i = 1, startIdx - 1 do
+ result[#result + 1] = textBoxes[i]
+ end
+
+ -- Handle the selection replacement
+ local firstBox = textBoxes[startIdx]
+ local lastBox = textBoxes[finishIdx]
+
+ if firstBox then
+ -- Text before selection in first box + newContent + text after selection in last box
+ local beforeText = firstBox.text:sub(1, startPos - 1)
+ local afterText = lastBox and lastBox.text:sub(finishPos + 1) or ""
+
+ -- Create merged box with replaced content
+ local newBox = {}
+ for k, v in pairs(firstBox) do
+ newBox[k] = v
+ end
+ newBox.text = beforeText .. (newContent or "") .. afterText
+ -- Recalculate width based on new text length
+ local scale = newBox.scale or 1
+ local charWidth = 8 * scale
+ newBox.w = #newBox.text * charWidth
+
+ result[#result + 1] = newBox
+ end
+
+ -- Copy boxes after selection
+ for i = finishIdx + 1, #textBoxes do
+ result[#result + 1] = textBoxes[i]
+ end
+
+ return result
+end
+
+-- Make calcSelectionEditted available globally
+_G.calcSelectionEditted = calcSelectionEditted
+
-- SafeGfx for cursor drawing (draws to cursor_buffer)
local function createCursorGfx()
local gfx = {}
@@ -948,7 +1036,6 @@ local function drawAllWindows()
-- 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)
@@ -1536,9 +1623,7 @@ function MainDraw()
-- 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 },
@@ -1548,7 +1633,6 @@ function MainDraw()
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
@@ -1728,16 +1812,11 @@ 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 success, result = pcall(win.onMouseUp, up_x, up_y)
@@ -1748,19 +1827,15 @@ function MainDraw()
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
diff --git a/iso_includes/os/libs/Sys.lua b/iso_includes/os/libs/Sys.lua
@@ -665,10 +665,73 @@ function sys.sendInput(key, scancode)
end
end
+ -- Check if there's an active selection and a printable key was pressed
+ local win = sys.activeWindow
+ local hasSelection = win.selection and win.selection.start and win.selection.finish and
+ win.selection.content and win.selection.content ~= ""
+
+ -- Check if this is a printable character or special editing key (backspace, delete, enter)
+ local isPrintable = key and #key == 1 and key:byte() >= 32
+ local isBackspace = key == "\b"
+ local isEnter = key == "\n"
+ local isDelete = baseScancode == 83
+
+ if hasSelection and (isPrintable or isBackspace or isEnter or isDelete) then
+ -- Selection is active and an editing key was pressed
+ if win.onSelectionEditted then
+ local newContent = ""
+ if isPrintable then
+ newContent = key
+ elseif isEnter then
+ newContent = "\n"
+ end
+ -- Backspace and Delete replace selection with empty string
+
+ -- Convert selection indices to x,y coordinates
+ local startText = win.selectableText and win.selectableText[win.selection.start.index]
+ local finishText = win.selectableText and win.selectableText[win.selection.finish.index]
+
+ local point1, point2
+ if startText then
+ local charWidth = 8 * (startText.scale or 1)
+ point1 = {
+ x = startText.x + (win.selection.start.pos - 1) * charWidth,
+ y = startText.y
+ }
+ end
+ if finishText then
+ local charWidth = 8 * (finishText.scale or 1)
+ point2 = {
+ x = finishText.x + (win.selection.finish.pos - 1) * charWidth,
+ y = finishText.y
+ }
+ end
+
+ if not point1 or not point2 then
+ -- Fall back to passing the raw selection if we can't get coordinates
+ point1 = win.selection.start
+ point2 = win.selection.finish
+ end
+
+ local success, err = pcall(function()
+ win.onSelectionEditted(point1, point2, newContent)
+ end)
+
+ if not success and osprint then
+ osprint("ERROR: onSelectionEditted callback failed: " .. tostring(err) .. "\n")
+ end
+
+ -- Clear selection after edit
+ win.selection = nil
+ win.dirty = true
+ return true
+ end
+ end
+
-- Call the window's input callback if it exists
- if sys.activeWindow.onInput then
+ if win.onInput then
local success, err = pcall(function()
- sys.activeWindow.onInput(key, scancode)
+ win.onInput(key, scancode)
end)
if not success and osprint then
diff --git a/iso_includes/os/postinit.lua b/iso_includes/os/postinit.lua
@@ -336,8 +336,42 @@ if _G.sys and _G.sys.hotkey then
_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
+ -- Check if there's an active selection - use onSelectionEditted
+ local hasSelection = win.selection and win.selection.start and win.selection.finish and
+ win.selection.content and win.selection.content ~= ""
+
+ if hasSelection and win.onSelectionEditted then
+ -- Convert selection indices to x,y coordinates
+ local startText = win.selectableText and win.selectableText[win.selection.start.index]
+ local finishText = win.selectableText and win.selectableText[win.selection.finish.index]
+
+ local point1, point2
+ if startText then
+ local charWidth = 8 * (startText.scale or 1)
+ point1 = {
+ x = startText.x + (win.selection.start.pos - 1) * charWidth,
+ y = startText.y
+ }
+ end
+ if finishText then
+ local charWidth = 8 * (finishText.scale or 1)
+ point2 = {
+ x = finishText.x + (win.selection.finish.pos - 1) * charWidth,
+ y = finishText.y
+ }
+ end
+
+ if point1 and point2 then
+ local success, err = pcall(win.onSelectionEditted, point1, point2, _G.clipboard.content)
+ if not success and osprint then
+ osprint("[CLIPBOARD] onSelectionEditted error: " .. tostring(err) .. "\n")
+ end
+ end
+ -- Clear selection after paste
+ win.selection = nil
+ win.dirty = true
+ elseif win.onPaste then
+ -- Try onPaste
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")