From 0422dd5cfddcdfd381908290381f08b29d353a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elm=C4=81rs=20=C4=80boli=C5=86=C5=A1?= Date: Mon, 25 Jan 2021 02:18:31 +0200 Subject: [PATCH] Scene --- README.md | 4 +- core/atlas.lua | 85 ++++++++++++------------ core/element.lua | 17 +++-- core/input.lua | 40 +++++++++++- core/scene.lua | 82 +++++++++++++++++++++++ core/stack.lua | 9 ++- docs/API-reference.md | 0 docs/Hooks.md | 0 docs/Input-events.md | 0 docs/Layout.md | 0 docs/State-Input-Guide.md | 79 ++++++++++++++++++++++ init.lua | 134 ++------------------------------------ 12 files changed, 272 insertions(+), 178 deletions(-) create mode 100644 core/scene.lua create mode 100644 docs/API-reference.md create mode 100644 docs/Hooks.md create mode 100644 docs/Input-events.md create mode 100644 docs/Layout.md create mode 100644 docs/State-Input-Guide.md diff --git a/README.md b/README.md index f77d5a2..bdebc19 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ function(param, view) end ``` -and you can make that function in to an element 'factory' like this: +and you can make that function into an element 'factory' like this: ```lua elementCreator = helium(function(param, view) @@ -30,7 +30,7 @@ elementCreator = helium(function(param, view) end) ``` -then you call the element factory with parameters and optionally width and height: +then you call the element factory with a table of parameters that will get passed to the element and optionally width and height: ```lua element = elementCreator({text = 'foo-bar'}, 100, 20) diff --git a/core/atlas.lua b/core/atlas.lua index 2ddedef..bba977c 100644 --- a/core/atlas.lua +++ b/core/atlas.lua @@ -1,38 +1,35 @@ local atlas = {} -local atlases atlas.__index = atlas +local atlases ={} +atlases.__index = atlases local BLOCK_SIZE = 5 - -function atlas.load() - if not atlases then - atlas.init() - end -end - -function atlas.getRatio(index) - return atlases[index].taken_area/atlases[index].ideal_area -end - -function atlas.getFreeArea(index) - return atlases[index].ideal_area - atlases[index].taken_area -end - -local sw, sh = love.graphics.getDimensions() -function atlas.init() - atlases = {} - atlases[1] = atlas.new(sw, sh) - atlases[2] = atlas.new(sw, sh) - atlas.atlases = atlases -end - +local coefficient = 1.5 local selfRenderTime = false +local sw, sh = love.graphics.getDimensions() -function atlas.setBench(time) +function atlases.create() + local self = { + atlases = {} + } + self.atlases[1] = atlas.new(sw, sh) + self.atlases[2] = atlas.new(sw, sh) + + return setmetatable(self, atlases) +end + +function atlases.setBench(time) selfRenderTime = time end -local coefficient = 1.5 -function atlas.assign(element) +function atlases:getRatio(index) + return self.atlases[index].taken_area/self.atlases[index].ideal_area +end + +function atlases:getFreeArea(index) + return self.atlases[index].ideal_area - self.atlases[index].taken_area +end + +function atlases:assign(element) local avg, sum, canvasID = 0, 0, element.context:getCanvasIndex(true) or 1 for i, e in ipairs(element.renderBench) do @@ -41,10 +38,10 @@ function atlas.assign(element) avg = sum/#element.renderBench - local areaBelow = atlas.getFreeArea(canvasID) + local areaBelow = self:getFreeArea(canvasID) local area = element.view.h*element.view.w - local areaCoef = (2-(atlas.getRatio(canvasID)) )-(area/(areaBelow/(4+3*atlas.getRatio(canvasID)))) + local areaCoef = (2-(self:getRatio(canvasID)) )-(area/(areaBelow/(4+3*self:getRatio(canvasID)))) local speedCoef = avg/selfRenderTime if not ((areaCoef+speedCoef)>coefficient) then @@ -53,11 +50,11 @@ function atlas.assign(element) local elW = element.view.w local elH = element.view.h - local canvas, quad, interQuad = atlases[canvasID]:assignElement(element) - if not canvas and atlases[canvasID].ideal_area < atlases[canvasID].taken_area*4 then + local canvas, quad, interQuad = self.atlases[canvasID]:assignElement(element) + if not canvas and self.atlases[canvasID].ideal_area < self.atlases[canvasID].taken_area*4 then --print('refragmenting ;3') - atlases[canvasID]:refragment() - canvas, quad, interQuad = atlases[canvasID]:assignElement(element) + self.atlases[canvasID]:refragment() + canvas, quad, interQuad = self.atlases[canvasID]:assignElement(element) if not canvas then --print('ran out of space') end @@ -67,18 +64,23 @@ function atlas.assign(element) return canvas, quad, interQuad end -function atlas.unassign(element) +function atlases:unassign(element) local canvasID = element.context:getCanvasIndex(true) or 1 - atlases[canvasID]:unassignElement(element) + self.atlases[canvasID]:unassignElement(element) end -function atlas.unassignAll() - createdAtlas.users = {} - createdAtlas:unMarkTiles(1, 1, createdAtlas.tileW, createdAtlas.tileH) - createdAtlas.taken_area = 0 +function atlases:unassignAll() + self.atlases[1].users = {} + self.atlases[2].users = {} + + self.atlases[1]:unMarkTiles(1, 1, self.atlases[1].tileW, self.atlases[1].tileH) + self.atlases[2]:unMarkTiles(1, 1, self.atlases[2].tileW, self.atlases[2].tileH) + + self.atlases[1].taken_area = 0 + self.atlases[2].taken_area = 0 end -function atlas.onscreenchange(newW, newH) +function atlases.onscreenchange(newW, newH) end @@ -139,7 +141,6 @@ function atlas:assignElement(element) w = tileSizeX, h = tileSizeY, quad = quad, - interQuad = iquad } self:markTiles(x, y, tileSizeX, tileSizeY) @@ -241,4 +242,4 @@ function atlas:unassignElement(element) self.users[element] = nil end -return atlas \ No newline at end of file +return atlases \ No newline at end of file diff --git a/core/element.lua b/core/element.lua index 39d5a64..e0943b8 100644 --- a/core/element.lua +++ b/core/element.lua @@ -3,6 +3,7 @@ local path = string.sub(..., 1, string.len(...) - string.len(".core.element")) local helium = require(path .. ".dummy") local context = require(path.. ".core.stack") +local scene = require(path.. ".core.scene") ---@class Element local element = {} @@ -157,9 +158,11 @@ end local newCanvas, newQuad = love.graphics.newCanvas, love.graphics.newQuad function element:createCanvas() - self.canvas, self.quad = helium.atlas.assign(self) + self.canvas, self.quad = scene.activeScene.atlas:assign(self) + print('here') if not self.canvas then + print('failed') self.settings.failedCanvas = true self.settings.hasCanvas = false return @@ -266,7 +269,7 @@ function element:externalRender() self:renderWrapper() end end - --lg.setScissor() + lg.setScissor() setCanvas(cnvs) @@ -282,7 +285,11 @@ end function element:externalUpdate() self.context:zIndex() - if not self.settings.failedCanvas and self.settings.testRenderPasses == 0 and not self.settings.hasCanvas then + if not self.settings.failedCanvas + and self.settings.testRenderPasses == 0 + and not self.settings.hasCanvas + and scene.activeScene.cached then + self:createCanvas() self.settings.pendingUpdate = true @@ -296,7 +303,7 @@ function element:externalUpdate() if self.deferResize then self.context:sizeChanged() if self.settings.hasCanvas then - helium.atlas.unassign(self) + scene.activeScene.atlas:unassign(self) self.settings.hasCanvas = false self.settings.testRenderPasses = 15 self.canvas = nil @@ -334,7 +341,7 @@ function element:draw(x, y, w, h) end elseif not self.settings.inserted then self.settings.inserted = true - insert(helium.elementInsertionQueue, self) + insert(scene.activeScene.buffer, self) end if self.settings.firstDraw then diff --git a/core/input.lua b/core/input.lua index 8a1b818..1f1ab38 100644 --- a/core/input.lua +++ b/core/input.lua @@ -1,13 +1,51 @@ local path = string.sub(..., 1, string.len(...) - string.len(".core.input")) local stack = require(path .. ".core.stack") +local helium = require(path .. ".dummy") local input = { eventHandlers = {}, - subscriptions = {}, + subscriptions = setmetatable({}, {__index = function (t, index) + return helium.scene.activeScene and helium.scene.activeScene[index] or nil + end}), activeEvents = {} } input.__index = input +--Middle man functions +local orig = { + mousepressed = love.handlers['mousepressed'], + mousereleased = love.handlers['mousereleased'], + keypressed = love.handlers['keypressed'], + keyreleased = love.handlers['keyreleased'], + mousemoved = love.handlers['mousemoved'] +} + +love.handlers['mousepressed'] = function(x, y, btn, d, e, f) + if not input.eventHandlers.mousepressed(x, y, btn, d, e ,f) then + orig.mousepressed(x, y, btn, d, e, f) + end +end +love.handlers['mousereleased'] = function(x, y, btn, d, e, f) + if not input.eventHandlers.mousereleased(x, y, btn, d, e ,f) then + orig.mousereleased(x, y, btn, d, e, f) + end +end +love.handlers['keypressed'] = function(key, b, c, d, e, f) + if not input.eventHandlers.keypressed(key, b, c, d, e, f) then + orig.keypressed(key, b, c, d, e, f) + end +end +love.handlers['keyreleased'] = function(key, b, c, d, e, f) + if not input.eventHandlers.keyreleased(key, b, c, d, e, f) then + orig.keyreleased(key, b, c, d, e, f) + end +end +love.handlers['mousemoved'] = function(x, y, dx, dy, e, f) + if not input.eventHandlers.mousemoved(x, y, dx, dy, e, f) then + orig.mousemoved(x, y, dx, dy, e, f) + end +end + local function sortFunc(t1, t2) if t1 == t2 then return false diff --git a/core/scene.lua b/core/scene.lua new file mode 100644 index 0000000..43c6887 --- /dev/null +++ b/core/scene.lua @@ -0,0 +1,82 @@ +local path = string.sub(..., 1, string.len(...) - string.len(".core.scene")) + +local atlas = require(path..'.core.atlas') +local helium = require(path..'.dummy') +local input = require(path..'.core.input') + +local scene = { + activeScene = nil +} +scene.__index = scene + +function scene.new(cached) + local self = { + atlas = cached and atlas.create() or nil, + cached = cached or false, + subscriptions = {}, + buffer = {} + } + return setmetatable(self, scene) +end + +local skipframes = 10 +function scene.bench() + if skipframes == 0 then + local startTime = love.timer.getTime() + + for i = 1, 20 do + love.graphics.print(i,-100,-100) + end + + helium.setBench((love.timer.getTime()-startTime)/5) + elseif skipframes>0 then + skipframes = skipframes - 1 + end +end + +function scene:activate() + scene.activeScene = self +end + +--Keeps the scene in memory with potentially the atlas +function scene:deactivate() + scene.activeScene = nil +end + + +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 +function scene:unload() + self.atlas = nil + self.buffer = nil + self.ioSubscriptions = nil +end + +function scene:draw() + helium.stack.newFrame() + if not helium.benchNum then + scene.bench() + end + --We don't want any side effects affecting internal rendering + love.graphics.reset() + for i, e in ipairs(self.buffer) do + e:externalRender() + end + +end + +function scene:update(dt) + for i = 1, #self.buffer do + if self.buffer[i]:externalUpdate(i) then + table.remove(self.buffer, i) + end + end +end + +return scene \ No newline at end of file diff --git a/core/stack.lua b/core/stack.lua index 7380619..e4c8c34 100644 --- a/core/stack.lua +++ b/core/stack.lua @@ -14,13 +14,13 @@ local currentTemporalZ = 0 ---@param elem element function context.new(elem) local ctx = setmetatable({ + capturedChilds = {}, view = elem.view, element = elem, childrenContexts = {}, childRenderTime = 0, deferChildren = false, events = event.new(), - capturedChilds = {}, temporalZ = {z = nil}, }, context) @@ -221,6 +221,13 @@ function context:offPosChange(callback) self.events:unsub('poschange', callback) end +function context:onEveryChild(func) + func(self.element) + for i, e in ipairs(self.childrenContexts) do + self.childrenContexts:onEveryChild(func) + end +end + --Function meant for external context capture function context.getContext() return activeContext diff --git a/docs/API-reference.md b/docs/API-reference.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/Hooks.md b/docs/Hooks.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/Input-events.md b/docs/Input-events.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/Layout.md b/docs/Layout.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/State-Input-Guide.md b/docs/State-Input-Guide.md new file mode 100644 index 0000000..85863c4 --- /dev/null +++ b/docs/State-Input-Guide.md @@ -0,0 +1,79 @@ +# State and Input + +This guide assumes you already have read the getting started in [README.md](../README.md) +And will use the [hello world repo](https://github.com/qeffects/helium-demo/) as a starting point + +## State + +UI elements tend to have various state, be it a button being pressed, a scroll bar's current value, current open tab, animations etc. + +In helium you introduce state to your elements by importing the state module like this: + +Note that you can have other values in the top level function(obviously), but changing them doesn't guarantee a display update + +```lua +local useState = require 'helium.control.state' +``` + +And then using it inside the element like: + +```lua +local elementCreator = helium(function(param, view) + local elementState = useState({var = 10}) +. + return function() + + end +end) +``` + +Now you can change the values of `elementState` and see the value update: + +```lua +local elementCreator = helium(function(param, view) + local elementState = useState({var = 10}) + + return function() + elementState.var = elementState.var + 1 + love.graphics.setColor(1, 1, 1) + love.graphics.print('elementState.var: '..elementState.var) + end +end) +``` + +This is where +## Input +comes in + +Input is a convenient module for various input subscriptions, import it like this: +```lua +local input = require 'helium.core.input' +``` + +Now you can use it in conjunction with state in your element like this: +```lua +local elementCreator = helium(function(param, view) + local elementState = useState({down = false}) + input('clicked', function() + elementState.down = not elementState.down + end) + + return function() + if elementState.down then + love.graphics.setColor(1, 0, 0) + else + love.graphics.setColor(0, 1, 1) + end + love.graphics.print('button text') + end +end) +``` + +The text now should toggle between 2 colors whenever pressed + +The full call signature of input is: +`local sub = input(eventType, callback, startOn, x, y, w, h)` + +See the demo repository with this example here: ~~link +See all event types explained here: ~~link +There are a few pre-made hooks that abstract away state management, see here: ~~link \ No newline at end of file diff --git a/init.lua b/init.lua index 7f0b99a..f59a06e 100644 --- a/init.lua +++ b/init.lua @@ -17,19 +17,23 @@ else end helium.utils = require(path..".utils") +helium.scene = require(path..".core.scene") helium.element = require(path..".core.element") helium.input = require(path..".core.input") helium.loader = require(path..".loader") helium.stack = require(path..".core.stack") helium.atlas = require(path..".core.atlas") -helium.elementBuffer = {} -helium.elementInsertionQueue = {} helium.__index = helium +function helium.setBench(time) + helium.benchNum = time + helium.element.setBench(time) + helium.atlas.setBench(time) +end + setmetatable(helium, {__call = function(s, chunk) return setmetatable({ draw = function (param, inputs, x, y, w, h) - return helium.element.immediate(param, inputs, chunk, x, y, w, h) end }, {__call = function(s, param, w, h) @@ -37,130 +41,6 @@ setmetatable(helium, {__call = function(s, chunk) end,}) end}) -local skipframes = 10 -local skip = true - -function helium.load() - helium.atlas.load() -end - -helium.load() - -function helium.unload() - helium.atlas.unassignAll() - helium.elementBuffer = {} -end - - -function helium.draw() - helium.stack.newFrame() - if skipframes == 0 then - local startTime = love.timer.getTime() - - for i = 1, 20 do - love.graphics.print(i,-100,-100) - end - - helium.element.setBench((love.timer.getTime()-startTime)/5) - helium.atlas.setBench((love.timer.getTime()-startTime)/5) - elseif skipframes>0 then - skipframes = skipframes - 1 - end - - --We don't want any side effects affecting internal rendering - love.graphics.reset() - for i, e in ipairs(helium.elementBuffer) do - e:externalRender() - end - - for i, e in ipairs(helium.elementInsertionQueue) do - table.insert(helium.elementBuffer, e) - end - helium.elementInsertionQueue = {} -end - -function helium.update(dt) - - for i = 1, #helium.elementBuffer do - if helium.elementBuffer[i]:externalUpdate(i) then - table.remove(helium.elementBuffer,i) - end - end -end - ---[[ - A user doesn't have to use this particular love.run - - helium.render() - helium.update(dt) - - Need to be called either through love.update and love.draw respectively - or put in to your custom love.run - - And for inputs to work the love.event part needs to look something like this: - - for name, a,b,c,d,e,f in love.event.poll() do - if name == "quit" then - if not love.quit or not love.quit() then - return a - end - end - - if not(helium.eventHandlers[name]) or not(helium.eventHandlers[name](a, b, c, d, e, f)) then - love.handlers[name](a, b, c, d, e, f) - end - end -]] -if helium.conf.AUTO_RUN then - function love.run() - if love.load then love.load() end--love.arg.parseGameArguments(arg), arg) end - - -- We don't want the first frame's dt to include time taken by love.load. - if love.timer then love.timer.step() end - - local dt = 0 - - -- Main loop time. - return function() - -- Process events. - if love.event then - love.event.pump() - for name, a,b,c,d,e,f in love.event.poll() do - if name == "quit" then - if not love.quit or not love.quit() then - return a or 0 - end - end - - if not(helium.input.eventHandlers[name]) or not(helium.input.eventHandlers[name](a, b, c, d, e, f)) then - love.handlers[name](a, b, c, d, e, f) - end - end - end - - -- Update dt, as we'll be passing it to update - if love.timer then dt = love.timer.step() end - - -- Call update and draw - if love.update then love.update(dt) end -- will pass 0 if love.timer is disabled - helium.update(dt) - - if love.graphics and love.graphics.isActive() then - love.graphics.origin() - love.graphics.clear(love.graphics.getBackgroundColor()) - - helium.draw() - - if love.draw then love.draw() end - - love.graphics.present() - end - - if love.timer then love.timer.sleep(0.001) end - end - end -end - --Typescript helium.helium = helium return helium \ No newline at end of file