respect bounds
This commit is contained in:
121
examples/16-full-imput-demo.lua
Normal file
121
examples/16-full-imput-demo.lua
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
-- ====================
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user