diff --git a/modules/Element.lua b/modules/Element.lua index 70a3a71..2c7fa03 100644 --- a/modules/Element.lua +++ b/modules/Element.lua @@ -2459,59 +2459,6 @@ function Element:replaceText(startPos, endPos, newText) end end ---- Split text into lines (for multi-line text) -function Element:_splitLines() - if not self.editable then - return - end - - if not self.multiline then - self._lines = { self._textBuffer or "" } - return - end - - self._lines = {} - local text = self._textBuffer or "" - - -- Split on newlines - for line in (text .. "\n"):gmatch("([^\n]*)\n") do - table.insert(self._lines, line) - end - - -- Ensure at least one line - if #self._lines == 0 then - self._lines = { "" } - end -end - ---- Calculate text wrapping -function Element:_calculateWrapping() - if not self.editable or not self.textWrap then - self._wrappedLines = nil - return - end - - self._wrappedLines = {} - local availableWidth = self.width - self.padding.left - self.padding.right - - for lineNum, line in ipairs(self._lines or {}) do - if line == "" then - table.insert(self._wrappedLines, { - text = "", - startIdx = 0, - endIdx = 0, - lineNum = lineNum, - }) - else - local wrappedParts = self:_wrapLine(line, availableWidth) - for _, part in ipairs(wrappedParts) do - part.lineNum = lineNum - table.insert(self._wrappedLines, part) - end - end - end -end - --- Wrap a single line of text ---@param line string -- Line to wrap ---@param maxWidth number -- Maximum width in pixels @@ -2529,154 +2476,6 @@ end -- Input Handling - Mouse Selection -- ==================== ---- Convert mouse coordinates to cursor position in text ----@param mouseX number -- Mouse X coordinate (absolute) ----@param mouseY number -- Mouse Y coordinate (absolute) ----@return number -- Cursor position (character index) -function Element:_mouseToTextPosition(mouseX, mouseY) - if not self.editable or not self._textBuffer then - return 0 - end - - -- Get content area bounds - local contentX = (self._absoluteX or self.x) + self.padding.left - local contentY = (self._absoluteY or self.y) + self.padding.top - - -- Calculate relative position within text area - local relativeX = mouseX - contentX - local relativeY = mouseY - contentY - - -- Get font for measuring text - local font = self:_getFont() - if not font then - return 0 - end - - local text = self._textBuffer - 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 closestDist = math.huge - - -- Check each position in the text - for i = 0, textLength do - -- Get text up to this position - local offset = utf8.offset(text, i + 1) - local beforeText = offset and text:sub(1, offset - 1) or text - local textWidth = font:getWidth(beforeText) - - -- Calculate distance from click to this position - local dist = math.abs(relativeX - textWidth) - - if dist < closestDist then - closestDist = dist - closestPos = i - end - end - - return closestPos - end - - -- === MULTILINE TEXT HANDLING === - - -- Update text wrapping if dirty - if self._textEditor then - self._textEditor:_updateTextIfDirty() - end - - -- 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 - --- Handle mouse click on text (set cursor position or start selection) ---@param mouseX number -- Mouse X coordinate ---@param mouseY number -- Mouse Y coordinate diff --git a/testing/__tests__/33_input_field_tests.lua b/testing/__tests__/33_input_field_tests.lua index 28a63f4..0bb361f 100644 --- a/testing/__tests__/33_input_field_tests.lua +++ b/testing/__tests__/33_input_field_tests.lua @@ -690,12 +690,12 @@ function TestInputField:testMultilineTextSplitting() }) -- Trigger line splitting - element:_splitLines() + element._textEditor:_splitLines() - lu.assertEquals(#element._lines, 3) - lu.assertEquals(element._lines[1], "Line 1") - lu.assertEquals(element._lines[2], "Line 2") - lu.assertEquals(element._lines[3], "Line 3") + lu.assertEquals(#element._textEditor._lines, 3) + lu.assertEquals(element._textEditor._lines[1], "Line 1") + lu.assertEquals(element._textEditor._lines[2], "Line 2") + lu.assertEquals(element._textEditor._lines[3], "Line 3") end -- ==================== @@ -1155,11 +1155,11 @@ function TestInputField:testMouseToTextPosition() element:focus() -- Test conversion at start of element - local pos = element:_mouseToTextPosition(10, 10) + local pos = element._textEditor:mouseToTextPosition(10, 10) lu.assertEquals(pos, 0) -- Test conversion far to the right (should be at end) - pos = element:_mouseToTextPosition(200, 10) + pos = element._textEditor:mouseToTextPosition(200, 10) lu.assertEquals(pos, 5) -- "Hello" has 5 characters end @@ -1822,15 +1822,15 @@ function TestInputField:testMultilineMouseToTextPositionBasic() local lineHeight = font:getHeight() -- Click at start (should be position 0) - local pos = element:_mouseToTextPosition(10, 10) + local pos = element._textEditor: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) + pos = element._textEditor: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) + pos = element._textEditor:mouseToTextPosition(10, 10 + lineHeight * 2) lu.assertEquals(pos, 14) end @@ -1852,12 +1852,12 @@ function TestInputField:testMultilineMouseToTextPositionXCoordinate() 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) + local pos = element._textEditor: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) + pos = element._textEditor:mouseToTextPosition(10 + charWidth * 3, 10 + lineHeight) lu.assertTrue(pos >= 6 and pos <= 7) end @@ -2076,11 +2076,11 @@ function TestInputField:testMultilineEmptyLinesHandling() local lineHeight = font:getHeight() -- Click on empty line (second line) - local pos = element:_mouseToTextPosition(10, 10 + lineHeight) + local pos = element._textEditor:mouseToTextPosition(10, 10 + lineHeight) lu.assertEquals(pos, 7) -- After "Line 1\n" -- Click on third line - pos = element:_mouseToTextPosition(10, 10 + lineHeight * 2) + pos = element._textEditor:mouseToTextPosition(10, 10 + lineHeight * 2) lu.assertEquals(pos, 8) -- After "Line 1\n\n" end @@ -2101,7 +2101,7 @@ function TestInputField:testMultilineYCoordinateBeyondText() local lineHeight = font:getHeight() -- Click way below the text (should clamp to last line) - local pos = element:_mouseToTextPosition(10, 10 + lineHeight * 10) + local pos = element._textEditor:mouseToTextPosition(10, 10 + lineHeight * 10) local textLen = utf8.len(element.text) -- Should be at or near end of text