diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..8628e10 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/core/element.lua b/core/element.lua new file mode 100755 index 0000000..a57ee48 --- /dev/null +++ b/core/element.lua @@ -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 diff --git a/core/input.lua b/core/input.lua new file mode 100755 index 0000000..ce23a2b --- /dev/null +++ b/core/input.lua @@ -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 xself.y and yself.x and xself.y and y