First commit

This commit is contained in:
qfx 2020-01-12 18:43:36 +02:00
parent 99865de03a
commit ba5ecca443
7 changed files with 734 additions and 0 deletions

23
LICENSE Executable file
View File

@ -0,0 +1,23 @@
Copyright (c) 2019, Elmārs Āboliņš
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

281
core/element.lua Executable file
View File

@ -0,0 +1,281 @@
--[[ Element superclass ]]
local path = string.sub(..., 1, string.len(...) - string.len(".core.element"))
local helium = require(path .. ".dummy")
local context = {}
context.__index = context
local activeContext
function context.new(elem)
local ctx = setmetatable({view = elem.view, element = elem}, context)
return ctx
end
function context:bubbleUpdate()
self.element.settings.pendingUpdate = true
self.element.settings.needsRendering = true
if self.parentCtx then
self.parentCtx:bubbleUpdate()
end
end
function context:set()
if activeContext then
self.parentCtx = activeContext
self.absX = self.parentCtx.absX + self.view.x
self.absY = self.parentCtx.absY + self.view.y
activeContext = self
else
self.absX = self.view.x
self.absY = self.view.y
activeContext = self
end
end
function context:unset()
if self.parentCtx then
activeContext = self.parentCtx
else
activeContext = nil
end
end
local element = {}
element.__index = element
setmetatable(element,{
__call = function(cls, ...)
local self
if type(...)=='function' then
self = setmetatable({}, element)
self.renderer = ...
self.classless = true
elseif type(cls)=='table' then
self = setmetatable({}, cls)
self.classless = false
end
self:new(...)
return self
end
})
--Dummy functions
function element:renderer() print('no renderer') end
function element:updater() end
function element:constructor() end
--Control functions
--The new function that should be used for element creation
function element:new()
local dimensions
--save the parameters
self.parameters = {}
--The element canvas
self.canvas = nil
--Internal settings
self.settings = {
isSetup = false,
pendingUpdate = true,
needsRendering = true,
calculatedDimensions = true,
inserted = false
}
self.view = {
x = 0,
y = 0,
w = 10,
h = 10,
}
if self.classless then
self.classlessState = {}
self.classlessData = {}
end
end
--Called once dimensions are validated
function element:setup()
self.state =
setmetatable(
{},
{
__index = function(t, index)
return self.baseState[index]
end,
__newindex = function(t, index, val)
if self.baseState[index] ~= val then
self.baseState[index] = val
self.context:bubbleUpdate()
end
end
}
)
self.parameters =
setmetatable(
{},
{
__index = function(t, index)
return self.baseParams[index] or nil
end,
__newindex = function(t, index, val)
if self.baseParams[index] ~= val then
self.baseParams[index] = val
self.context:bubbleUpdate()
end
end
}
)
self.canvas = love.graphics.newCanvas(self.view.w, self.view.h)
self.quad = love.graphics.newQuad(0, 0, self.view.w, self.view.h, self.view.w, self.view.h)
--Context makes sure element internals don't have to worry about absolute coordinates
self.inputContext = helium.input.newContext(self)
self.context = context.new(self)
self.settings.isSetup = true
--Classless rendering
if self.classless then
self.classlessData.loadEffect = function (func)
if self.classlessData.loadCaptured then
return unpack(self.classlessData.loadCaptured)
else
self.classlessData.loadCaptured = func()
if type(self.classlessData.loadCaptured) == 'table' then
return unpack(self.classlessData.loadCaptured)
elseif self.classlessData.loadCaptured then
return self.classlessData.loadCaptured
end
end
end
self.classlessData.useState = function (initial)
self.settings.indice = self.settings.indice + 1
local indice = self.settings.indice
if self.classlessState[indice] and self.classlessState[indice].state then
return self.classlessState[indice].state, self.classlessState[indice].setState
else
self.classlessState[indice] = {}
self.classlessState[indice].state = initial
self.classlessState[indice].setState = function(set)
self.classlessState[indice].state = set
self.context:bubbleUpdate()
end
return self.classlessState[indice].state, self.classlessState[indice].setState
end
end
end
end
function element:classlessRender()
self.inputContext:set()
self.settings.indice = 0
local denv = getfenv(self.renderer)
denv['useState'] = self.classlessData.useState
denv['loadEffect'] = self.classlessData.loadEffect
self.renderer(self.parameters, self.view.w, self.view.h)
denv['useState'] = nil
denv['loadEffect'] = nil
self.inputContext:unset()
end
function element:renderWrapper()
local cnvs = love.graphics.getCanvas()
love.graphics.setCanvas({self.canvas, stencil = true})
love.graphics.clear()
if self.classless and self.parameters then
self:classlessRender()
self.settings.pendingUpdate = false
else
self:renderer()
end
love.graphics.setCanvas(cnvs)
end
function element:externalRender()
self.context:set()
if self.settings.needsRendering then
self:renderWrapper()
self.settings.needsRendering = false
end
love.graphics.setColor(1,1,1)
love.graphics.draw(self.canvas, self.quad, self.view.x, self.view.y)
self.context:unset()
end
function element:externalUpdate()
if self.settings.pendingUpdate then
if self.updater then
self:updater()
end
self.settings.needsRendering = true
self.settings.pendingUpdate = false
end
end
--External functions
--Acts as the entrypoint for beginning rendering
function element:draw(params, x, y, w, h)
self.view.x = x or self.view.x
self.view.y = y or self.view.y
self.view.w = w or self.view.w
self.view.h = h or self.view.h
if params then
if type(params)=='table' and self.baseParams then
helium.utils.tableMerge(params, self.parameters)
elseif self.baseParams==nil then
self.baseParams = params
else
self.parameters = params
end
end
if not self.settings.isSetup then
self:setup()
end
if activeContext then
self:externalRender()
elseif not self.settings.inserted then
self.settings.inserted = true
table.insert(helium.elementBuffer, self)
end
end
function element:undraw()
self.settings.remove = true
self.settings.isSetup = false
end
return element

