respect bounds

This commit is contained in:
Michael Freno
2025-11-07 14:21:09 -05:00
parent d2f205edd5
commit 092044cfd7
3 changed files with 374 additions and 0 deletions

View 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

View File

@@ -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
-- ==================== -- ====================

View File

@@ -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()