multi-line selection fixed
This commit is contained in:
@@ -2749,35 +2749,24 @@ function Element:draw(backdropCanvas)
|
|||||||
local selectionColor = self.selectionColor or Color.new(0.3, 0.5, 0.8, 0.5)
|
local selectionColor = self.selectionColor or Color.new(0.3, 0.5, 0.8, 0.5)
|
||||||
local selectionWithOpacity = Color.new(selectionColor.r, selectionColor.g, selectionColor.b, selectionColor.a * self.opacity)
|
local selectionWithOpacity = Color.new(selectionColor.r, selectionColor.g, selectionColor.b, selectionColor.a * self.opacity)
|
||||||
|
|
||||||
-- Calculate selection bounds safely
|
-- Get selection rectangles (handles multiline and wrapping)
|
||||||
local beforeSelection = ""
|
local selectionRects = self:_getSelectionRects(selStart, selEnd)
|
||||||
local selectedText = ""
|
|
||||||
|
|
||||||
local startByte = utf8.offset(self.text, selStart + 1)
|
|
||||||
local endByte = utf8.offset(self.text, selEnd + 1)
|
|
||||||
|
|
||||||
if startByte and endByte then
|
|
||||||
beforeSelection = self.text:sub(1, startByte - 1)
|
|
||||||
selectedText = self.text:sub(startByte, endByte - 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
local selX = (tx or contentX) + font:getWidth(beforeSelection)
|
|
||||||
local selWidth = font:getWidth(selectedText)
|
|
||||||
local selY = ty or contentY
|
|
||||||
local selHeight = textHeight
|
|
||||||
|
|
||||||
-- Apply scissor for single-line editable inputs
|
-- Apply scissor for single-line editable inputs
|
||||||
if not self.multiline then
|
if not self.multiline then
|
||||||
love.graphics.setScissor(contentX, contentY, textAreaWidth, textAreaHeight)
|
love.graphics.setScissor(contentX, contentY, textAreaWidth, textAreaHeight)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Draw selection background
|
-- Draw selection background rectangles
|
||||||
love.graphics.setColor(selectionWithOpacity:toRGBA())
|
love.graphics.setColor(selectionWithOpacity:toRGBA())
|
||||||
love.graphics.rectangle("fill", selX, selY, selWidth, selHeight)
|
for _, rect in ipairs(selectionRects) do
|
||||||
|
local rectX = contentX + rect.x
|
||||||
-- Redraw selected text on top
|
local rectY = contentY + rect.y
|
||||||
love.graphics.setColor(textColorWithOpacity:toRGBA())
|
if not self.multiline and self._textScrollX then
|
||||||
love.graphics.print(selectedText, selX, selY)
|
rectX = rectX - self._textScrollX
|
||||||
|
end
|
||||||
|
love.graphics.rectangle("fill", rectX, rectY, rect.width, rect.height)
|
||||||
|
end
|
||||||
|
|
||||||
-- Reset scissor
|
-- Reset scissor
|
||||||
if not self.multiline then
|
if not self.multiline then
|
||||||
@@ -4769,6 +4758,158 @@ function Element:_getCursorScreenPosition()
|
|||||||
return 0, #lines * lineHeight
|
return 0, #lines * lineHeight
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Get selection rectangles for rendering (handles multiline and wrapped text)
|
||||||
|
---@param selStart number -- Selection start position (character index)
|
||||||
|
---@param selEnd number -- Selection end position (character index)
|
||||||
|
---@return table -- Array of rectangles {x, y, width, height} relative to content area
|
||||||
|
function Element:_getSelectionRects(selStart, selEnd)
|
||||||
|
if not self.editable then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local font = self:_getFont()
|
||||||
|
if not font then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local text = self._textBuffer or ""
|
||||||
|
local rects = {}
|
||||||
|
|
||||||
|
-- For single-line text, calculate simple rectangle
|
||||||
|
if not self.multiline then
|
||||||
|
local startByte = utf8.offset(text, selStart + 1)
|
||||||
|
local endByte = utf8.offset(text, selEnd + 1)
|
||||||
|
|
||||||
|
if startByte and endByte then
|
||||||
|
local beforeSelection = text:sub(1, startByte - 1)
|
||||||
|
local selectedText = text:sub(startByte, endByte - 1)
|
||||||
|
local selX = font:getWidth(beforeSelection)
|
||||||
|
local selWidth = font:getWidth(selectedText)
|
||||||
|
local selY = 0
|
||||||
|
local selHeight = font:getHeight()
|
||||||
|
|
||||||
|
table.insert(rects, {x = selX, y = selY, width = selWidth, height = selHeight})
|
||||||
|
end
|
||||||
|
|
||||||
|
return rects
|
||||||
|
end
|
||||||
|
|
||||||
|
-- For multiline text, we need to handle line wrapping
|
||||||
|
self:_updateTextIfDirty()
|
||||||
|
|
||||||
|
-- Get text area width for wrapping
|
||||||
|
local textAreaWidth = self.width
|
||||||
|
local scaledContentPadding = self:getScaledContentPadding()
|
||||||
|
if scaledContentPadding then
|
||||||
|
local borderBoxWidth = self._borderBoxWidth or (self.width + self.padding.left + self.padding.right)
|
||||||
|
textAreaWidth = borderBoxWidth - scaledContentPadding.left - scaledContentPadding.right
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Split text by actual newlines first
|
||||||
|
local lines = {}
|
||||||
|
for line in (text .. "\n"):gmatch("([^\n]*)\n") do
|
||||||
|
table.insert(lines, line)
|
||||||
|
end
|
||||||
|
if #lines == 0 then
|
||||||
|
lines = { "" }
|
||||||
|
end
|
||||||
|
|
||||||
|
local lineHeight = font:getHeight()
|
||||||
|
local charCount = 0
|
||||||
|
local visualLineNum = 0
|
||||||
|
|
||||||
|
for lineNum, line in ipairs(lines) do
|
||||||
|
local lineLength = utf8.len(line) or 0
|
||||||
|
|
||||||
|
-- Check if selection intersects with this line
|
||||||
|
local lineStartChar = charCount
|
||||||
|
local lineEndChar = charCount + lineLength
|
||||||
|
|
||||||
|
if selEnd > lineStartChar and selStart <= lineEndChar then
|
||||||
|
-- Selection intersects with this line
|
||||||
|
local selStartInLine = math.max(0, selStart - charCount)
|
||||||
|
local selEndInLine = math.min(lineLength, selEnd - charCount)
|
||||||
|
|
||||||
|
-- If text wrapping is enabled, handle wrapped segments
|
||||||
|
if self.textWrap and textAreaWidth > 0 then
|
||||||
|
local wrappedSegments = self:_wrapLine(line, textAreaWidth)
|
||||||
|
|
||||||
|
for segmentIdx, segment in ipairs(wrappedSegments) do
|
||||||
|
-- Check if selection intersects with this segment
|
||||||
|
if selEndInLine > segment.startIdx and selStartInLine <= segment.endIdx then
|
||||||
|
-- Selection intersects with this segment
|
||||||
|
local segSelStart = math.max(segment.startIdx, selStartInLine)
|
||||||
|
local segSelEnd = math.min(segment.endIdx, selEndInLine)
|
||||||
|
|
||||||
|
-- Calculate X position and width
|
||||||
|
local beforeText = ""
|
||||||
|
local selectedText = ""
|
||||||
|
|
||||||
|
if segSelStart > segment.startIdx then
|
||||||
|
local startByte = utf8.offset(segment.text, segSelStart - segment.startIdx + 1)
|
||||||
|
if startByte then
|
||||||
|
beforeText = segment.text:sub(1, startByte - 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local selStartByte = utf8.offset(segment.text, segSelStart - segment.startIdx + 1)
|
||||||
|
local selEndByte = utf8.offset(segment.text, segSelEnd - segment.startIdx + 1)
|
||||||
|
if selStartByte and selEndByte then
|
||||||
|
selectedText = segment.text:sub(selStartByte, selEndByte - 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local selX = font:getWidth(beforeText)
|
||||||
|
local selWidth = font:getWidth(selectedText)
|
||||||
|
local selY = visualLineNum * lineHeight
|
||||||
|
local selHeight = lineHeight
|
||||||
|
|
||||||
|
table.insert(rects, {x = selX, y = selY, width = selWidth, height = selHeight})
|
||||||
|
end
|
||||||
|
|
||||||
|
visualLineNum = visualLineNum + 1
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- No wrapping, simple calculation
|
||||||
|
local beforeText = ""
|
||||||
|
local selectedText = ""
|
||||||
|
|
||||||
|
if selStartInLine > 0 then
|
||||||
|
local startByte = utf8.offset(line, selStartInLine + 1)
|
||||||
|
if startByte then
|
||||||
|
beforeText = line:sub(1, startByte - 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local selStartByte = utf8.offset(line, selStartInLine + 1)
|
||||||
|
local selEndByte = utf8.offset(line, selEndInLine + 1)
|
||||||
|
if selStartByte and selEndByte then
|
||||||
|
selectedText = line:sub(selStartByte, selEndByte - 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local selX = font:getWidth(beforeText)
|
||||||
|
local selWidth = font:getWidth(selectedText)
|
||||||
|
local selY = visualLineNum * lineHeight
|
||||||
|
local selHeight = lineHeight
|
||||||
|
|
||||||
|
table.insert(rects, {x = selX, y = selY, width = selWidth, height = selHeight})
|
||||||
|
visualLineNum = visualLineNum + 1
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- Selection doesn't intersect, but we still need to count visual lines
|
||||||
|
if self.textWrap and textAreaWidth > 0 then
|
||||||
|
local wrappedSegments = self:_wrapLine(line, textAreaWidth)
|
||||||
|
visualLineNum = visualLineNum + #wrappedSegments
|
||||||
|
else
|
||||||
|
visualLineNum = visualLineNum + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
charCount = charCount + lineLength + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
return rects
|
||||||
|
end
|
||||||
|
|
||||||
--- Update element height based on text content (for autoGrow multiline fields)
|
--- Update element height based on text content (for autoGrow multiline fields)
|
||||||
function Element:_updateAutoGrowHeight()
|
function Element:_updateAutoGrowHeight()
|
||||||
if not self.editable or not self.multiline or not self.autoGrow then
|
if not self.editable or not self.multiline or not self.autoGrow then
|
||||||
@@ -4845,20 +4986,27 @@ function Element:_mouseToTextPosition(mouseX, mouseY)
|
|||||||
local contentX = (self._absoluteX or self.x) + self.padding.left
|
local contentX = (self._absoluteX or self.x) + self.padding.left
|
||||||
local contentY = (self._absoluteY or self.y) + self.padding.top
|
local contentY = (self._absoluteY or self.y) + self.padding.top
|
||||||
|
|
||||||
-- Calculate relative X position within text area
|
-- Calculate relative position within text area
|
||||||
local relativeX = mouseX - contentX
|
local relativeX = mouseX - contentX
|
||||||
|
local relativeY = mouseY - contentY
|
||||||
-- Account for horizontal scroll offset in single-line inputs
|
|
||||||
if not self.multiline and self._textScrollX then
|
|
||||||
relativeX = relativeX + self._textScrollX
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Get font for measuring text
|
-- Get font for measuring text
|
||||||
local font = self:_getFont()
|
local font = self:_getFont()
|
||||||
|
if not font then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
-- Find the character position closest to the click
|
|
||||||
local text = self._textBuffer
|
local text = self._textBuffer
|
||||||
local textLength = utf8.len(text) or 0
|
local textLength = utf8.len(text) or 0
|
||||||
|
|
||||||
|
-- === SINGLE-LINE TEXT HANDLING ===
|
||||||
|
if not self.multiline then
|
||||||
|
-- Account for horizontal scroll offset in single-line inputs
|
||||||
|
if self._textScrollX then
|
||||||
|
relativeX = relativeX + self._textScrollX
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Find the character position closest to the click
|
||||||
local closestPos = 0
|
local closestPos = 0
|
||||||
local closestDist = math.huge
|
local closestDist = math.huge
|
||||||
|
|
||||||
@@ -4879,6 +5027,96 @@ function Element:_mouseToTextPosition(mouseX, mouseY)
|
|||||||
end
|
end
|
||||||
|
|
||||||
return closestPos
|
return closestPos
|
||||||
|
end
|
||||||
|
|
||||||
|
-- === MULTILINE TEXT HANDLING ===
|
||||||
|
|
||||||
|
-- Update text wrapping if dirty
|
||||||
|
self:_updateTextIfDirty()
|
||||||
|
|
||||||
|
-- Split text into lines
|
||||||
|
local lines = {}
|
||||||
|
for line in (text .. "\n"):gmatch("([^\n]*)\n") do
|
||||||
|
table.insert(lines, line)
|
||||||
|
end
|
||||||
|
if #lines == 0 then
|
||||||
|
lines = { "" }
|
||||||
|
end
|
||||||
|
|
||||||
|
local lineHeight = font:getHeight()
|
||||||
|
|
||||||
|
-- Get text area width for wrapping calculations
|
||||||
|
local textAreaWidth = self.width
|
||||||
|
local scaledContentPadding = self:getScaledContentPadding()
|
||||||
|
if scaledContentPadding then
|
||||||
|
local borderBoxWidth = self._borderBoxWidth or (self.width + self.padding.left + self.padding.right)
|
||||||
|
textAreaWidth = borderBoxWidth - scaledContentPadding.left - scaledContentPadding.right
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Determine which line the click is on based on Y coordinate
|
||||||
|
local clickedLineNum = math.floor(relativeY / lineHeight) + 1
|
||||||
|
clickedLineNum = math.max(1, math.min(clickedLineNum, #lines))
|
||||||
|
|
||||||
|
-- Calculate character offset for lines before the clicked line
|
||||||
|
local charOffset = 0
|
||||||
|
for i = 1, clickedLineNum - 1 do
|
||||||
|
local lineLen = utf8.len(lines[i]) or 0
|
||||||
|
charOffset = charOffset + lineLen + 1 -- +1 for newline character
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Get the clicked line
|
||||||
|
local clickedLine = lines[clickedLineNum]
|
||||||
|
local lineLen = utf8.len(clickedLine) or 0
|
||||||
|
|
||||||
|
-- If text wrapping is enabled, handle wrapped segments
|
||||||
|
if self.textWrap and textAreaWidth > 0 then
|
||||||
|
local wrappedSegments = self:_wrapLine(clickedLine, textAreaWidth)
|
||||||
|
|
||||||
|
-- Determine which wrapped segment was clicked
|
||||||
|
local lineYOffset = (clickedLineNum - 1) * lineHeight
|
||||||
|
local segmentNum = math.floor((relativeY - lineYOffset) / lineHeight) + 1
|
||||||
|
segmentNum = math.max(1, math.min(segmentNum, #wrappedSegments))
|
||||||
|
|
||||||
|
local segment = wrappedSegments[segmentNum]
|
||||||
|
|
||||||
|
-- Find closest position within the segment
|
||||||
|
local segmentText = segment.text
|
||||||
|
local segmentLen = utf8.len(segmentText) or 0
|
||||||
|
local closestPos = segment.startIdx
|
||||||
|
local closestDist = math.huge
|
||||||
|
|
||||||
|
for i = 0, segmentLen do
|
||||||
|
local offset = utf8.offset(segmentText, i + 1)
|
||||||
|
local beforeText = offset and segmentText:sub(1, offset - 1) or segmentText
|
||||||
|
local textWidth = font:getWidth(beforeText)
|
||||||
|
local dist = math.abs(relativeX - textWidth)
|
||||||
|
|
||||||
|
if dist < closestDist then
|
||||||
|
closestDist = dist
|
||||||
|
closestPos = segment.startIdx + i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return charOffset + closestPos
|
||||||
|
end
|
||||||
|
|
||||||
|
-- No wrapping - find closest position in the clicked line
|
||||||
|
local closestPos = 0
|
||||||
|
local closestDist = math.huge
|
||||||
|
|
||||||
|
for i = 0, lineLen do
|
||||||
|
local offset = utf8.offset(clickedLine, i + 1)
|
||||||
|
local beforeText = offset and clickedLine:sub(1, offset - 1) or clickedLine
|
||||||
|
local textWidth = font:getWidth(beforeText)
|
||||||
|
local dist = math.abs(relativeX - textWidth)
|
||||||
|
|
||||||
|
if dist < closestDist then
|
||||||
|
closestDist = dist
|
||||||
|
closestPos = i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return charOffset + closestPos
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Handle mouse click on text (set cursor position or start selection)
|
--- Handle mouse click on text (set cursor position or start selection)
|
||||||
|
|||||||
@@ -1776,5 +1776,313 @@ function TestInputField:testTextScrollWithBackspace()
|
|||||||
lu.assertTrue(element._textScrollX <= initialScroll)
|
lu.assertTrue(element._textScrollX <= initialScroll)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- ====================
|
||||||
|
-- Multiline Text Selection Tests
|
||||||
|
-- ====================
|
||||||
|
|
||||||
|
function TestInputField:testMultilineMouseToTextPositionBasic()
|
||||||
|
local element = FlexLove.Element.new({
|
||||||
|
x = 10,
|
||||||
|
y = 10,
|
||||||
|
width = 300,
|
||||||
|
height = 100,
|
||||||
|
editable = true,
|
||||||
|
multiline = true,
|
||||||
|
text = "Line 1\nLine 2\nLine 3",
|
||||||
|
})
|
||||||
|
|
||||||
|
element:focus()
|
||||||
|
|
||||||
|
-- Get font to calculate positions
|
||||||
|
local font = element:_getFont()
|
||||||
|
local lineHeight = font:getHeight()
|
||||||
|
|
||||||
|
-- Click at start (should be position 0)
|
||||||
|
local pos = element:_mouseToTextPosition(10, 10)
|
||||||
|
lu.assertEquals(pos, 0)
|
||||||
|
|
||||||
|
-- Click on second line start (should be after "Line 1\n" = position 7)
|
||||||
|
pos = element:_mouseToTextPosition(10, 10 + lineHeight)
|
||||||
|
lu.assertEquals(pos, 7)
|
||||||
|
|
||||||
|
-- Click on third line start (should be after "Line 1\nLine 2\n" = position 14)
|
||||||
|
pos = element:_mouseToTextPosition(10, 10 + lineHeight * 2)
|
||||||
|
lu.assertEquals(pos, 14)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestInputField:testMultilineMouseToTextPositionXCoordinate()
|
||||||
|
local element = FlexLove.Element.new({
|
||||||
|
x = 10,
|
||||||
|
y = 10,
|
||||||
|
width = 300,
|
||||||
|
height = 100,
|
||||||
|
editable = true,
|
||||||
|
multiline = true,
|
||||||
|
text = "ABC\nDEF\nGHI",
|
||||||
|
})
|
||||||
|
|
||||||
|
element:focus()
|
||||||
|
|
||||||
|
local font = element:_getFont()
|
||||||
|
local lineHeight = font:getHeight()
|
||||||
|
local charWidth = font:getWidth("A")
|
||||||
|
|
||||||
|
-- Click in middle of first line (should be around position 1-2)
|
||||||
|
local pos = element:_mouseToTextPosition(10 + charWidth * 1.5, 10)
|
||||||
|
lu.assertTrue(pos >= 1 and pos <= 2)
|
||||||
|
|
||||||
|
-- Click at end of second line (should be around position 6-7)
|
||||||
|
-- Text is "ABC\nDEF\nGHI", so second line "DEF" ends at position 6 or 7 (newline)
|
||||||
|
pos = element:_mouseToTextPosition(10 + charWidth * 3, 10 + lineHeight)
|
||||||
|
lu.assertTrue(pos >= 6 and pos <= 7)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestInputField:testMultilineMouseDragSelection()
|
||||||
|
local element = FlexLove.Element.new({
|
||||||
|
x = 10,
|
||||||
|
y = 10,
|
||||||
|
width = 300,
|
||||||
|
height = 100,
|
||||||
|
editable = true,
|
||||||
|
multiline = true,
|
||||||
|
text = "Line 1\nLine 2\nLine 3",
|
||||||
|
})
|
||||||
|
|
||||||
|
element:focus()
|
||||||
|
|
||||||
|
local font = element:_getFont()
|
||||||
|
local lineHeight = font:getHeight()
|
||||||
|
|
||||||
|
-- Simulate mouse click on first line (sets _mouseDownPosition)
|
||||||
|
element:_handleTextClick(10, 10, 1)
|
||||||
|
lu.assertEquals(element._cursorPosition, 0)
|
||||||
|
lu.assertFalse(element:hasSelection())
|
||||||
|
|
||||||
|
-- Drag to second line
|
||||||
|
element:_handleTextDrag(50, 10 + lineHeight)
|
||||||
|
lu.assertTrue(element:hasSelection())
|
||||||
|
|
||||||
|
-- Selection should span from first line to second line
|
||||||
|
local startPos, endPos = element:getSelection()
|
||||||
|
lu.assertTrue(startPos == 0 or endPos == 0)
|
||||||
|
lu.assertTrue(startPos > 6 or endPos > 6) -- Past first newline
|
||||||
|
|
||||||
|
-- After drag, selection should be preserved
|
||||||
|
lu.assertTrue(element:hasSelection())
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestInputField:testMultilineMouseDragAcrossThreeLines()
|
||||||
|
local element = FlexLove.Element.new({
|
||||||
|
x = 10,
|
||||||
|
y = 10,
|
||||||
|
width = 300,
|
||||||
|
height = 150,
|
||||||
|
editable = true,
|
||||||
|
multiline = true,
|
||||||
|
text = "First\nSecond\nThird\nFourth",
|
||||||
|
})
|
||||||
|
|
||||||
|
element:focus()
|
||||||
|
|
||||||
|
local font = element:_getFont()
|
||||||
|
local lineHeight = font:getHeight()
|
||||||
|
|
||||||
|
-- Click on first line, then drag to third line
|
||||||
|
element:_handleTextClick(10, 10, 1)
|
||||||
|
element:_handleTextDrag(50, 10 + lineHeight * 2.5)
|
||||||
|
|
||||||
|
lu.assertTrue(element:hasSelection())
|
||||||
|
local startPos, endPos = element:getSelection()
|
||||||
|
|
||||||
|
-- Should select across multiple lines
|
||||||
|
local minPos = math.min(startPos, endPos)
|
||||||
|
local maxPos = math.max(startPos, endPos)
|
||||||
|
lu.assertEquals(minPos, 0) -- From start
|
||||||
|
lu.assertTrue(maxPos > 12) -- Past "First\nSecond\n"
|
||||||
|
|
||||||
|
-- Selection should persist after drag
|
||||||
|
lu.assertTrue(element:hasSelection())
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestInputField:testMultilineClickOnDifferentLines()
|
||||||
|
local element = FlexLove.Element.new({
|
||||||
|
x = 10,
|
||||||
|
y = 10,
|
||||||
|
width = 300,
|
||||||
|
height = 100,
|
||||||
|
editable = true,
|
||||||
|
multiline = true,
|
||||||
|
text = "AAA\nBBB\nCCC",
|
||||||
|
})
|
||||||
|
|
||||||
|
element:focus()
|
||||||
|
|
||||||
|
local font = element:_getFont()
|
||||||
|
local lineHeight = font:getHeight()
|
||||||
|
|
||||||
|
-- Click on first line
|
||||||
|
element:_handleTextClick(10, 10, 1)
|
||||||
|
local pos1 = element._cursorPosition
|
||||||
|
lu.assertEquals(pos1, 0)
|
||||||
|
|
||||||
|
-- Click on second line
|
||||||
|
element:_handleTextClick(10, 10 + lineHeight, 1)
|
||||||
|
local pos2 = element._cursorPosition
|
||||||
|
lu.assertEquals(pos2, 4) -- After "AAA\n"
|
||||||
|
|
||||||
|
-- Click on third line
|
||||||
|
element:_handleTextClick(10, 10 + lineHeight * 2, 1)
|
||||||
|
local pos3 = element._cursorPosition
|
||||||
|
lu.assertEquals(pos3, 8) -- After "AAA\nBBB\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestInputField:testMultilineSelectionWithKeyboard()
|
||||||
|
local element = FlexLove.Element.new({
|
||||||
|
x = 10,
|
||||||
|
y = 10,
|
||||||
|
width = 300,
|
||||||
|
height = 100,
|
||||||
|
editable = true,
|
||||||
|
multiline = true,
|
||||||
|
text = "Line 1\nLine 2\nLine 3",
|
||||||
|
})
|
||||||
|
|
||||||
|
element:focus()
|
||||||
|
element:setCursorPosition(0)
|
||||||
|
|
||||||
|
-- Mock Shift key
|
||||||
|
local oldIsDown = _G.love.keyboard.isDown
|
||||||
|
_G.love.keyboard.isDown = function(...)
|
||||||
|
local keys = {...}
|
||||||
|
for _, key in ipairs(keys) do
|
||||||
|
if key == "lshift" or key == "rshift" then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test Shift+Right selection (horizontal movement works)
|
||||||
|
element:keypressed("right", nil, false)
|
||||||
|
lu.assertTrue(element:hasSelection())
|
||||||
|
|
||||||
|
local startPos, endPos = element:getSelection()
|
||||||
|
lu.assertTrue(math.abs(endPos - startPos) > 0)
|
||||||
|
|
||||||
|
-- Reset mock
|
||||||
|
_G.love.keyboard.isDown = oldIsDown
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestInputField:testMultilineMouseSelectionPreservedAfterRelease()
|
||||||
|
local element = FlexLove.Element.new({
|
||||||
|
x = 10,
|
||||||
|
y = 10,
|
||||||
|
width = 300,
|
||||||
|
height = 100,
|
||||||
|
editable = true,
|
||||||
|
multiline = true,
|
||||||
|
text = "First line\nSecond line\nThird line",
|
||||||
|
})
|
||||||
|
|
||||||
|
element:focus()
|
||||||
|
|
||||||
|
local font = element:_getFont()
|
||||||
|
local lineHeight = font:getHeight()
|
||||||
|
|
||||||
|
-- Create a selection by dragging
|
||||||
|
element:_handleTextClick(10, 10, 1)
|
||||||
|
element:_handleTextDrag(100, 10 + lineHeight * 1.5)
|
||||||
|
|
||||||
|
local startPos, endPos = element:getSelection()
|
||||||
|
local hadSelection = element:hasSelection()
|
||||||
|
|
||||||
|
lu.assertTrue(hadSelection)
|
||||||
|
|
||||||
|
-- Note: There's no mouse release handler that affects selection
|
||||||
|
-- The drag creates the selection and it persists
|
||||||
|
|
||||||
|
-- Selection should still exist
|
||||||
|
lu.assertTrue(element:hasSelection())
|
||||||
|
local startPos2, endPos2 = element:getSelection()
|
||||||
|
lu.assertEquals(startPos, startPos2)
|
||||||
|
lu.assertEquals(endPos, endPos2)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestInputField:testMultilineClickDoesNotPreserveSelection()
|
||||||
|
local element = FlexLove.Element.new({
|
||||||
|
x = 10,
|
||||||
|
y = 10,
|
||||||
|
width = 300,
|
||||||
|
height = 100,
|
||||||
|
editable = true,
|
||||||
|
multiline = true,
|
||||||
|
text = "First line\nSecond line",
|
||||||
|
})
|
||||||
|
|
||||||
|
element:focus()
|
||||||
|
|
||||||
|
local font = element:_getFont()
|
||||||
|
local lineHeight = font:getHeight()
|
||||||
|
|
||||||
|
-- Create a selection
|
||||||
|
element:setSelection(0, 5)
|
||||||
|
lu.assertTrue(element:hasSelection())
|
||||||
|
|
||||||
|
-- Click somewhere else (should clear selection)
|
||||||
|
element:_handleTextClick(10, 10 + lineHeight, 1)
|
||||||
|
|
||||||
|
-- Selection should be cleared
|
||||||
|
lu.assertFalse(element:hasSelection())
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestInputField:testMultilineEmptyLinesHandling()
|
||||||
|
local element = FlexLove.Element.new({
|
||||||
|
x = 10,
|
||||||
|
y = 10,
|
||||||
|
width = 300,
|
||||||
|
height = 150,
|
||||||
|
editable = true,
|
||||||
|
multiline = true,
|
||||||
|
text = "Line 1\n\nLine 3",
|
||||||
|
})
|
||||||
|
|
||||||
|
element:focus()
|
||||||
|
|
||||||
|
local font = element:_getFont()
|
||||||
|
local lineHeight = font:getHeight()
|
||||||
|
|
||||||
|
-- Click on empty line (second line)
|
||||||
|
local pos = element:_mouseToTextPosition(10, 10 + lineHeight)
|
||||||
|
lu.assertEquals(pos, 7) -- After "Line 1\n"
|
||||||
|
|
||||||
|
-- Click on third line
|
||||||
|
pos = element:_mouseToTextPosition(10, 10 + lineHeight * 2)
|
||||||
|
lu.assertEquals(pos, 8) -- After "Line 1\n\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestInputField:testMultilineYCoordinateBeyondText()
|
||||||
|
local element = FlexLove.Element.new({
|
||||||
|
x = 10,
|
||||||
|
y = 10,
|
||||||
|
width = 300,
|
||||||
|
height = 200,
|
||||||
|
editable = true,
|
||||||
|
multiline = true,
|
||||||
|
text = "Line 1\nLine 2",
|
||||||
|
})
|
||||||
|
|
||||||
|
element:focus()
|
||||||
|
|
||||||
|
local font = element:_getFont()
|
||||||
|
local lineHeight = font:getHeight()
|
||||||
|
|
||||||
|
-- Click way below the text (should clamp to last line)
|
||||||
|
local pos = element:_mouseToTextPosition(10, 10 + lineHeight * 10)
|
||||||
|
local textLen = utf8.len(element.text)
|
||||||
|
|
||||||
|
-- Should be at or near end of text
|
||||||
|
lu.assertTrue(pos >= textLen - 6) -- Within last line
|
||||||
|
end
|
||||||
|
|
||||||
-- Run tests
|
-- Run tests
|
||||||
lu.LuaUnit.run()
|
lu.LuaUnit.run()
|
||||||
|
|||||||
Reference in New Issue
Block a user