281
core/input.lua Executable file
View File

@ -0,0 +1,281 @@
local path = string.sub(..., 1, string.len(...) - string.len(".core.input"))
local helium = require(path .. ".dummy")
local input={
eventHandlers = {},
subscriptions = {},
activeEvents = {}
}
local subscription = {}
subscription.__index = subscription
local context = {}
context.__index = context
local activeContext
--[[Event types
###SIMPLE EVENTS###
mousepressed,--press started
mousereleased,--press released after an event inside started
mousepressed_outside --mousepressed outside of the subscription
mousereleased_outside --mousereleased outside of the subscription
keypressed,--key pressed
keyreleased,--key released
###COMPLEX EVENTS###
dragged,
clicked,
hover,
]]
function input.newContext(element)
local ctx = setmetatable({view = element.view, subs = {}}, context)
return ctx
end
function context:set()
if activeContext then
self.parentCtx = activeContext
self.absX = self.parentCtx.absX + self.view.x
self.absY = self.parentCtx.absY + self.view.y
activeContext = self
else
self.absX = self.view.x
self.absY = self.view.y
activeContext = self
end
end
function context:unset()
if self.parentCtx then
activeContext = self.parentCtx
else
activeContext = nil
end
end
function context:destroy()
for i, e in ipairs(self.subs) do
self.subs:destroy()
end
end
function subscription.create(x, y, w, h, eventType, callback, doff)
local sub
if activeContext then
sub = setmetatable({
x = activeContext.absX + x,
y = activeContext.absY + y,
w = w,
h = h,
eventType = eventType,
active = doff or true,
callback = callback
},subscription)
else
sub = setmetatable({
x = x,
y = y,
w = w,
h = h,
eventType = eventType,
active = doff or true,
callback = callback
},subscription)
end
if not input.subscriptions[eventType] then
input.subscriptions[eventType] = {}
end
table.insert(input.subscriptions[eventType],sub)
return sub
end
function subscription:off()
self.active = false
end
function subscription:on()
self.active = true
end
function subscription:destroy()
self.destroy = true
self.active = false
end
function subscription:update(x, y, w, h)
self.x = x or self.x
self.y = y or self.y
self.w = w or self.w
self.h = h or self.h
end
function subscription:emit(...)
return self.callback(...)
end
function subscription:checkInside(x, y)
if x>self.x and x<self.x+self.w and y>self.y and y<self.y+self.h then
return true
else
return false
end
end
function subscription:checkOutside(x, y)
if x>self.x and x<self.x+self.w and y>self.y and y<self.y+self.h then
return false
else
return true
end
end
input.subscribe = function(...)
return subscription.create(...)
end
function input.eventHandlers.mousereleased(x, y, btn)
local captured = false
if input.subscriptions.mousereleased then
for index, sub in ipairs(input.subscriptions.mousereleased) do
local succ = sub:checkInside(x, y)
if succ and sub.active then
sub:emit(x, y, btn)
captured = true
end
end
end
if input.subscriptions.mousereleased_outside then
for index, sub in ipairs(input.subscriptions.mousereleased_outside) do
local succ = sub:checkOutside(x, y)
if succ and sub.active then
sub:emit(x, y, btn)
captured = true
end
end
end
if input.subscriptions.clicked then
for index, sub in ipairs(input.subscriptions.clicked) do
if sub.currentEvent then
sub.currentEvent = false
captured = true
if sub.cleanUp then
sub.cleanUp(x, y, btn)
end
end
end
end
return captured
end
function input.eventHandlers.mousepressed(x, y, btn)
local captured = false
if input.subscriptions.mousepressed then
for index, sub in ipairs(input.subscriptions.mousepressed) do
local succ = sub:checkInside(x, y)
if succ and sub.active then
sub:emit(x, y, btn)
captured = true
end
end
end
if input.subscriptions.mousepressed_outside then
for index, sub in ipairs(input.subscriptions.mousepressed_outside) do
local succ = sub:checkOutside(x, y)
if succ and sub.active then
sub:emit(x, y, btn)
captured = true
end
end
end
if input.subscriptions.clicked then
for index, sub in ipairs(input.subscriptions.clicked) do
local succ = sub:checkInside(x, y)
if succ and sub.active then
sub.cleanUp = sub:emit(x, y, btn)
sub.currentEvent = true
captured = true
end
end
end
return captured
end
function input.eventHandlers.keypressed(btn)
local captured = false
if input.subscriptions.keypressed then
for index, sub in ipairs(input.subscriptions.keypressed) do
if sub.active ==true then
sub:emit( btn)
captured = true
end
end
end
return captured
end
function input.eventHandlers.keyreleased(btn)
local captured = false
if input.subscriptions.keyreleased then
for index, sub in ipairs(input.subscriptions.keyreleased) do
if sub.active then
sub:emit(btn)
captured = true
end
end
end
return captured
end
function input.eventHandlers.mousemoved(x, y)
local captured = false
if input.subscriptions.hover then
for index, sub in ipairs(input.subscriptions.hover) do
local succ = sub:checkInside(x, y)
if succ and sub.active and not sub.currentEvent then
sub.cleanUp = sub:emit(x, y)
sub.currentEvent = true
captured = true
elseif sub.currentEvent and not sub:checkInside(x, y) then
sub.currentEvent = false
captured = true
if sub.cleanUp then
sub.cleanUp(x, y)
end
end
end
end
return captured
end
return input

