cursor animation for immediate mode

This commit is contained in:
Michael Freno
2025-11-11 09:43:00 -05:00
parent 35113685d5
commit 6d8324f61b
2 changed files with 72 additions and 10 deletions

View File

@@ -221,6 +221,11 @@ function Gui.endFrame()
state._scrollbarDragging = element._scrollbarDragging state._scrollbarDragging = element._scrollbarDragging
state._hoveredScrollbar = element._hoveredScrollbar state._hoveredScrollbar = element._hoveredScrollbar
state._scrollbarDragOffset = element._scrollbarDragOffset state._scrollbarDragOffset = element._scrollbarDragOffset
-- Save cursor blink state
state._cursorBlinkTimer = element._cursorBlinkTimer
state._cursorVisible = element._cursorVisible
state._cursorBlinkPaused = element._cursorBlinkPaused
state._cursorBlinkPauseTimer = element._cursorBlinkPauseTimer
StateManager.setState(element.id, state) StateManager.setState(element.id, state)
end end
@@ -464,6 +469,23 @@ function Gui.update(dt)
end end
Gui._activeEventElement = nil Gui._activeEventElement = nil
-- In immediate mode, save state after update so that cursor blink timer changes persist
if Gui._immediateMode and Gui._currentFrameElements then
for _, element in ipairs(Gui._currentFrameElements) do
if element.id and element.id ~= "" and element.editable and element._focused then
local state = StateManager.getState(element.id, {})
-- Save cursor blink state (updated during element:update())
state._cursorBlinkTimer = element._cursorBlinkTimer
state._cursorVisible = element._cursorVisible
state._cursorBlinkPaused = element._cursorBlinkPaused
state._cursorBlinkPauseTimer = element._cursorBlinkPauseTimer
StateManager.setState(element.id, state)
end
end
end
end end
--- Forward text input to focused element --- Forward text input to focused element
@@ -621,6 +643,15 @@ function Gui.new(props)
element._scrollbarDragging = state._scrollbarDragging ~= nil and state._scrollbarDragging or false element._scrollbarDragging = state._scrollbarDragging ~= nil and state._scrollbarDragging or false
element._hoveredScrollbar = state._hoveredScrollbar element._hoveredScrollbar = state._hoveredScrollbar
element._scrollbarDragOffset = state._scrollbarDragOffset ~= nil and state._scrollbarDragOffset or 0 element._scrollbarDragOffset = state._scrollbarDragOffset ~= nil and state._scrollbarDragOffset or 0
-- Restore cursor blink state
element._cursorBlinkTimer = state._cursorBlinkTimer or element._cursorBlinkTimer or 0
if state._cursorVisible ~= nil then
element._cursorVisible = state._cursorVisible
elseif element._cursorVisible == nil then
element._cursorVisible = true
end
element._cursorBlinkPaused = state._cursorBlinkPaused or false
element._cursorBlinkPauseTimer = state._cursorBlinkPauseTimer or 0
-- Bind element to StateManager for interactive states -- Bind element to StateManager for interactive states
-- Use the same ID for StateManager so state persists across frames -- Use the same ID for StateManager so state persists across frames

View File

