This commit is contained in:
Elmārs Āboliņš 2020-06-28 18:43:28 +03:00
parent 32befc1a89
commit 3bd423243e
15 changed files with 468 additions and 450 deletions

2
.gitignore vendored
View File

@ -1 +1 @@
local_conf.lua local_conf.lua

View File

@ -1,33 +1,33 @@
![alt text](https://i.imgur.com/ZQBQfsa.png "Helium") ![alt text](https://i.imgur.com/ZQBQfsa.png "Helium")
# Helium # Helium
## Major features: ## Major features:
### Custom elements ### Custom elements
Write your own elements, and interface with them however you want to, Write your own elements, and interface with them however you want to,
Whether you need a generalized button that can have many themes and options or something super specific, it can be done Whether you need a generalized button that can have many themes and options or something super specific, it can be done
### Efficient rendering & updating ### Efficient rendering & updating
The elements only update&re-render when state changes The elements only update&re-render when state changes
### Code hotswap ### Code hotswap
Change and save a file loaded through the helium.loader, and see changes immediately Change and save a file loaded through the helium.loader, and see changes immediately
## Basic overview: ## Basic overview:
Helium is practically more like a UI framework than a fully fledged UI library. Helium is practically more like a UI framework than a fully fledged UI library.
The idea is to build custom, build simple and build fast, encapsulate. The idea is to build custom, build simple and build fast, encapsulate.
## Demo's / Practical examples ## Demo's / Practical examples
[There's a repository of examples here](https://github.com/qfluxstudio/helium_demos) [There's a repository of examples here](https://github.com/qfluxstudio/helium_demos)
## Getting started: ## Getting started:
Load helium with `local helium = require 'helium'` Load helium with `local helium = require 'helium'`
The basic structure for an element is: The basic structure for an element is:
```lua ```lua
return function(param,state,view) return function(param,state,view)
--Setup zone --Setup zone
return function() return function()
--Rendering zone --Rendering zone
end end
end end
``` ```
[The documentation outgrew this readme, see the github wiki](https://github.com/qfluxstudio/helium/wiki/) [The documentation outgrew this readme, see the github wiki](https://github.com/qfluxstudio/helium/wiki/)

View File

@ -1,7 +1,7 @@
return { return {
HOTSWAP = true, --Turns on hotswap, disable this once you're deploying a project HOTSWAP = true, --Turns on hotswap, disable this once you're deploying a project
AUTO_RUN = true, --Replaces the default love.run AUTO_RUN = true, --Replaces the default love.run
DEBUG = true, --Reserved for later DEBUG = true, --Reserved for later
PURE_G = true, --whether to keep _G pure PURE_G = true, --whether to keep _G pure
HARD_ERROR = true, --Whether to display element errors inside or hard cras HARD_ERROR = true, --Whether to display element errors inside or hard cras
} }

View File

@ -1,61 +1,61 @@
local path = string.sub(..., 1, string.len(...) - string.len(".core.layout")) local path = string.sub(..., 1, string.len(...) - string.len(".core.layout"))
local layout = {} local layout = {}
layout.__index = layout layout.__index = layout
local element = require(path..'core.element') local element = require(path..'core.element')
local function layout_new(type, x, y, w, h) local function layout_new(type, x, y, w, h)
local ctx = element.getContext() local ctx = element.getContext()
--The output will be in pixel numbers regardless of inputs --The output will be in pixel numbers regardless of inputs
if x <= 1 or not x then if x <= 1 or not x then
x = ctx.view.x * (x or 0) x = ctx.view.x * (x or 0)
end end
if y <= 1 then if y <= 1 then
y = ctx.view.y * (y or 0) y = ctx.view.y * (y or 0)
end end
if w <= 1 then if w <= 1 then
w = ctx.view.w * (w or 1) w = ctx.view.w * (w or 1)
end end
if h <= 1 then if h <= 1 then
h = ctx.view.h * (h or 1) h = ctx.view.h * (h or 1)
end end
return setmetatable({ return setmetatable({
x = x, x = x,
y = y, y = y,
w = w, w = w,
h = h h = h
}, layout) }, layout)
end end
--Sets mode for the proceding operations --Sets mode for the proceding operations
function layout.mode() function layout.mode()
end end
--Sets padding for the next operations --Sets padding for the next operations
function layout.pad() function layout.pad()
end end
--Sets margins for the proceding operations --Sets margins for the proceding operations
function layout.margin() function layout.margin()
end end
function layout.offset() function layout.offset()
end end
function layout:draw() function layout:draw()
end end
layout(0,0,1,1) layout(0,0,1,1)
return layout return layout

View File

@ -1,7 +1,8 @@
local path = string.sub(..., 1, string.len(...) - string.len(".control.size")) local path = string.sub(..., 1, string.len(...) - string.len(".control.size"))
local stack = require(path..'.core.stack') local stack = require(path..'.core.stack')
--Sets the computed/minimum size of an element to be used with layout calculations and rendering --Sets the computed/minimum size of an element to be used with layout calculations and rendering
return function(w, h) return function(w, h)
local currentStack = stack.getContext()
currentStack.element:setCalculatedSize(w, h)
end end

View File

@ -1,19 +1,19 @@
local path = string.sub(..., 1, string.len(...) - string.len(".control.state")) local path = string.sub(..., 1, string.len(...) - string.len(".control.state"))
local context = require(path.. ".core.context") local context = require(path.. ".core.stack")
return function (base) return function (base)
local base = base or {} base = base or {}
local fakeBase = {} local fakeBase = {}
local activeContext = context.getContext() local activeContext = context.getContext()
return setmetatable({},{ return setmetatable({},{
__index = function(t, index) __index = function(t, index)
return fakeBase[index] or base[index] return fakeBase[index] or base[index]
end, end,
__newindex = function(t, index, val) __newindex = function(t, index, val)
if fakeBase[index] ~= val then if fakeBase[index] ~= val then
fakeBase[index] = val fakeBase[index] = val
activeContext:bubbleUpdate() activeContext:bubbleUpdate()
end end
end end
}) })
end end

View File

@ -87,14 +87,23 @@ function element:new(param)
self.context = context.new(self) self.context = context.new(self)
end end
function element:setCalculatedSize(w, h)
self.view.minW = w or self.view.minW
self.view.minH = h or self.view.minH
self.view.w = math.max(self.view.minW, self.view.w)
self.view.h = math.max(self.view.minH, self.view.h)
end
function element:updateInputCtx() function element:updateInputCtx()
self.context.inputContext:update() self.context.inputContext:update()
if self.settings.canvasW then if self.settings.canvasW then
--If canvas too small make a bigger one
if self.settings.canvasW < self.view.w or self.settings.canvasH < self.view.h then if self.settings.canvasW < self.view.w or self.settings.canvasH < self.view.h then
self.settings.canvasW = self.view.w*1.25 self.settings.canvasW = self.view.w*1.25
self.settings.canvasH = self.view.h*1.25 self.settings.canvasH = self.view.h*1.25
self.canvas = love.graphics.newCanvas(self.view.w*1.25, self.view.h*1.25) self.canvas = love.graphics.newCanvas(self.view.w*1.25, self.view.h*1.25)
--If canvas too big make a smaller one
elseif self.settings.canvasW > self.view.w*1.50 or self.settings.canvasH > self.view.h*1.50 then elseif self.settings.canvasW > self.view.w*1.50 or self.settings.canvasH > self.view.h*1.50 then
self.settings.canvasW = self.view.w*1.25 self.settings.canvasW = self.view.w*1.25
self.settings.canvasH = self.view.h*1.25 self.settings.canvasH = self.view.h*1.25
@ -246,10 +255,12 @@ local insert = table.insert
--Acts as the entrypoint for beginning rendering --Acts as the entrypoint for beginning rendering
---@param x number ---@param x number
---@param y number ---@param y number
function element:draw(x, y) function element:draw(x, y, w, h)
if not self.view.lock then if not self.view.lock then
if x then self.view.x = x end if x then self.view.x = x end
if y then self.view.y = y end if y then self.view.y = y end
if w then self.view.w = self.view.minW<=w and w or self.view.minW end
if h then self.view.h = self.view.minH<=h and h or self.view.minH end
end end
if self.settings.firstDraw then if self.settings.firstDraw then

10
core/input.d.ts vendored
View File

@ -1,6 +1,6 @@
interface Subscription{ interface Subscription{
on():void; on():void;
off():void; off():void;
} }
export default function input(it:string,cb:(x?:number,y?:number)=>void,doff?:boolean,x?:number,y?:number,w?:number,h?:number): Subscription; export default function input(it:string,cb:(x?:number,y?:number)=>void,doff?:boolean,x?:number,y?:number,w?:number,h?:number): Subscription;

View File

@ -1,72 +1,72 @@
--Internal event/zone/perf-log system --Internal event/zone/perf-log system
local signals = {} local signals = {}
signals.__index = signals signals.__index = signals
function signals.newController() function signals.newController()
return setmetatable({ return setmetatable({
stack = {}, stack = {},
eventSubs = {}, eventSubs = {},
zoneSubs = {}, zoneSubs = {},
startTime = 0, startTime = 0,
totalTime = 0 totalTime = 0
}, signals) }, signals)
end end
function signals:push(name) function signals:push(name)
self.stack[#self.stack+1] = {name = name} self.stack[#self.stack+1] = {name = name}
self.startTime = love.timer.getTime() self.startTime = love.timer.getTime()
if self.zoneSubs[name] then if self.zoneSubs[name] then
for i, e in ipairs(self.zoneSubs[name]) do for i, e in ipairs(self.zoneSubs[name]) do
if e.on and e.func() then if e.on and e.func() then
end end
end end
end end
end end
function signals:pop() function signals:pop()
local name = self.stack[#self.stack].name local name = self.stack[#self.stack].name
if self.zoneSubs[name] then if self.zoneSubs[name] then
for i, e in ipairs(self.zoneSubs[name]) do for i, e in ipairs(self.zoneSubs[name]) do
if not e.on and e.func() then if not e.on and e.func() then
end end
end end
end end
self.totalTime = love.timer.getTime() - self.startTime self.totalTime = love.timer.getTime() - self.startTime
self.stack[#self.stack] = nil self.stack[#self.stack] = nil
end end
function signals:emitEvent(name, content) function signals:emitEvent(name, content)
if self.eventSubs[name] then if self.eventSubs[name] then
for i,e in ipairs(self.eventSubs[name]) do for i,e in ipairs(self.eventSubs[name]) do
e.func(content) e.func(content)
end end
end end
end end
function signals:onEvent(func, event) function signals:onEvent(func, event)
if not self.eventSubs[event] then if not self.eventSubs[event] then
self.eventSubs[event] = {} self.eventSubs[event] = {}
end end
self.eventSubs[event][#self.eventSubs[event]+1] = {func = func} self.eventSubs[event][#self.eventSubs[event]+1] = {func = func}
end end
--on - true when new zone is pushed --on - true when new zone is pushed
-- false when zone is popped -- false when zone is popped
function signals:onSignal(func, name, on) function signals:onSignal(func, name, on)
if not self.zoneSubs[name] then if not self.zoneSubs[name] then
self.zoneSubs[name] = {} self.zoneSubs[name] = {}
end end
self.zoneSubs[name][#self.zoneSubs[name]+1] = {func = func, on = on} self.zoneSubs[name][#self.zoneSubs[name]+1] = {func = func, on = on}
end end
return signals return signals

View File

@ -1,88 +1,88 @@
--Builds the element stack basically --Builds the element stack basically
local path = string.sub(..., 1, string.len(...) - string.len(".core.stack")) local path = string.sub(..., 1, string.len(...) - string.len(".core.stack"))
local helium = require(path .. ".dummy") local helium = require(path .. ".dummy")
---@class context ---@class context
local context = {} local context = {}
context.__index = context context.__index = context
local activeContext local activeContext
---@param elem element ---@param elem element
function context.new(elem) function context.new(elem)
local ctx = setmetatable({ local ctx = setmetatable({
view = elem.view, view = elem.view,
element = elem, element = elem,
childrenContexts = {}, childrenContexts = {},
inputContext = helium.input.newContext(elem) inputContext = helium.input.newContext(elem)
}, context) }, context)
return ctx return ctx
end end
function context:bubbleUpdate() function context:bubbleUpdate()
self.element.settings.pendingUpdate = true self.element.settings.pendingUpdate = true
self.element.settings.needsRendering = true self.element.settings.needsRendering = true
if self.parentCtx and self.parentCtx~=self then if self.parentCtx and self.parentCtx~=self then
self.parentCtx:bubbleUpdate() self.parentCtx:bubbleUpdate()
end end
end end
function context:set() function context:set()
if activeContext then if activeContext then
if not self.parentCtx and activeContext~=self then if not self.parentCtx and activeContext~=self then
self.parentCtx = activeContext self.parentCtx = activeContext
activeContext.childrenContexts[#activeContext.childrenContexts] = self activeContext.childrenContexts[#activeContext.childrenContexts] = self
end end
self.absX = self.parentCtx.absX + self.view.x self.absX = self.parentCtx.absX + self.view.x
self.absY = self.parentCtx.absY + self.view.y self.absY = self.parentCtx.absY + self.view.y
activeContext = self activeContext = self
else else
self.absX = self.view.x self.absX = self.view.x
self.absY = self.view.y self.absY = self.view.y
activeContext = self activeContext = self
end end
self.inputContext:set() self.inputContext:set()
end end
function context:unset() function context:unset()
self.inputContext:unset() self.inputContext:unset()
self.inputContext:afterLoad() self.inputContext:afterLoad()
if self.parentCtx then if self.parentCtx then
activeContext = self.parentCtx activeContext = self.parentCtx
else else
activeContext = nil activeContext = nil
end end
end end
function context:unsuspend() function context:unsuspend()
self.inputContext:unsuspend() self.inputContext:unsuspend()
end end
function context:destroy() function context:destroy()
self.elem:undraw() self.elem:undraw()
for i=1,#self.childrenContexts do for i=1,#self.childrenContexts do
self.childrenContexts[i]:destroy() self.childrenContexts[i]:destroy()
end end
end end
function context:suspend() function context:suspend()
self.inputContext:set() self.inputContext:set()
self.inputContext:suspend() self.inputContext:suspend()
self.inputContext:unset() self.inputContext:unset()
end end
--Function meant for external context capture --Function meant for external context capture
function context.getContext() function context.getContext()
return activeContext return activeContext
end end
return context return context

View File

@ -1,2 +1,2 @@
--thicc --thicc
return {} return {}

View File

@ -1,18 +1,18 @@
Hooks are additional functions to utilize the element lifecycle more granularly Hooks are additional functions to utilize the element lifecycle more granularly
e.g. e.g.
```lua ```lua
local onDestroyHook = require("helium/hooks/onDestroy") local onDestroyHook = require("helium/hooks/onDestroy")
return function (param) return function (param)
onDestroyHook(function() onDestroyHook(function()
doSomething() doSomething()
end) end)
return function() return function()
love.graphics.print("Help") love.graphics.print("Help")
end end
end end
``` ```

66
init.d.ts vendored
View File

@ -1,33 +1,33 @@
export interface parameters{ export interface parameters{
[index: string]: any; [index: string]: any;
[index: number]: any; [index: number]: any;
} }
export interface view{ export interface view{
x: number; x: number;
y: number; y: number;
w: number; w: number;
h: number; h: number;
lock?: boolean; lock?: boolean;
onChange?(): null; onChange?(): null;
} }
export interface state{ export interface state{
[index: string]: any; [index: string]: any;
[index: number]: any; [index: number]: any;
} }
interface HeliumElement{ interface HeliumElement{
view: view; view: view;
state: state; state: state;
parameters: parameters; parameters: parameters;
draw(this,x:number,y:number): null; draw(this,x:number,y:number): null;
undraw(this): null; undraw(this): null;
} }
declare function HeliumLoader(filepath:string):(params:parameters, w:number, h:number)=>HeliumElement; declare function HeliumLoader(filepath:string):(params:parameters, w:number, h:number)=>HeliumElement;
export module helium{ export module helium{
export let input: typeof import("./core/input") ; export let input: typeof import("./core/input") ;
} }
export function helium<T>(chunk:(params:T,state:state,view:view)=>()=>void):(params:T, w:number, h:number)=>HeliumElement; export function helium<T>(chunk:(params:T,state:state,view:view)=>()=>void):(params:T, w:number, h:number)=>HeliumElement;

View File

@ -12,10 +12,16 @@ helium.input = require(path..".core.input")
helium.loader = require(path..".loader") helium.loader = require(path..".loader")
helium.elementBuffer = {} helium.elementBuffer = {}
helium.__index = helium helium.__index = helium
setmetatable(helium, {__call = function(s,chunk)
return function(param,w,h) setmetatable(helium, {__call = function(s, chunk)
return helium.element(chunk,nil,w,h,param) return {
end __call = function(s, param, w, h)
return helium.element(chunk, nil, w, h, param)
end,
draw = function (param, x, y, w, h)
return helium.element.immediate(param, chunk, x, y, w, h)
end
}
end}) end})
function helium.render() function helium.render()

View File

@ -1,108 +1,108 @@
local path = string.sub(..., 1, string.len(...) - string.len(".loader")) local path = string.sub(..., 1, string.len(...) - string.len(".loader"))
local helium = require(path..'.dummy') local helium = require(path..'.dummy')
local elements = {} local elements = {}
local debugLoader = {} local debugLoader = {}
--Return level: 1--string; 2--chunk; 3--return value; default: element factory --Return level: 1--string; 2--chunk; 3--return value; default: element factory
local function loader(path) local function loader(path)
local succ = true local succ = true
--File string --File string
local fileContents, err = love.filesystem.read(path) local fileContents, err = love.filesystem.read(path)
if fileContents==nil then if fileContents==nil then
print('Error loading ',path,':',tostring(err),', will continue watching!') print('Error loading ',path,':',tostring(err),', will continue watching!')
succ = false succ = false
end end
local t, lastLoaded local t, lastLoaded
if succ then if succ then
t = love.filesystem.getInfo(path) t = love.filesystem.getInfo(path)
lastLoaded = t['modtime'] lastLoaded = t['modtime']
end end
--Chunk --Chunk
local status, err local status, err
if succ then if succ then
status, err = pcall(loadstring,fileContents) status, err = pcall(loadstring,fileContents)
end end
if status==false or status==nil then if status==false or status==nil then
print('Error compiling ',path,':',tostring(err),', will continue watching!') print('Error compiling ',path,':',tostring(err),', will continue watching!')
succ = false succ = false
end end
--Return values --Return values
local ret local ret
if succ then if succ then
succ, ret = pcall(err,path) succ, ret = pcall(err,path)
if not succ then if not succ then
print('Error calling ',path,':',tostring(ret)) print('Error calling ',path,':',tostring(ret))
end end
end end
return fileContents, err, ret, lastLoaded return fileContents, err, ret, lastLoaded
end end
debugLoader.loader = function(path,returnLevel) debugLoader.loader = function(path,returnLevel)
local level = returnLevel or 6 local level = returnLevel or 6
if elements[path] then if elements[path] then
return elements[path][level] return elements[path][level]
end end
local setfuncs = {} local setfuncs = {}
local fileContents, func, ret, lastLoaded = loader(path) local fileContents, func, ret, lastLoaded = loader(path)
local reloader = function(setFunc) local reloader = function(setFunc)
setfuncs[#setfuncs+1] = setFunc setfuncs[#setfuncs+1] = setFunc
end end
local factory = function(param,w,h) local factory = function(param,w,h)
return helium.element(ret, reloader, w, h, param) return helium.element(ret, reloader, w, h, param)
end end
elements[path] = {fileContents, func, ret, path, lastLoaded, factory, setfuncs = setfuncs} elements[path] = {fileContents, func, ret, path, lastLoaded, factory, setfuncs = setfuncs}
return elements[path][level] return elements[path][level]
end end
local counter = 0 local counter = 0
function debugLoader.update(dt) function debugLoader.update(dt)
counter = counter+dt counter = counter+dt
if counter>2 then if counter>2 then
for ind, elem in pairs(elements) do for ind, elem in pairs(elements) do
--Get the current last save time --Get the current last save time
local t = love.filesystem.getInfo(elem[4]) local t = love.filesystem.getInfo(elem[4])
local ll = t['modtime'] local ll = t['modtime']
if ll ~= elem[5] then if ll ~= elem[5] then
--If last save time differs then start reload sequence --If last save time differs then start reload sequence
local _, _, ret, lastLoaded = loader(elem[4]) local _, _, ret, lastLoaded = loader(elem[4])
local setfuncs = {} local setfuncs = {}
local reloader = function(setFunc) local reloader = function(setFunc)
setfuncs[#setfuncs+1] = setFunc setfuncs[#setfuncs+1] = setFunc
end end
local factory = function() local factory = function()
return helium.element(ret, reloader) return helium.element(ret, reloader)
end end
elem[5] = lastLoaded elem[5] = lastLoaded
elem[6] = factory elem[6] = factory
for i, func in ipairs(elem.setfuncs) do for i, func in ipairs(elem.setfuncs) do
func(ret) func(ret)
end end
end end
end end
counter = 0 counter = 0
end end
end end
if helium.conf.PURE_G then if helium.conf.PURE_G then
HeliumLoader = debugLoader.loader HeliumLoader = debugLoader.loader
end end
return debugLoader return debugLoader