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._lines = nil -- Split lines (for multiline)
|
||||||
self._wrappedLines = nil -- Wrapped line data
|
self._wrappedLines = nil -- Wrapped line data
|
||||||
self._textDirty = true -- Flag to recalculate lines/wrapping
|
self._textDirty = true -- Flag to recalculate lines/wrapping
|
||||||
|
|
||||||
|
-- Scroll state for text overflow
|
||||||
|
self._textScrollX = 0 -- Horizontal scroll offset in pixels
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Set parent first so it's available for size calculations
|
-- Set parent first so it's available for size calculations
|
||||||
@@ -2679,7 +2682,23 @@ function Element:draw(backdropCanvas)
|
|||||||
tx = contentX
|
tx = contentX
|
||||||
ty = contentY
|
ty = contentY
|
||||||
end
|
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)
|
love.graphics.print(displayText, tx, ty)
|
||||||
|
|
||||||
|
-- Reset scissor
|
||||||
|
if self.editable and not self.multiline then
|
||||||
|
love.graphics.setScissor()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Draw cursor for focused editable elements (even if text is empty)
|
-- 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 cursorY = ty or contentY
|
||||||
local cursorHeight = textHeight
|
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
|
-- Draw cursor line
|
||||||
love.graphics.rectangle("fill", cursorX, cursorY, 2, cursorHeight)
|
love.graphics.rectangle("fill", cursorX, cursorY, 2, cursorHeight)
|
||||||
|
|
||||||
|
-- Reset scissor
|
||||||
|
if not self.multiline then
|
||||||
|
love.graphics.setScissor()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Draw selection highlight for editable elements
|
-- Draw selection highlight for editable elements
|
||||||
@@ -2727,6 +2756,11 @@ function Element:draw(backdropCanvas)
|
|||||||
local selY = ty or contentY
|
local selY = ty or contentY
|
||||||
local selHeight = textHeight
|
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
|
-- Draw selection background
|
||||||
love.graphics.setColor(selectionWithOpacity:toRGBA())
|
love.graphics.setColor(selectionWithOpacity:toRGBA())
|
||||||
love.graphics.rectangle("fill", selX, selY, selWidth, selHeight)
|
love.graphics.rectangle("fill", selX, selY, selWidth, selHeight)
|
||||||
@@ -2734,6 +2768,11 @@ function Element:draw(backdropCanvas)
|
|||||||
-- Redraw selected text on top
|
-- Redraw selected text on top
|
||||||
love.graphics.setColor(textColorWithOpacity:toRGBA())
|
love.graphics.setColor(textColorWithOpacity:toRGBA())
|
||||||
love.graphics.print(selectedText, selX, selY)
|
love.graphics.print(selectedText, selX, selY)
|
||||||
|
|
||||||
|
-- Reset scissor
|
||||||
|
if not self.multiline then
|
||||||
|
love.graphics.setScissor()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if self.textSize then
|
if self.textSize then
|
||||||
@@ -4019,6 +4058,56 @@ function Element:_resetCursorBlink()
|
|||||||
end
|
end
|
||||||
self._cursorBlinkTimer = 0
|
self._cursorBlinkTimer = 0
|
||||||
self._cursorVisible = true
|
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
|
end
|
||||||
|
|
||||||
-- ====================
|
-- ====================
|
||||||
|
|||||||
@@ -1464,5 +1464,169 @@ function TestInputField:testWordNavigationWithPunctuation()
|
|||||||
_G.love.keyboard.isDown = oldIsDown
|
_G.love.keyboard.isDown = oldIsDown
|
||||||
end
|
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
|
-- Run tests
|
||||||
lu.LuaUnit.run()
|
lu.LuaUnit.run()
|
||||||
|
|||||||
Reference in New Issue
Block a user