@@ -163,6 +163,8 @@ Public API methods to access internal state:
---@field _cursorColumn number? -- Internal: cursor column within line ---@field _cursorColumn number? -- Internal: cursor column within line
---@field _cursorBlinkTimer number? -- Internal: cursor blink timer ---@field _cursorBlinkTimer number? -- Internal: cursor blink timer
---@field _cursorVisible boolean? -- Internal: cursor visibility state ---@field _cursorVisible boolean? -- Internal: cursor visibility state
---@field _cursorBlinkPaused boolean? -- Internal: whether cursor blink is paused (e.g., while typing)
---@field _cursorBlinkPauseTimer number? -- Internal: timer for how long cursor blink has been paused
---@field _selectionStart number? -- Internal: selection start position ---@field _selectionStart number? -- Internal: selection start position
---@field _selectionEnd number? -- Internal: selection end position ---@field _selectionEnd number? -- Internal: selection end position
---@field _selectionAnchor number? -- Internal: selection anchor point ---@field _selectionAnchor number? -- Internal: selection anchor point
@@ -329,6 +331,8 @@ function Element.new(props)
self._cursorColumn = 0 -- Column within current line self._cursorColumn = 0 -- Column within current line
self._cursorBlinkTimer = 0 self._cursorBlinkTimer = 0
self._cursorVisible = true self._cursorVisible = true
self._cursorBlinkPaused = false
self._cursorBlinkPauseTimer = 0
-- Selection state -- Selection state
self._selectionStart = nil -- nil = no selection self._selectionStart = nil -- nil = no selection
@@ -3026,10 +3030,21 @@ function Element:update(dt)
-- Update cursor blink timer (only if editable and focused) -- Update cursor blink timer (only if editable and focused)
if self.editable and self._focused then if self.editable and self._focused then
self._cursorBlinkTimer = self._cursorBlinkTimer + dt -- If blink is paused, increment pause timer
if self._cursorBlinkTimer >= self.cursorBlinkRate then if self._cursorBlinkPaused then
self._cursorBlinkTimer = 0 self._cursorBlinkPauseTimer = (self._cursorBlinkPauseTimer or 0) + dt
self._cursorVisible = not self._cursorVisible -- Unpause after 0.5 seconds of no typing
if self._cursorBlinkPauseTimer >= 0.5 then
self._cursorBlinkPaused = false
self._cursorBlinkPauseTimer = 0
end
else
-- Normal blinking
self._cursorBlinkTimer = self._cursorBlinkTimer + dt
if self._cursorBlinkTimer >= self.cursorBlinkRate then
self._cursorBlinkTimer = 0
self._cursorVisible = not self._cursorVisible
end
end end
end end
@@ -4145,13 +4160,19 @@ function Element:_validateCursorPosition()
end end
--- Reset cursor blink (show cursor immediately) --- Reset cursor blink (show cursor immediately)
function Element:_resetCursorBlink() ---@param pauseBlink boolean|nil -- Whether to pause blinking (for typing)
function Element:_resetCursorBlink(pauseBlink)
if not self.editable then if not self.editable then
return return
end end
self._cursorBlinkTimer = 0 self._cursorBlinkTimer = 0
self._cursorVisible = true self._cursorVisible = true
if pauseBlink then
self._cursorBlinkPaused = true -- Pause blinking while typing
self._cursorBlinkPauseTimer = 0 -- Reset pause timer
end
-- Update scroll to keep cursor visible -- Update scroll to keep cursor visible
self:_updateTextScroll() self:_updateTextScroll()
end end
@@ -4396,6 +4417,10 @@ function Element:_saveEditableState()
_cursorPosition = self._cursorPosition, _cursorPosition = self._cursorPosition,
_selectionStart = self._selectionStart, _selectionStart = self._selectionStart,
_selectionEnd = self._selectionEnd, _selectionEnd = self._selectionEnd,
_cursorBlinkTimer = self._cursorBlinkTimer,
_cursorVisible = self._cursorVisible,
_cursorBlinkPaused = self._cursorBlinkPaused,
_cursorBlinkPauseTimer = self._cursorBlinkPauseTimer,
}) })
end end
@@ -4470,6 +4495,9 @@ function Element:insertText(text, position)
self:_updateAutoGrowHeight() -- Then update height based on new content self:_updateAutoGrowHeight() -- Then update height based on new content
self:_validateCursorPosition() self:_validateCursorPosition()
-- Reset cursor blink to show cursor and pause blinking while typing
self:_resetCursorBlink(true)
-- Save state to StateManager in immediate mode -- Save state to StateManager in immediate mode
self:_saveEditableState() self:_saveEditableState()
end end
@@ -4506,6 +4534,9 @@ function Element:deleteText(startPos, endPos)
self:_updateTextIfDirty() -- Update immediately to recalculate lines/wrapping self:_updateTextIfDirty() -- Update immediately to recalculate lines/wrapping
self:_updateAutoGrowHeight() -- Then update height based on new content self:_updateAutoGrowHeight() -- Then update height based on new content
-- Reset cursor blink to show cursor and pause blinking while deleting
self:_resetCursorBlink(true)
-- Save state to StateManager in immediate mode -- Save state to StateManager in immediate mode
self:_saveEditableState() self:_saveEditableState()
end end
@@ -5553,7 +5584,7 @@ function Element:keypressed(key, scancode, isrepeat)
if self.onTextChange and self._textBuffer ~= oldText then if self.onTextChange and self._textBuffer ~= oldText then
self.onTextChange(self, self._textBuffer, oldText) self.onTextChange(self, self._textBuffer, oldText)
end end
self:_resetCursorBlink() self:_resetCursorBlink(true)
elseif key == "delete" then elseif key == "delete" then
local oldText = self._textBuffer local oldText = self._textBuffer
if self:hasSelection() then if self:hasSelection() then
@@ -5571,7 +5602,7 @@ function Element:keypressed(key, scancode, isrepeat)
if self.onTextChange and self._textBuffer ~= oldText then if self.onTextChange and self._textBuffer ~= oldText then
self.onTextChange(self, self._textBuffer, oldText) self.onTextChange(self, self._textBuffer, oldText)
end end
self:_resetCursorBlink() self:_resetCursorBlink(true)
-- Handle return/enter -- Handle return/enter
elseif key == "return" or key == "kpenter" then elseif key == "return" or key == "kpenter" then
@@ -5593,7 +5624,7 @@ function Element:keypressed(key, scancode, isrepeat)
self.onEnter(self) self.onEnter(self)
end end
end end
self:_resetCursorBlink() self:_resetCursorBlink(true)
-- Handle Ctrl/Cmd+A (select all) -- Handle Ctrl/Cmd+A (select all)
elseif ctrl and key == "a" then elseif ctrl and key == "a" then
@@ -5627,7 +5658,7 @@ function Element:keypressed(key, scancode, isrepeat)
end end
end end
end end
self:_resetCursorBlink() self:_resetCursorBlink(true)
-- Handle Ctrl/Cmd+V (paste) -- Handle Ctrl/Cmd+V (paste)
elseif ctrl and key == "v" then elseif ctrl and key == "v" then
@@ -5648,7 +5679,7 @@ function Element:keypressed(key, scancode, isrepeat)
self.onTextChange(self, self._textBuffer, oldText) self.onTextChange(self, self._textBuffer, oldText)
end end
end end
self:_resetCursorBlink() self:_resetCursorBlink(true)
-- Handle Escape -- Handle Escape
elseif key == "escape" then elseif key == "escape" then