1
dummy.lua Executable file
View File

@ -0,0 +1 @@
return {}

BIN
helium-draft.pdf Executable file

Binary file not shown.

118
init.lua Executable file
View File

@ -0,0 +1,118 @@
--[[--------------------------------------------------
Helium UI by qfx (qfluxstudios@gmail.com)
Copyright (c) 2019 Elmārs Āboliņš
gitlab.com/project link here
----------------------------------------------------]]
local path = ...
local helium = require(path..".dummy")
helium.utils = require(path..".utils")
helium.element = require(path..".core.element")
helium.input = require(path..".core.input")
helium.elementBuffer = {}
function helium.render()
for i, e in ipairs(helium.elementBuffer) do
e:externalRender()
end
end
function helium.update()
local remove = false
for i, e in ipairs(helium.elementBuffer) do
if e.settings.remove then
remove = true
else
e:externalUpdate()
end
end
if remove then
helium.utils.ArrayRemove(helium.elementBuffer, function(t, i)
--returns false or (true if nil)
return (not t[i].settings.remove)
end)
end
end
--[[
A user doesn't have to use this particular love.run
*.element.bufferUpdate()
*.draw()
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(gui.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
]]
function love.run()
if love.math then
love.math.setRandomSeed(os.time())
end
if love.load then love.load(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.
while true do
-- 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
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
love.timer.step()
dt = love.timer.getDelta()
end
-- Call update and draw
if love.update then love.update(dt) end -- will pass 0 if love.timer is disabled
helium.update()
if love.graphics and love.graphics.isActive() then
love.graphics.clear(love.graphics.getBackgroundColor())
love.graphics.origin()
if love.draw then love.draw() end
helium.render()
love.graphics.present()
end
if love.timer then love.timer.sleep(0.00001) end
end
end
return helium

30
utils.lua Executable file
View File

@ -0,0 +1,30 @@
local utils = {}
function utils.ArrayRemove(t, fnKeep)
local j, n = 1, #t;
for i=1,n do
if (fnKeep(t, i, j)) then
-- Move i's kept value to j's position, if it's not already there.
if (i ~= j) then
t[j] = t[i];
t[i] = nil;
end
j = j + 1; -- Increment position of where we'll place the next kept value.
else
t[i] = nil;
end
end
return t;
end
function utils.tableMerge(t, bt)
for i, e in pairs(t) do
if e ~= bt[i] then
bt[i] = e
end
end
end
return utils