stable id - fixes input for immediate mode
This commit is contained in:
@@ -191,7 +191,7 @@ function Element.new(props)
|
||||
|
||||
-- Auto-generate ID in immediate mode if not provided
|
||||
if Gui._immediateMode and (not props.id or props.id == "") then
|
||||
self.id = StateManager.generateID(props)
|
||||
self.id = StateManager.generateID(props, props.parent)
|
||||
else
|
||||
self.id = props.id or ""
|
||||
end
|
||||
@@ -346,6 +346,36 @@ function Element.new(props)
|
||||
|
||||
-- Scroll state for text overflow
|
||||
self._textScrollX = 0 -- Horizontal scroll offset in pixels
|
||||
|
||||
-- Restore state from StateManager in immediate mode
|
||||
if Gui._immediateMode and self._stateId then
|
||||
local state = StateManager.getState(self._stateId)
|
||||
if state then
|
||||
-- Restore focus state
|
||||
if state._focused then
|
||||
self._focused = true
|
||||
Gui._focusedElement = self
|
||||
end
|
||||
|
||||
-- Restore text buffer (prefer state over props for immediate mode)
|
||||
if state._textBuffer and state._textBuffer ~= "" then
|
||||
self._textBuffer = state._textBuffer
|
||||
end
|
||||
|
||||
-- Restore cursor position
|
||||
if state._cursorPosition then
|
||||
self._cursorPosition = state._cursorPosition
|
||||
end
|
||||
|
||||
-- Restore selection
|
||||
if state._selectionStart then
|
||||
self._selectionStart = state._selectionStart
|
||||
end
|
||||
if state._selectionEnd then
|
||||
self._selectionEnd = state._selectionEnd
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Set parent first so it's available for size calculations
|
||||
@@ -396,7 +426,18 @@ function Element.new(props)
|
||||
}
|
||||
end
|
||||
|
||||
self.text = props.text
|
||||
-- For editable elements, default text to empty string if not provided
|
||||
if self.editable and props.text == nil then
|
||||
self.text = ""
|
||||
else
|
||||
self.text = props.text
|
||||
end
|
||||
|
||||
-- Sync self.text with restored _textBuffer for editable elements in immediate mode
|
||||
if self.editable and Gui._immediateMode and self._textBuffer then
|
||||
self.text = self._textBuffer
|
||||
end
|
||||
|
||||
self.textAlign = props.textAlign or TextAlign.START
|
||||
|
||||
-- Image properties
|
||||
@@ -4068,8 +4109,9 @@ function Element:_validateCursorPosition()
|
||||
if not self.editable then
|
||||
return
|
||||
end
|
||||
local textLength = utf8.len(self._textBuffer or "")
|
||||
self._cursorPosition = math.max(0, math.min(self._cursorPosition, textLength))
|
||||
local textLength = utf8.len(self._textBuffer or "") or 0
|
||||
local cursorPos = tonumber(self._cursorPosition) or 0
|
||||
self._cursorPosition = math.max(0, math.min(cursorPos, textLength))
|
||||
end
|
||||
|
||||
--- Reset cursor blink (show cursor immediately)
|
||||
@@ -4239,6 +4281,10 @@ function Element:deleteSelection()
|
||||
self:clearSelection()
|
||||
self._cursorPosition = startPos
|
||||
self:_validateCursorPosition()
|
||||
|
||||
-- Save state to StateManager in immediate mode
|
||||
self:_saveEditableState()
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -4272,6 +4318,9 @@ function Element:focus()
|
||||
if self.onFocus then
|
||||
self.onFocus(self)
|
||||
end
|
||||
|
||||
-- Save state to StateManager in immediate mode
|
||||
self:_saveEditableState()
|
||||
end
|
||||
|
||||
--- Remove focus from this element
|
||||
@@ -4291,6 +4340,9 @@ function Element:blur()
|
||||
if self.onBlur then
|
||||
self.onBlur(self)
|
||||
end
|
||||
|
||||
-- Save state to StateManager in immediate mode
|
||||
self:_saveEditableState()
|
||||
end
|
||||
|
||||
--- Check if this element is focused
|
||||
@@ -4302,6 +4354,21 @@ function Element:isFocused()
|
||||
return self._focused == true
|
||||
end
|
||||
|
||||
--- Save editable element state to StateManager (for immediate mode)
|
||||
function Element:_saveEditableState()
|
||||
if not self.editable or not self._stateId or not Gui._immediateMode then
|
||||
return
|
||||
end
|
||||
|
||||
StateManager.updateState(self._stateId, {
|
||||
_focused = self._focused,
|
||||
_textBuffer = self._textBuffer,
|
||||
_cursorPosition = self._cursorPosition,
|
||||
_selectionStart = self._selectionStart,
|
||||
_selectionEnd = self._selectionEnd,
|
||||
})
|
||||
end
|
||||
|
||||
-- ====================
|
||||
-- Input Handling - Text Buffer Management
|
||||
-- ====================
|
||||
@@ -4329,6 +4396,9 @@ function Element:setText(text)
|
||||
self:_updateTextIfDirty() -- Update immediately to recalculate lines/wrapping
|
||||
self:_updateAutoGrowHeight() -- Then update height based on new content
|
||||
self:_validateCursorPosition()
|
||||
|
||||
-- Save state to StateManager in immediate mode
|
||||
self:_saveEditableState()
|
||||
end
|
||||
|
||||
--- Insert text at position
|
||||
@@ -4369,6 +4439,9 @@ function Element:insertText(text, position)
|
||||
self:_updateTextIfDirty() -- Update immediately to recalculate lines/wrapping
|
||||
self:_updateAutoGrowHeight() -- Then update height based on new content
|
||||
self:_validateCursorPosition()
|
||||
|
||||
-- Save state to StateManager in immediate mode
|
||||
self:_saveEditableState()
|
||||
end
|
||||
|
||||
---@param startPos number -- Start position (inclusive)
|
||||
@@ -4402,6 +4475,9 @@ function Element:deleteText(startPos, endPos)
|
||||
self:_markTextDirty()
|
||||
self:_updateTextIfDirty() -- Update immediately to recalculate lines/wrapping
|
||||
self:_updateAutoGrowHeight() -- Then update height based on new content
|
||||
|
||||
-- Save state to StateManager in immediate mode
|
||||
self:_saveEditableState()
|
||||
end
|
||||
|
||||
--- Replace text in range
|
||||
@@ -5272,6 +5348,9 @@ function Element:textinput(text)
|
||||
if self.onTextChange and self._textBuffer ~= oldText then
|
||||
self.onTextChange(self, self._textBuffer, oldText)
|
||||
end
|
||||
|
||||
-- Save state to StateManager in immediate mode
|
||||
self:_saveEditableState()
|
||||
end
|
||||
|
||||
--- Handle key press (special keys)
|
||||
@@ -5510,6 +5589,9 @@ function Element:keypressed(key, scancode, isrepeat)
|
||||
end
|
||||
self:_resetCursorBlink()
|
||||
end
|
||||
|
||||
-- Save state to StateManager in immediate mode
|
||||
self:_saveEditableState()
|
||||
end
|
||||
|
||||
return Element
|
||||
|
||||
@@ -131,8 +131,8 @@ function GuiState.getTopElementAt(x, y)
|
||||
local function findInteractiveAncestor(elem)
|
||||
local current = elem
|
||||
while current do
|
||||
-- An element is interactive if it has a callback or themeComponent
|
||||
if current.callback or current.themeComponent then
|
||||
-- An element is interactive if it has a callback, themeComponent, or is editable
|
||||
if current.callback or current.themeComponent or current.editable then
|
||||
return current
|
||||
end
|
||||
current = current.parent
|
||||
|
||||
@@ -69,6 +69,13 @@ local function hashProps(props, visited, depth)
|
||||
onTextChange = true,
|
||||
onEnter = true,
|
||||
userdata = true,
|
||||
-- Dynamic input/state properties that should not affect ID stability
|
||||
text = true, -- Text content changes as user types
|
||||
placeholder = true, -- Placeholder text is presentational
|
||||
editable = true, -- Editable state can be toggled dynamically
|
||||
selectOnFocus = true, -- Input behavior flag
|
||||
autoGrow = true, -- Auto-grow behavior flag
|
||||
passwordMode = true, -- Password mode can be toggled
|
||||
}
|
||||
|
||||
-- Collect and sort keys for consistent ordering
|
||||
@@ -96,8 +103,9 @@ end
|
||||
|
||||
--- Generate a unique ID from call site and properties
|
||||
---@param props table|nil Optional properties to include in ID generation
|
||||
---@param parent table|nil Optional parent element for tree-based ID generation
|
||||
---@return string
|
||||
function StateManager.generateID(props)
|
||||
function StateManager.generateID(props, parent)
|
||||
-- Get call stack information
|
||||
local info = debug.getinfo(3, "Sl") -- Level 3: caller of Element.new -> caller of generateID
|
||||
|
||||
@@ -109,16 +117,43 @@ function StateManager.generateID(props)
|
||||
local source = info.source or "unknown"
|
||||
local line = info.currentline or 0
|
||||
|
||||
-- Create ID from source file and line number
|
||||
local baseID = source:match("([^/\\]+)$") or source -- Get filename
|
||||
baseID = baseID:gsub("%.lua$", "") -- Remove .lua extension
|
||||
local locationKey = baseID .. "_L" .. line
|
||||
-- Create base location key from source file and line number
|
||||
local filename = source:match("([^/\\]+)$") or source -- Get filename
|
||||
filename = filename:gsub("%.lua$", "") -- Remove .lua extension
|
||||
local locationKey = filename .. "_L" .. line
|
||||
|
||||
-- If we have a parent, use tree-based ID generation for stability
|
||||
if parent and parent.id and parent.id ~= "" then
|
||||
-- Count how many children the parent currently has
|
||||
-- This gives us a stable sibling index
|
||||
local siblingIndex = #(parent.children or {})
|
||||
|
||||
-- Generate ID based on parent ID + sibling position (NO line number for stability)
|
||||
-- This ensures the same position in the tree always gets the same ID
|
||||
local baseID = parent.id .. "_child" .. siblingIndex
|
||||
|
||||
-- Add property hash if provided (for additional differentiation at same position)
|
||||
if props then
|
||||
local propHash = hashProps(props)
|
||||
if propHash ~= "" then
|
||||
-- Use first 8 chars of a simple hash
|
||||
local hash = 0
|
||||
for i = 1, #propHash do
|
||||
hash = (hash * 31 + string.byte(propHash, i)) % 1000000
|
||||
end
|
||||
baseID = baseID .. "_" .. hash
|
||||
end
|
||||
end
|
||||
|
||||
return baseID
|
||||
end
|
||||
|
||||
-- No parent (top-level element): use call-site counter approach
|
||||
-- Track how many elements have been created at this location
|
||||
callSiteCounters[locationKey] = (callSiteCounters[locationKey] or 0) + 1
|
||||
local instanceNum = callSiteCounters[locationKey]
|
||||
|
||||
baseID = locationKey
|
||||
local baseID = locationKey
|
||||
|
||||
-- Add instance number if multiple elements created at same location (e.g., in loops)
|
||||
if instanceNum > 1 then
|
||||
|
||||
Reference in New Issue
Block a user