diff --git a/examples/16-full-imput-demo.lua b/examples/16-full-imput-demo.lua new file mode 100644 index 0000000..38b0f38 --- /dev/null +++ b/examples/16-full-imput-demo.lua @@ -0,0 +1,121 @@ +--[[ +InputFieldsDemo.lua +Simple input field demo - multiple fields to test all features +Uses retained mode - elements are created once and reused +--]] + +local FlexLove = require("FlexLove") +local Element = FlexLove.Element +local Color = FlexLove.Color + +local InputFieldsDemo = {} + +-- Elements (created once) +local elements = {} +local initialized = false + +-- Initialize elements once +local function initialize() + if initialized then + return + end + initialized = true + + -- Title + elements.title = Element.new({ + x = 50, + y = 50, + width = 700, + height = 40, + text = "FlexLove Input Field Demo", + textSize = 28, + textColor = Color.new(1, 1, 1, 1), + z = 1000, + }) + + -- Input field 1 - Empty with placeholder + elements.inputField1 = Element.new({ + x = 50, + y = 120, + width = 600, + height = 50, + editable = true, + text = "", + textSize = 18, + textColor = Color.new(1, 1, 1, 1), + backgroundColor = Color.new(0.2, 0.2, 0.3, 0.9), + cornerRadius = 8, + padding = { horizontal = 15, vertical = 12 }, + placeholder = "Type here... (empty field with placeholder)", + selectOnFocus = false, + z = 1000, + }) + + elements.inputField1.onTextChange = function(element, newText) + print("Field 1 changed:", newText) + end + + -- Input field 2 - Pre-filled with selectOnFocus + elements.inputField2 = Element.new({ + x = 50, + y = 200, + width = 600, + height = 50, + editable = true, + text = "Pre-filled text", + textSize = 18, + textColor = Color.new(1, 1, 1, 1), + backgroundColor = Color.new(0.2, 0.3, 0.2, 0.9), + cornerRadius = 8, + padding = { horizontal = 15, vertical = 12 }, + placeholder = "This shouldn't show", + selectOnFocus = true, + z = 1000, + }) + + elements.inputField2.onTextChange = function(element, newText) + print("Field 2 changed:", newText) + end + + -- Input field 3 - With max length + elements.inputField3 = Element.new({ + x = 50, + y = 280, + width = 600, + height = 50, + editable = true, + text = "", + textSize = 18, + textColor = Color.new(1, 1, 1, 1), + backgroundColor = Color.new(0.3, 0.2, 0.2, 0.9), + cornerRadius = 8, + padding = { horizontal = 15, vertical = 12 }, + placeholder = "Max 20 characters", + maxLength = 20, + selectOnFocus = false, + z = 1000, + }) + + elements.inputField3.onTextChange = function(element, newText) + print("Field 3 changed:", newText) + end + + -- Instructions + elements.instructions = Element.new({ + x = 50, + y = 360, + width = 700, + height = 200, + text = "Instructions:\n• Click on a field to focus it\n• Type to enter text\n• Field 1: Empty with placeholder\n• Field 2: Pre-filled, selects all on focus\n• Field 3: Max 20 characters\n• Press ESC to unfocus\n• Use arrow keys to move cursor", + textSize = 14, + textColor = Color.new(0.8, 0.8, 0.8, 1), + z = 1000, + }) +end + +-- Render function (just initializes if needed) +function InputFieldsDemo.render() + initialize() +end + +return InputFieldsDemo diff --git a/modules/Element.lua b/modules/Element.lua index 9498cd7..2d2e3df 100644 --- a/modules/Element.lua +++ b/modules/Element.lua @@ -338,6 +338,9 @@ function Element.new(props) self._lines = nil -- Split lines (for multiline) self._wrappedLines = nil -- Wrapped line data self._textDirty = true -- Flag to recalculate lines/wrapping + + -- Scroll state for text overflow + self._textScrollX = 0 -- Horizontal scroll offset in pixels end -- Set parent first so it's available for size calculations @@ -2679,7 +2682,23 @@ function Element:draw(backdropCanvas) tx = contentX ty = contentY end + + -- Apply scroll offset for editable single-line inputs + if self.editable and not self.multiline and self._textScrollX then + tx = tx - self._textScrollX + end + + -- Use scissor to clip text to content area for editable inputs + if self.editable and not self.multiline then + love.graphics.setScissor(contentX, contentY, textAreaWidth, textAreaHeight) + end + love.graphics.print(displayText, tx, ty) + + -- Reset scissor + if self.editable and not self.multiline then + love.graphics.setScissor() + end end -- Draw cursor for focused editable elements (even if text is empty) @@ -2700,8 +2719,18 @@ function Element:draw(backdropCanvas) local cursorY = ty or contentY local cursorHeight = textHeight + -- Apply scissor for single-line editable inputs + if not self.multiline then + love.graphics.setScissor(contentX, contentY, textAreaWidth, textAreaHeight) + end + -- Draw cursor line love.graphics.rectangle("fill", cursorX, cursorY, 2, cursorHeight) + + -- Reset scissor + if not self.multiline then + love.graphics.setScissor() + end end -- Draw selection highlight for editable elements @@ -2727,6 +2756,11 @@ function Element:draw(backdropCanvas) local selY = ty or contentY local selHeight = textHeight + -- Apply scissor for single-line editable inputs + if not self.multiline then + love.graphics.setScissor(contentX, contentY, textAreaWidth, textAreaHeight) + end + -- Draw selection background love.graphics.setColor(selectionWithOpacity:toRGBA()) love.graphics.rectangle("fill", selX, selY, selWidth, selHeight) @@ -2734,6 +2768,11 @@ function Element:draw(backdropCanvas) -- Redraw selected text on top love.graphics.setColor(textColorWithOpacity:toRGBA()) love.graphics.print(selectedText, selX, selY) + + -- Reset scissor + if not self.multiline then + love.graphics.setScissor() + end end if self.textSize then @@ -4019,6 +4058,56 @@ function Element:_resetCursorBlink() end self._cursorBlinkTimer = 0 self._cursorVisible = true + + -- Update scroll to keep cursor visible + self:_updateTextScroll() +end + +--- Update text scroll offset to keep cursor visible +function Element:_updateTextScroll() + if not self.editable or self.multiline then + return + end + + -- Get font for measuring text + local font = self:_getFont() + if not font then + return + end + + -- Calculate cursor X position in text coordinates + local cursorText = "" + if self._textBuffer and self._textBuffer ~= "" and self._cursorPosition > 0 then + local byteOffset = utf8.offset(self._textBuffer, self._cursorPosition + 1) + if byteOffset then + cursorText = self._textBuffer:sub(1, byteOffset - 1) + end + end + local cursorX = font:getWidth(cursorText) + + -- Get available text area width (accounting for padding) + 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 + + -- Add some padding on the right for the cursor + local cursorPadding = 4 + local visibleWidth = textAreaWidth - cursorPadding + + -- Adjust scroll to keep cursor visible + if cursorX - self._textScrollX < 0 then + -- Cursor is to the left of visible area - scroll left + self._textScrollX = cursorX + elseif cursorX - self._textScrollX > visibleWidth then + -- Cursor is to the right of visible area - scroll right + self._textScrollX = cursorX - visibleWidth + end + + -- Ensure we don't scroll past the beginning + self._textScrollX = math.max(0, self._textScrollX) end -- ==================== diff --git a/testing/__tests__/33_input_field_tests.lua b/testing/__tests__/33_input_field_tests.lua index 4672914..c87ba0b 100644 --- a/testing/__tests__/33_input_field_tests.lua +++ b/testing/__tests__/33_input_field_tests.lua @@ -1464,5 +1464,169 @@ function TestInputField:testWordNavigationWithPunctuation() _G.love.keyboard.isDown = oldIsDown end +-- ==================== +-- Text Scrolling Tests +-- ==================== + +function TestInputField:testTextScrollInitiallyZero() + local element = FlexLove.Element.new({ + x = 10, + y = 10, + width = 50, -- Small width to force scrolling + height = 30, + editable = true, + text = "", + }) + + element:focus() + + -- Initial scroll should be 0 + lu.assertEquals(element._textScrollX, 0) +end + +function TestInputField:testTextScrollUpdatesOnCursorMove() + local element = FlexLove.Element.new({ + x = 10, + y = 10, + width = 50, -- Small width to force scrolling + height = 30, + editable = true, + text = "This is a very long text that will overflow", + }) + + element:focus() + element:setCursorPosition(0) + + -- Scroll should be 0 at start + lu.assertEquals(element._textScrollX, 0) + + -- Move cursor to end + element:moveCursorToEnd() + + -- Scroll should have increased to keep cursor visible + lu.assertTrue(element._textScrollX > 0) +end + +function TestInputField:testTextScrollKeepsCursorVisible() + local element = FlexLove.Element.new({ + x = 10, + y = 10, + width = 50, -- Small width + height = 30, + editable = true, + text = "", + }) + + element:focus() + element:setCursorPosition(0) + + -- Set long text directly + element:setText("This is a very long text that will definitely overflow the bounds") + element:moveCursorToEnd() + + -- Cursor should be at end and scroll should be adjusted + lu.assertTrue(element._textScrollX > 0) + + -- Move cursor back to start + element:moveCursorToStart() + + -- Scroll should reset to 0 + lu.assertEquals(element._textScrollX, 0) +end + +function TestInputField:testTextScrollWithSelection() + local element = FlexLove.Element.new({ + x = 10, + y = 10, + width = 50, + height = 30, + editable = true, + text = "This is a very long text for testing", + }) + + element:focus() + element:setCursorPosition(0) + + -- Move to end and check scroll + element:moveCursorToEnd() + local scrollAtEnd = element._textScrollX + lu.assertTrue(scrollAtEnd > 0) + + -- Select from end backwards + element:setSelection(20, 37) + element._cursorPosition = 20 + element:_updateTextScroll() + + -- Scroll should adjust to show cursor at position 20 + lu.assertTrue(element._textScrollX < scrollAtEnd) +end + +function TestInputField:testTextScrollDoesNotAffectMultiline() + local element = FlexLove.Element.new({ + x = 10, + y = 10, + width = 50, + height = 60, + editable = true, + multiline = true, + text = "This is a very long text", + }) + + element:focus() + element:moveCursorToEnd() + + -- Multiline should not use horizontal scroll + lu.assertEquals(element._textScrollX, 0) +end + +function TestInputField:testTextScrollResetsOnClear() + local element = FlexLove.Element.new({ + x = 10, + y = 10, + width = 50, + height = 30, + editable = true, + text = "This is a very long text that overflows", + }) + + element:focus() + element:moveCursorToEnd() + + -- Should have scrolled + lu.assertTrue(element._textScrollX > 0) + + -- Clear text + element:setText("") + element:setCursorPosition(0) + + -- Scroll should reset + lu.assertEquals(element._textScrollX, 0) +end + +function TestInputField:testTextScrollWithBackspace() + local element = FlexLove.Element.new({ + x = 10, + y = 10, + width = 50, + height = 30, + editable = true, + text = "XXXXXXXXXXXXXXXXXXXXXXXXXX", -- Long text + }) + + element:focus() + element:moveCursorToEnd() + + local initialScroll = element._textScrollX + lu.assertTrue(initialScroll > 0) + + -- Delete characters from end + element:keypressed("backspace", nil, false) + element:keypressed("backspace", nil, false) + element:keypressed("backspace", nil, false) + + -- Scroll should decrease as text gets shorter + lu.assertTrue(element._textScrollX <= initialScroll) +end + -- Run tests lu.LuaUnit.run()