implemented the rest of the state callbacks

redid layout
luadoc + emmylua annotations everywhere
grid layout started
This commit is contained in:
Elmārs Āboliņš 2021-06-15 03:36:09 +03:00
parent ea7d0c2da0
commit 5309f35dfc
22 changed files with 578 additions and 52 deletions

View File

@ -80,8 +80,17 @@ function atlases:unassignAll()
self.atlases[2].taken_area = 0
end
function atlases.onscreenchange(newW, newH)
function atlases:onscreenchange(newW, newH)
for i, e in ipairs(self.atlases[1].users) do
e:reassignCanvas()
end
for i, e in ipairs(self.atlases[2].users) do
e:reassignCanvas()
end
self.atlases[1] = atlas.new(newW, newH)
self.atlases[2] = atlas.new(newW, newH)
end
function atlas.new(w, h)

View File

@ -11,9 +11,8 @@ element.__index = element
local type,pcall = type,pcall
setmetatable(element, {
__call = function(cls, ...)
__call = function(cls, func, param, w, h, flags)
local self
local func, param, w, h = ...
--Make the object inherit class
self = setmetatable({}, element)
@ -22,6 +21,7 @@ setmetatable(element, {
self:new(param,nil, w, h)
self:createProxies()
---@type Element
return self
end
})
@ -32,6 +32,9 @@ function element:new(param, immediate, w, h)
self.parameters = {}
self.baseParams = param
--Internal state callbacks
self.callbacks = {}
--Internal settings
self.settings = {
isSetup = false,
@ -73,6 +76,18 @@ function element:new(param, immediate, w, h)
self.view = self.baseView
end
function element:reassignCanvas()
self.settings.failedCanvas = false
self.settings.hasCanvas = false
self.canvas = nil
self.quad = nil
self.interQuad = nil
self.deferResize = nil
self.context:bubbleUpdate()
end
function element:sizeChange(i, v)
local increase = self.baseView[i] < v
@ -90,6 +105,12 @@ function element:sizeChange(i, v)
else
self.deferResize = { increased = increase }
end
if self.callbacks.onSizeChange then
for i, cb in ipairs(self.callbacks.onSizeChange) do
cb(self.view.w, self.view.h)
end
end
end
function element:posChange(i, v)
@ -99,22 +120,37 @@ function element:posChange(i, v)
if not self.deferRepos then
self.deferRepos = true
end
if self.callbacks.onPosChange then
for i, cb in ipairs(self.callbacks.onPosChange) do
cb(self.view.x, self.view.y)
end
end
end
function element:onUpdate()
end
function element:onDraw()
if self.callbacks.onUpdate then
for i, cb in ipairs(self.callbacks.onUpdate) do
cb()
end
end
end
function element:onLoad()
if self.callbacks.onLoad then
for i, cb in ipairs(self.callbacks.onLoad) do
cb()
end
end
end
function element:onDestroy()
if self.callbacks.onDestroy then
for i, cb in ipairs(self.callbacks.onDestroy) do
cb()
end
end
end
function element:createProxies()
@ -204,6 +240,7 @@ function element:setup()
self.context:unset()
self.settings.isSetup = true
self:onLoad()
end
local setColor, rectangle, setFont, printf = love.graphics.setColor, love.graphics.rectangle, love.graphics.setFont, love.graphics.printf
@ -294,6 +331,7 @@ function element:externalUpdate()
end
if self.settings.pendingUpdate then
self:onUpdate()
self.settings.needsRendering = true
self.settings.pendingUpdate = false
end
@ -321,8 +359,8 @@ end
local insert = table.insert
--External functions
--Acts as the entrypoint for beginning rendering
---External functions
---Acts as the entrypoint for beginning rendering
---@param x number
---@param y number
function element:draw(x, y, w, h)
@ -355,10 +393,12 @@ function element:getSize()
return self.view.w, self.view.h
end
---Destroys this element
function element:destroy()
self.settings.remove = true
self.settings.firstDraw = true
self.settings.isSetup = false
self:onDestroy()
self.context:destroy()
end

View File

@ -174,14 +174,18 @@ function subscription:checkOutside(x, y)
return not (x>self.x and x<self.x+self.w and y>self.y and y<self.y+self.h)
end
---@alias InputMouseClickSubscriptionCallback fun(x:number, y:number, mouseButton:string)
---@alias InputMouseClickComplexSubscriptionCallback fun(x:number, y:number, mouseButton:string):fun(x:number, y:number)|nil
---@alias InputMouseMoveComplexSubscriptionCallback fun(x:number, y:number, dx:number, dy:number):fun(x:number, y:number)|nil
---@alias InputKeyPressSubscriptionCallback fun(key:KeyConstant|string, scancode:Scancode|string)
---@alias InputTextInputSubscriptionCallback fun(text:string)
input.subscribe = subscription.create
---@param eventType string
---@param callback function
---@param cbOff boolean
---@param x number
---@param y number
---@param w number
---@param h number
---@overload fun(self:any, eventType: "'mousepressed'"|"'mousereleased'"|"'mousepressed_outside'"|"'mousereleased_outside'", callback: InputMouseClickSubscriptionCallback, cbOff: boolean, x: number|nil, y:number|nil, w: number|nil, h:number|nil)
---@overload fun(self:any, eventType: "'clicked'", callback: InputMouseClickComplexSubscriptionCallback, cbOff: boolean, x:number|nil, y:number|nil, w:number|nil, h:number|nil)
---@overload fun(self:any, eventType: "'hover'"|"'dragged'", callback: InputMouseMoveComplexSubscriptionCallback, cbOff: boolean, x:number|nil, y:number|nil, w:number|nil, h:number|nil)
---@overload fun(self:any, eventType: "'keypressed'"|"'keyreleased'", callback: InputKeyPressSubscriptionCallback, cbOff: boolean)
---@overload fun(self:any, eventType: "'textinput'", callback: InputTextInputSubscriptionCallback, cbOff: boolean)
input.__call = function(self, eventType, callback, cbOff, x, y, w, h)
x = x or 0
y = y or 0

View File

@ -4,12 +4,17 @@ local atlas = require(path..'.core.atlas')
local helium = require(path..'.dummy')
local input = require(path..'.core.input')
---@class scene
local scene = {
activeScene = nil
}
scene.__index = scene
---comment
---@param cached boolean @whether to enable caching on this scene
---@return scene
function scene.new(cached)
---@type scene
local self = {
atlas = cached and atlas.create() or nil,
cached = cached or false,
@ -34,30 +39,32 @@ function scene.bench()
end
end
---Activates this scene
function scene:activate()
scene.activeScene = self
end
--Keeps the scene in memory with potentially the atlas
---Keeps the scene in memory with potentially the atlas
function scene:deactivate()
scene.activeScene = nil
end
---Recreates this scene
function scene:reload()
self.atlas = self.cached and atlas.create() or nil
self.ioSubscriptions = {}
self.buffer = {}
end
--Nukes the scene and it's elements and atlases and subscriptions from memory
--To achieve same state as after creation, use reload
---Nukes the scene and it's elements and atlases and subscriptions from memory
---To achieve same state as after creation, use reload
function scene:unload()
self.atlas = nil
self.buffer = nil
self.ioSubscriptions = nil
end
---Draws this scene and it's elements
function scene:draw()
helium.stack.newFrame()
if not helium.benchNum then
@ -73,6 +80,8 @@ function scene:draw()
end
---Updates this scene and it's elements
---@param dt number
function scene:update(dt)
for i = 1, #self.buffer do
if self.buffer[i]:externalUpdate(i) then

View File

@ -1 +1,16 @@
--Allows to expose a function to outside the element simply
--Allows to expose a function to outside the element simply
local path = string.sub(..., 1, string.len(...) - string.len(".hooks.onDestroy"))
local context = require(path.. ".core.stack")
---Creates a callback on the 'name' field for the current element
---@param name string
---@param callback function
return function (name, callback)
local activeContext = context.getContext()
if context.element[name] then
error('callback with name '..name..' would interfere with internal fields')
end
context.element[name] = callback
end

View File

@ -3,6 +3,10 @@ local context = require(path.. ".core.stack")
local c = {}
---Creates a context, that will be available to ALL nodes below this element
---@param name string the name of this context to be referenced later
---@param base table the table to be used as the base of this new context
---@return table
function c.use(name, base)
base = base or {}
local fakeBase = {}
@ -39,6 +43,9 @@ function c.use(name, base)
return ctx
end
---Gets the context with 'name', if it was initialized
---@param name string
---@return table|nil
function c.get(name)
local activeContext = context.getContext()

View File

@ -0,0 +1,14 @@
local path = string.sub(..., 1, string.len(...) - string.len(".hooks.onDestroy"))
local context = require(path.. ".core.stack")
---Sets a callback on a destructionevent for the current element (can have multiple)
---@param callback function
return function (callback)
local activeContext = context.getContext()
if not activeContext.element.callbacks['onDestroy'] then
activeContext.element.callbacks.onDestroy = {}
end
activeContext.element.callbacks.onDestroy[#activeContext.element.callbacks.onDestroy+1] = callback
end

View File

View File

@ -0,0 +1,14 @@
local path = string.sub(..., 1, string.len(...) - string.len(".hooks.onLoad"))
local context = require(path.. ".core.stack")
---Sets a callback on a load event for the current element (can have multiple)
---@param callback function
return function (callback)
local activeContext = context.getContext()
if not activeContext.element.callbacks['onLoad'] then
activeContext.element.callbacks.onLoad = {}
end
activeContext.element.callbacks.onLoad[#activeContext.element.callbacks.onLoad+1] = callback
end

View File

@ -0,0 +1,16 @@
local path = string.sub(..., 1, string.len(...) - string.len(".hooks.onPosChange"))
local context = require(path.. ".core.stack")
---@alias PosChangeCallback fun(x: number, y:number)
---Sets a callback on a position change event for the current element (can have multiple)
---@param callback PosChangeCallback
return function (callback)
local activeContext = context.getContext()
if not activeContext.element.callbacks['onPosChange'] then
activeContext.element.callbacks.onPosChange = {}
end
activeContext.element.callbacks.onPosChange[#activeContext.element.callbacks.onPosChange+1] = callback
end

View File

@ -0,0 +1,16 @@
local path = string.sub(..., 1, string.len(...) - string.len(".hooks.onSizeChange"))
local context = require(path.. ".core.stack")
---@alias SizeChangeCallback fun(w: number, h:number)
---Sets a callback on a size change event for the current element (can have multiple)
---@param callback SizeChangeCallback
return function (callback)
local activeContext = context.getContext()
if not activeContext.element.callbacks['onSizeChange'] then
activeContext.element.callbacks.onSizeChange = {}
end
activeContext.element.callbacks.onSizeChange[#activeContext.element.callbacks.onSizeChange+1] = callback
end

View File

@ -0,0 +1,15 @@
local path = string.sub(..., 1, string.len(...) - string.len(".hooks.onUpdate"))
local context = require(path.. ".core.stack")
---Sets a callback on any updatefor the current element (can have multiple)
---Use this to get logic outside of rendering function
---@param callback function
return function (callback)
local activeContext = context.getContext()
if not activeContext.element.callbacks['onUpdate'] then
activeContext.element.callbacks.onUpdate = {}
end
activeContext.element.callbacks.onUpdate[activeContext.element.callbacks.onUpdate+1] = callback
end

View File

@ -1,6 +1,10 @@
local path = string.sub(..., 1, string.len(...) - string.len(".hooks.state"))
local context = require(path.. ".core.stack")
---Creates a new 'state' object that will update the current element whenever a field is changed
---@generic T : table
---@param base T
---@return T
return function (base)
base = base or {}
local fakeBase = {}

View File

@ -1,4 +1,17 @@
return function(x, y, width, height, children, hpad, vpad, alignX)
local path = string.sub(..., 1, string.len(...) - string.len(".column"))
local layout = require(path..'.init')
---@class Column
local column = {}
column.__index = column
---@return layout
function column.new()
local self = setmetatable({}, column)
return layout(self.draw)
end
function column:draw(x, y, width, height, children, hpad, vpad, alignX)
local carriagePos = 0
if children then
for i, e in ipairs(children) do
@ -7,4 +20,6 @@ return function(x, y, width, height, children, hpad, vpad, alignX)
carriagePos = carriagePos + h + vpad
end
end
end
end
return column

277
layout/grid.lua Normal file
View File

@ -0,0 +1,277 @@
local column = require "helium.layout.column"
--my copy of the cssssss grids
local path = string.sub(..., 1, string.len(...) - string.len(".grid"))
local layout = require(path..'.init')
---@class GridCell
---@field name string @Will find the element with the relevant flag
---@alias GridRow GridCell[]
---@class HGridCell
---@field width number @Determines how wide this column will be
---@class WGridCell
---@field height string @Determines how high this column will be
---@alias HGridRow number[]|number|nil @Width of the row in cells
---@alias WGridCol number[]|number|nil @Width of the row in cells
---@alias GridLayout GridRow[]
---@class GridConfig
---@field layout GridLayout|nil @preconfigured layout table
---@field rows HGridRow|number|nil @set these instead of layout if you just want a regularly spaced 'table'
---@field columns WGridCol|number|nil @set these instead of layout if you just want a regularly spaced 'table' leave empty to flow in as many elements as you have
---@field verticalStretchMode "'stretch'"|"'normal'"
---@field horizontalStretchMode "'stretch'"|"'normal'"
---@field horizontalAlignMode "'left'"|"'center'"|"'right'"
---@field verticalAlignMode "'top'"|"'center'"|"'bottom'"
---@field rowSpacing number @size in pixels to space the rows
---@field colSpacing number @size in pixels to space the columns
---@field rowSizeMode "'relative'"|"'absolute'"
---@field colSizeMode "'relative'"|"'absolute'"
---@type GridConfig
local preconfiguredGrid = {
colSpacing = 3,
rowSpacing = 3,
verticalStretchMode = 'normal',
horizontalStretchMode = 'normal',
verticalAlignMode = 'center',
horizontalAlignMode = 'center',
--rows = {1, 1, 1, 1},
columns = {1, 1, 1, 1},
--[[layout = {
{'header', 'header', 'header'},
{'sidebar','content','content'},
{'sidebar','content','content'},
}]]
}
---@class grid
---@field gridLayout GridConfig
local grid = {}
grid.__index = grid
---@param gridLayout GridConfig
---@return layout
function grid.new(gridLayout)
local gridLayout = gridLayout or preconfiguredGrid
local self = setmetatable({
gridLayout = gridLayout
}, grid)
return layout(self, self.draw)
end
local function alignLeft(x, wroot, wchild)
return x
end
local function alignCenter(x, wroot, wchild)
return x+(wroot/2-wchild/2)
end
local function alignRight(x, wroot, wchild)
return x+(wroot-wchild)
end
local function alignHandlerX(mode, x, wr, wc)
if mode == 'center' then
return alignCenter(x, wr, wc)
elseif mode == 'right' then
return alignRight(x, wr, wc)
else
return alignLeft(x)
end
end
local function alignHandlerY(mode, y, hr, hc)
if mode == 'center' then
return alignCenter(y, hr, hc)
elseif mode == 'bottom' then
return alignRight(y, hr, hc)
else
return alignLeft(y)
end
end
function grid:draw(xRoot, yRoot, width, height, children, hpad, vpad)
-- Either of these means no named layout
local fullyAutoLayout = false
local autoCols = false
local autoRows = false
local equalRows = false
local equalCols = false
local vertValueToPixels = 0
local horValueToPixels = 0
local finalLayout = {}
if self.gridLayout.columns then
if not self.gridLayout.rows then
autoRows = true
else
if type(self.gridLayout.rows)=="table" then
local total = 0
for i, col in ipairs(self.gridLayout.rows) do
total = total + col
end
vertValueToPixels = (height-(self.gridLayout.rowSpacing*total))/total
else
vertValueToPixels = (height-(self.gridLayout.rowSpacing*self.gridLayout.rows))/self.gridLayout.rows
equalRows = true
end
end
if type(self.gridLayout.columns)=="table" then
local total = 0
for i, col in ipairs(self.gridLayout.columns) do
total = total + col
end
horValueToPixels = width/total
else
horValueToPixels = width/self.gridLayout.columns
equalCols = true
end
else
if not self.gridLayout.rows then
fullyAutoLayout = true
autoRows = true
else
autoCols = true
if type(self.gridLayout.rows)=="table" then
local total = 0
for i, col in ipairs(self.gridLayout.rows) do
total = total + col
end
vertValueToPixels = (height-(self.gridLayout.rowSpacing*total))/total
else
vertValueToPixels = (height-(self.gridLayout.rowSpacing*self.gridLayout.rows))/self.gridLayout.rows
equalRows = true
end
end
end
print(horValueToPixels, width)
if (not autoRows) and (not autoCols) then
elseif fullyAutoLayout then--one element per width, vertically down
local carriagePos = 0
if children then
for i, e in ipairs(children) do
local w, h = e:getSize()
if self.gridLayout.horizontalStretchMode =='stretch' then
w = width
e:draw(xRoot, yRoot+carriagePos, w)
else
local x = alignHandlerX(self.gridLayout.horizontalAlignMode, xRoot, width, w)
e:draw(x, yRoot+carriagePos)
end
carriagePos = carriagePos + self.gridLayout.rowSpacing + h
end
end
elseif autoCols then--one element per width, rows spaced
local carriagePos = 0
local row = 1
local lastRowSize = 1
if children then
for i, e in ipairs(children) do
local w, h = e:getSize()
local rowSize
local x, y
if equalRows then
rowSize = 1 * vertValueToPixels
else
rowSize = (self.gridLayout.rows[row] or lastRowSize)*vertValueToPixels
end
rowSize = math.max(h, rowSize)
if self.gridLayout.horizontalStretchMode =='stretch' then
w = width
else
x = alignHandlerX(self.gridLayout.horizontalAlignMode, xRoot, width, w)
end
if self.gridLayout.verticalStretchMode =='stretch' then
h = rowSize
else
y = alignHandlerY(self.gridLayout.verticalAlignMode, carriagePos, rowSize, h)
end
e:draw(x, y, w, h)
carriagePos = carriagePos + self.gridLayout.rowSpacing + rowSize
row = row + 1
end
end
elseif autoRows then--flow the elements freely vertically, space columns according to layout
local carriagePos = 0
local row = 1
local colDrawStart = 0
local currentRowMax = 1
local currentCol = 1
local rowWidth
if equalCols then
rowWidth = self.gridLayout.columns
else
rowWidth = #self.gridLayout.columns
end
if children then
for i, e in ipairs(children) do
local w, h = e:getSize()
local colSize
local x, y
if equalCols then
colSize = 1 * horValueToPixels
else
colSize = self.gridLayout.columns[currentCol] * horValueToPixels
end
currentRowMax = math.max(currentRowMax, h)
if self.gridLayout.horizontalStretchMode =='stretch' then
w = colSize
else
x = alignHandlerX(self.gridLayout.horizontalAlignMode, colDrawStart, colSize, w)
end
e:draw(x, yRoot+carriagePos, w, h)
colDrawStart = colDrawStart + colSize + self.gridLayout.colSpacing
if currentCol == rowWidth then
carriagePos = carriagePos + self.gridLayout.rowSpacing + currentRowMax
currentCol = 0
currentRowMax = 0
colDrawStart = 0
end
currentCol = currentCol + 1
end
end
end
end
return grid

View File

@ -1,92 +1,109 @@
local path = string.sub(..., 1, string.len(...) - string.len(".layout"))
local path = string.sub(..., 1, string.len(...) - string.len(".layout.init"))
---@class layout
---@field protected vars table
---@field protected type function
local layout = {}
local layouts = {}
layouts.column = require(path..'.layout.column')
layouts.row = require(path..'.layout.row')
layout.__index = layout
local element = require(path..'.core.element')
local stack = require(path..'.core.stack')
--Start prep phase
function layout.type(type)
function layout.type(binder, callback)
local curStack = stack.getContext()
curStack:startDeferingChildren()
local self = {
vars = {
type = type or 'column',
offLeft = 0,
offTop = 0,
width = 1,
hpad = 3,
vpad = 3,
height = 1,
alignX = 'left', --options: left, center, right
alignY = 'top', --options: top, center, bottom
--flowDir = 'rtl', --options: rtl/ttb
},
stack = curStack
stack = curStack,
binder = binder,
callback = callback,
}
return setmetatable(self, layout)
end
---Aligns the container vertically
---@param pos 'left'|'center'|'right'
function layout:alignVert(pos)
self.vars.alignY = pos
return self
end
---Aligns the container horizontally
---@param pos 'top'|'center'|'bottom'
function layout:alignHoriz(pos)
self.vars.alignX = pos
return self
end
--Sets up the box of the layout
---Sets up the width of the box of the layout
---@param w number width in pixels or absolute 0-1
function layout:width(w)
self.vars.width = w
return self
end
---Sets up the height of the box of the layout
---@param h number width in pixels or absolute 0-1
function layout:height(h)
self.vars.height = h
return self
end
---Offset from the left
---@param x number offset in pixels or absolute 0-1
function layout:left(x)
self.vars.offLeft = x
return self
end
---Offset from the right
---@param x number offset in pixels or absolute 0-1
function layout:right(x)
self.vars.offRight = x
return self
end
---Offset from the top
---@param y number offset in pixels or absolute 0-1
function layout:top(y)
self.vars.offTop = y
return self
end
---Offset from the bottom
---@param y number offset in pixels or absolute 0-1
function layout:bottom(y)
self.vars.offBot = y
return self
end
---Padding for the elements vertically
---@param px number offset in pixels
function layout:vPadding(px)
self.vars.vpad = px
return self
end
---Padding for the elements horizontally
---@param px number offset in pixels
function layout:hPadding(px)
self.vars.hpad = px
@ -97,6 +114,8 @@ end
--top + bottom = height ignored
--top px + bottom relative works
--left relative + bottom px works
function layout:draw()
local stack = self.stack
local children = stack:stopDeferingChildren()
@ -127,19 +146,9 @@ function layout:draw()
x = 0
end
layouts[self.vars.type](
x,
y,
width,
height,
children,
self.vars.hpad,
self.vars.vpad,
self.vars.alignX,
self.vars.alignY
)
self.callback(self.binder, x, y, width, height, children, self.vars.hpad, self.vars.vpad)
end
setmetatable(layout, {__call = function(s, type) return layout.type(type) end })
return layout
setmetatable(layout, {__call = function(s, callback, binder) return layout.type(callback, binder) end })
return layout

View File

@ -1,4 +1,18 @@
return function(x, y, width, height, children, hpad, vpad, alignX)
local path = string.sub(..., 1, string.len(...) - string.len(".row"))
local layout = require(path..'.init')
---@class Row
local row = {}
row.__index = row
---@return layout
function row.new()
local self = setmetatable({}, row)
return layout(self.draw)
end
function row:draw(x, y, width, height, children, hpad, vpad, alignX)
local carriagePos = 0
if children then
for i, e in ipairs(children) do
@ -7,4 +21,6 @@ return function(x, y, width, height, children, hpad, vpad, alignX)
carriagePos = carriagePos + w + vpad
end
end
end
end
return row

View File

@ -2,6 +2,20 @@ local path = string.sub(..., 1, string.len(...) - string.len(".shell.button"))
local state = require(path.. ".hooks.state")
local input = require(path.. ".core.input")
---@class buttonState
---@param down boolean @indicates whether this element is currently being pressed
---@param over boolean @indicates whether the mouse is over this button
---Creates a simple button wrapper, sets up all the callbacks and state for you
---@param onClick function|nil
---@param onRelease function|nil
---@param onEnter function|nil
---@param onExit function|nil
---@param x number|nil
---@param y number|nil
---@param w number|nil
---@param h number|nil
---@return buttonState
return function(onClick, onRelease, onEnter, onExit, x, y, w, h)
local button = state {
down = false,

View File

@ -2,6 +2,22 @@ local path = string.sub(..., 1, string.len(...) - string.len(".shell.button"))
local state = require(path.. ".hooks.state")
local input = require(path.. ".core.input")
---@class checkboxState
---@param down boolean @indicates whether this element is currently held down
---@param toggled boolean @current state of the checkbox
---@param over boolean @indicates whether the mouse is over this field
---A wrapper for state and subscriptions for a checkbox
---@param onClick function|nil
---@param onRelease function|nil
---@param onEnter function|nil
---@param onExit function|nil
---@param startOn boolean|nil
---@param x number|nil
---@param y number|nil
---@param w number|nil
---@param h number|nil
---@return checkboxState
return function(onClick, onRelease, onEnter, onExit, startOn, x, y, w, h)
local checkbox = state {
down = false,

View File

@ -3,6 +3,22 @@ local state = require(path.. ".hooks.state")
local input = require(path.. ".core.input")
local utf8 = require("utf8")
---@class textState
---@param focused boolean @indicates whether this element is currently focused
---@param text string @current state of the input string
---@param over boolean @indicates whether the mouse is over this field
---Textinput element wrapper
---@param onChange function|nil
---@param onFinish function|nil
---@param startStr function|nil
---@param onEnter function|nil
---@param onExit function|nil
---@param x number|nil
---@param y number|nil
---@param w number|nil
---@param h number|nil
---@return textState
return function(onChange, onFinish, startStr, onEnter, onExit, x, y, w, h)
local textState = state {
focused = false,