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")
# Helium
## Major features:
### Custom elements
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
### Efficient rendering & updating
The elements only update&re-render when state changes
### Code hotswap
Change and save a file loaded through the helium.loader, and see changes immediately
## Basic overview:
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.
## Demo's / Practical examples
[There's a repository of examples here](https://github.com/qfluxstudio/helium_demos)
## Getting started:
Load helium with `local helium = require 'helium'`
The basic structure for an element is:
```lua
return function(param,state,view)
--Setup zone
return function()
--Rendering zone
end
end
```
![alt text](https://i.imgur.com/ZQBQfsa.png "Helium")
# Helium
## Major features:
### Custom elements
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
### Efficient rendering & updating
The elements only update&re-render when state changes
### Code hotswap
Change and save a file loaded through the helium.loader, and see changes immediately
## Basic overview:
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.
## Demo's / Practical examples
[There's a repository of examples here](https://github.com/qfluxstudio/helium_demos)
## Getting started:
Load helium with `local helium = require 'helium'`
The basic structure for an element is:
```lua
return function(param,state,view)
--Setup zone
return function()
--Rendering zone
end
end
```
[The documentation outgrew this readme, see the github wiki](https://github.com/qfluxstudio/helium/wiki/)

View File

@ -1,7 +1,7 @@
return {
HOTSWAP = true, --Turns on hotswap, disable this once you're deploying a project
AUTO_RUN = true, --Replaces the default love.run
DEBUG = true, --Reserved for later
PURE_G = true, --whether to keep _G pure
HARD_ERROR = true, --Whether to display element errors inside or hard cras
return {
HOTSWAP = true, --Turns on hotswap, disable this once you're deploying a project
AUTO_RUN = true, --Replaces the default love.run
DEBUG = true, --Reserved for later
PURE_G = true, --whether to keep _G pure
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 layout = {}
layout.__index = layout
local element = require(path..'core.element')
local function layout_new(type, x, y, w, h)
local ctx = element.getContext()
--The output will be in pixel numbers regardless of inputs
if x <= 1 or not x then
x = ctx.view.x * (x or 0)
end
if y <= 1 then
y = ctx.view.y * (y or 0)
end
if w <= 1 then
w = ctx.view.w * (w or 1)
end
if h <= 1 then
h = ctx.view.h * (h or 1)
end
return setmetatable({
x = x,
y = y,
w = w,
h = h
}, layout)
end
--Sets mode for the proceding operations
function layout.mode()
end
--Sets padding for the next operations
function layout.pad()
end
--Sets margins for the proceding operations
function layout.margin()
end
function layout.offset()
end
function layout:draw()
end
layout(0,0,1,1)
return layout
local path = string.sub(..., 1, string.len(...) - string.len(".core.layout"))
local layout = {}
layout.__index = layout
local element = require(path..'core.element')
local function layout_new(type, x, y, w, h)
local ctx = element.getContext()
--The output will be in pixel numbers regardless of inputs
if x <= 1 or not x then
x = ctx.view.x * (x or 0)
end
if y <= 1 then
y = ctx.view.y * (y or 0)
end
if w <= 1 then
w = ctx.view.w * (w or 1)
end
if h <= 1 then
h = ctx.view.h * (h or 1)
end
return setmetatable({
x = x,
y = y,
w = w,
h = h
}, layout)
end
--Sets mode for the proceding operations
function layout.mode()
end
--Sets padding for the next operations
function layout.pad()
end
--Sets margins for the proceding operations
function layout.margin()
end
function layout.offset()
end
function layout:draw()
end
layout(0,0,1,1)
return layout

View File

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

View File

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

View File

@ -87,14 +87,23 @@ function element:new(param)
self.context = context.new(self)
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()
self.context.inputContext:update()
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
self.settings.canvasW = self.view.w*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)
--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
self.settings.canvasW = self.view.w*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
---@param x number
---@param y number
function element:draw(x, y)
function element:draw(x, y, w, h)
if not self.view.lock then
if x then self.view.x = x 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
if self.settings.firstDraw then

10
core/input.d.ts vendored
View File

@ -1,6 +1,6 @@
interface Subscription{
on():void;
off():void;
}
interface Subscription{
on():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;

View File

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

View File

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

View File

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

View File

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

66
init.d.ts vendored
View File

@ -1,33 +1,33 @@
export interface parameters{
[index: string]: any;
[index: number]: any;
}
export interface view{
x: number;
y: number;
w: number;
h: number;
lock?: boolean;
onChange?(): null;
}
export interface state{
[index: string]: any;
[index: number]: any;
}
interface HeliumElement{
view: view;
state: state;
parameters: parameters;
draw(this,x:number,y:number): null;
undraw(this): null;
}
declare function HeliumLoader(filepath:string):(params:parameters, w:number, h:number)=>HeliumElement;
export module helium{
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 interface parameters{
[index: string]: any;
[index: number]: any;
}
export interface view{
x: number;
y: number;
w: number;
h: number;
lock?: boolean;
onChange?(): null;
}
export interface state{
[index: string]: any;
[index: number]: any;
}
interface HeliumElement{
view: view;
state: state;
parameters: parameters;
draw(this,x:number,y:number): null;
undraw(this): null;
}
declare function HeliumLoader(filepath:string):(params:parameters, w:number, h:number)=>HeliumElement;
export module helium{
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;

View File

@ -12,10 +12,16 @@ helium.input = require(path..".core.input")
helium.loader = require(path..".loader")
helium.elementBuffer = {}
helium.__index = helium
setmetatable(helium, {__call = function(s,chunk)
return function(param,w,h)
return helium.element(chunk,nil,w,h,param)
end
setmetatable(helium, {__call = function(s, chunk)
return {
__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})
function helium.render()

View File

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