gui/init.lua
2023-12-22 22:27:01 -05:00

1629 lines
50 KiB
Lua

local utf8 = require("utf8")
local multi, thread = require("multi"):init()
local GLOBAL, THREAD = require("multi.integration.loveManager"):init()
local color = require("gui.core.color")
local gui = {}
local updater = multi:newProcessor("UpdateManager", true)
local drawer = multi:newProcessor("DrawManager", true)
local bit = require("bit")
local band, bor = bit.band, bit.bor
local cursor_hand = love.mouse.getSystemCursor("hand")
local clips = {}
local max, min, abs, rad, floor, ceil = math.max, math.min, math.abs, math.rad,
math.floor, math.ceil
local frame, image, text, box, video, button, anim = 0, 1, 2, 4, 8, 16, 32
local global_drag
local object_focus = gui
local first_loop = false
-- Types
gui.TYPE_FRAME = frame
gui.TYPE_IMAGE = image
gui.TYPE_TEXT = text
gui.TYPE_BOX = box
gui.TYPE_VIDEO = video
gui.TYPE_BUTTON = button
gui.TYPE_ANIM = anim
-- Variables
gui.__index = gui
gui.MOUSE_PRIMARY = 1
gui.MOUSE_SECONDARY = 2
gui.MOUSE_MIDDLE = 3
gui.ALIGN_CENTER = 0
gui.ALIGN_LEFT = 1
gui.ALIGN_RIGHT = 2
-- Connections
gui.Events = {} -- We are using fastmode for all connection objects.
gui.Events.OnQuit = multi:newConnection()
gui.Events.OnDirectoryDropped = multi:newConnection()
gui.Events.OnDisplayRotated = multi:newConnection()
gui.Events.OnFilesDropped = multi:newConnection()
gui.Events.OnFocus = multi:newConnection()
gui.Events.OnMouseFocus = multi:newConnection()
gui.Events.OnResized = multi:newConnection()
gui.Events.OnVisible = multi:newConnection()
gui.Events.OnKeyPressed = multi:newConnection()
gui.Events.OnKeyReleased = multi:newConnection()
gui.Events.OnTextEdited = multi:newConnection()
gui.Events.OnTextInputed = multi:newConnection()
gui.Events.OnMouseMoved = multi:newConnection()
gui.Events.OnMousePressed = multi:newConnection()
gui.Events.OnMouseReleased = multi:newConnection()
gui.Events.OnWheelMoved = multi:newConnection()
gui.Events.OnTouchMoved = multi:newConnection()
gui.Events.OnTouchPressed = multi:newConnection()
gui.Events.OnTouchReleased = multi:newConnection()
-- Non Love Events
gui.Events.OnThemeChanged = multi:newConnection()
-- Virtual gui init
gui.virtual = {}
-- Internal Connections
gui.Events.OnObjectFocusChanged = multi:newConnection()
-- Hooks
local function Hook(funcname, func)
if love[funcname] then
local cache = love[funcname]
love[funcname] = function(...)
cache(...)
func({}, ...)
end
else
love[funcname] = function(...) func({}, ...) end
end
end
Hook("quit", gui.Events.OnQuit.Fire)
Hook("directorydropped", gui.Events.OnDirectoryDropped.Fire)
Hook("displayrotated", gui.Events.OnDisplayRotated.Fire)
Hook("filedropped", gui.Events.OnFilesDropped.Fire)
Hook("focus", gui.Events.OnFocus.Fire)
Hook("mousefocus", gui.Events.OnMouseFocus.Fire)
Hook("resize", gui.Events.OnResized.Fire)
Hook("visible", gui.Events.OnVisible.Fire)
Hook("keypressed", gui.Events.OnKeyPressed.Fire)
Hook("keyreleased", gui.Events.OnKeyReleased.Fire)
Hook("textedited", gui.Events.OnTextEdited.Fire)
Hook("textinput", gui.Events.OnTextInputed.Fire)
Hook("mousemoved", gui.Events.OnMouseMoved.Fire)
Hook("mousepressed", gui.Events.OnMousePressed.Fire)
Hook("mousereleased", gui.Events.OnMouseReleased.Fire)
Hook("wheelmoved", gui.Events.OnWheelMoved.Fire)
Hook("touchmoved", gui.Events.OnTouchMoved.Fire)
Hook("touchpressed", gui.Events.OnTouchPressed.Fire)
Hook("touchreleased", gui.Events.OnTouchReleased.Fire)
-- Hotkeys
local has_hotkey = false
local hot_keys = {}
-- Wait for keys to release to reset
local unPress = updater:newFunction(function(keys)
thread.hold(function()
for key = 1, #keys["Keys"] do
if not love.keyboard.isDown(keys["Keys"][key]) then
keys.isBusy = false
return true
end
end
end)
end)
updater:newThread("GUI Hotkey Manager", function()
while true do
thread.hold(function() return has_hotkey end)
for i = 1, #hot_keys do
local good = true
for key = 1, #hot_keys[i]["Keys"] do
if not love.keyboard.isDown(hot_keys[i]["Keys"][key]) then
good = false
break
end
end
if good and not hot_keys[i].isBusy then
hot_keys[i]["Connection"]:Fire(hot_keys[i]["Ref"])
hot_keys[i].isBusy = true
unPress(hot_keys[i])
end
end
thread.sleep(.001)
end
end)
function gui:setHotKey(keys, conn)
has_hotkey = true
local conn = conn or multi:newConnection()
table.insert(hot_keys,
{Ref = self, Connection = conn, Keys = {unpack(keys)}})
return conn
end
-- Default HotKeys
gui.HotKeys = {}
-- Connections can be added together to create an OR logic to them, they can be multiplied together to create an AND logic to them
gui.HotKeys.OnSelectAll = gui:setHotKey({"lctrl", "a"}) +
gui:setHotKey({"rctrl", "a"})
gui.HotKeys.OnCopy = gui:setHotKey({"lctrl", "c"}) +
gui:setHotKey({"rctrl", "c"})
gui.HotKeys.OnPaste = gui:setHotKey({"lctrl", "v"}) +
gui:setHotKey({"rctrl", "v"})
gui.HotKeys.OnCut = gui:setHotKey({"lctrl", "x"}) +
gui:setHotKey({"rctrl", "x"})
gui.HotKeys.OnUndo = gui:setHotKey({"lctrl", "z"}) +
gui:setHotKey({"rctrl", "z"})
gui.HotKeys.OnRedo = gui:setHotKey({"lctrl", "y"}) +
gui:setHotKey({"rctrl", "y"}) +
gui:setHotKey({"lctrl", "lshift", "z"}) +
gui:setHotKey({"rctrl", "lshift", "z"}) +
gui:setHotKey({"lctrl", "rshift", "z"}) +
gui:setHotKey({"rctrl", "rshift", "z"})
-- Utils
gui.newFunction = updater.newFunction
function gui:getProcessor() return updater end
function gui:getObjectFocus() return object_focus end
function gui:hasType(t) return band(self.type, t) == t end
function gui:move(x, y)
self.dualDim.offset.pos.x = self.dualDim.offset.pos.x + x
self.dualDim.offset.pos.y = self.dualDim.offset.pos.y + y
self.OnPositionChanged:Fire(self, x, y)
end
function gui:moveInBounds(dx, dy)
local x, y, w, h = self:getAbsolutes()
local x1, y1, w1, h1 = self.parent:getAbsolutes()
if (x + dx >= x1 or dx > 0) and (x + w + dx <= x1 + w1 or dx < 0) and
(y + dy >= y1 or dy > 0) and (y + h + dy <= y1 + h1 or dy < 0) then
self:move(dx, dy)
end
end
local function intersecpt(x1, y1, x2, y2, x3, y3, x4, y4)
local x5 = max(x1, x3)
local y5 = max(y1, y3)
local x6 = min(x2, x4)
local y6 = min(y2, y4)
-- no intersection
if x5 > x6 or y5 > y6 then
return 0, 0, 0, 0 -- Return a no
end
local x7 = x5
local y7 = y6
local x8 = x6
local y8 = y5
return x7, y7, abs(x7 - x8), abs(y7 - y8)
end
local function toCoordPoints(x, y, w, h) return x, y, x + w, y + h end
function gui:intersecpt(x, y, w, h)
local x1, y1, x2, y2 = toCoordPoints(self:getAbsolutes())
local x3, y3, x4, y4 = toCoordPoints(x, y, w, h)
return intersecpt(x1, y1, x2, y2, x3, y3, x4, y4)
end
function gui:isDescendantOf(obj)
local parent = self.parent
while parent ~= gui do
if parent == obj then return true end
parent = parent.parent
end
return false
end
function gui:getChildren() return self.children end
function gui:getAbsolutes() -- returns x, y, w, h
return (self.parent.w * self.dualDim.scale.pos.x) +
self.dualDim.offset.pos.x + self.parent.x,
(self.parent.h * self.dualDim.scale.pos.y) +
self.dualDim.offset.pos.y + self.parent.y, (self.parent.w *
self.dualDim.scale.size.x) + self.dualDim.offset.size.x,
(self.parent.h * self.dualDim.scale.size.y) +
self.dualDim.offset.size.y
end
function gui:getAllChildren(vis)
local children = self:getChildren()
local allChildren = {}
for i, child in ipairs(children) do
if not (vis) and child.visible == true then
allChildren[#allChildren + 1] = child
local grandChildren = child:getAllChildren()
for j, grandChild in ipairs(grandChildren) do
allChildren[#allChildren + 1] = grandChild
end
end
end
return allChildren
end
function gui:newThread(func)
return updater:newThread("ThreadHandler<" .. self.type .. ">", func, self, thread)
end
function gui:setDualDim(x, y, w, h, sx, sy, sw, sh)
--[[
dd.offset.pos = {x = x or 0, y = y or 0}
self.dualDim.offset.size = {x = w or 0, y = h or 0}
self.dualDim.scale.pos = {x = sx or 0, y = sy or 0}
self.dualDim.scale.size = {x = sw or 0, y = sh or 0}
]]
self.dualDim = self:newDualDim(
x or self.dualDim.offset.pos.x,
y or self.dualDim.offset.pos.y,
w or self.dualDim.offset.size.x,
h or self.dualDim.offset.size.y,
sx or self.dualDim.scale.pos.x,
sy or self.dualDim.scale.pos.y,
sw or self.dualDim.scale.size.x,
sh or self.dualDim.scale.size.y)
self.OnSizeChanged:Fire(self, x, y, w, h, sx, sy, sw, sh)
end
function gui:rawSetDualDim(x, y, w, h, sx, sy, sw, sh)
self.dualDim = self:newDualDim(
x or self.dualDim.offset.pos.x,
y or self.dualDim.offset.pos.y,
w or self.dualDim.offset.size.x,
h or self.dualDim.offset.size.y,
sx or self.dualDim.scale.pos.x,
sy or self.dualDim.scale.pos.y,
sw or self.dualDim.scale.size.x,
sh or self.dualDim.scale.size.y)
end
local image_cache = {}
function gui:getTile(i, x, y, w, h) -- returns imagedata
local tw, wh
if i == nil then return end
if type(i) == "string" then i = image_cache[i] or i end
if type(i) == "string" then
i = love.image.newImageData(i)
image_cache[i] = i
elseif type(i) == "userdata" then
-- do nothing
elseif self:hasType(image) then
i, x, y, w, h = self.image, i, x, y, w
else
error("getTile invalid args!!! Usage: ImageElement:getTile(x,y,w,h) or gui:getTile(imagedata,x,y,w,h)")
end
return i, love.graphics.newQuad(x, y, w, h, i:getWidth(), i:getHeight())
end
function gui:topStack()
local siblings = self.parent.children
for i = 1, #siblings do
if siblings[i] == self then
table.remove(siblings, i)
break
end
end
siblings[#siblings + 1] = self
end
function gui:bottomStack()
local siblings = self.parent.children
for i = 1, #siblings do
if siblings[i] == self then
table.remove(siblings, i)
break
end
end
table.insert(siblings, 1, self)
end
function gui:OnUpdate(func) -- Not crazy about this approach, will probably rework this
if type(self) == "function" then func = self end
mainupdater(function() func(c) end)
end
local mainupdater = updater:newLoop().OnLoop
function gui:canPress(mx, my) -- Get the intersection of the clip area and the self then test with the clip, otherwise test as normal
local x, y, w, h
if self.__variables.clip[1] then
local clip = self.__variables.clip
x, y, w, h = self:intersecpt(clip[2], clip[3], clip[4], clip[5])
return mx < x + w and mx > x and my + h < y + h and my + h > y
else
x, y, w, h = self:getAbsolutes()
end
return not (mx > x + w or mx < x or my > y + h or my < y)
end
function gui:isBeingCovered(mx, my)
local children = gui:getAllChildren()
for i = #children, 1, -1 do
if children[i] == self then
return false
elseif children[i]:canPress(mx, my) and not (children[i] == self) and
not (children[i].ignore) then
return true
end
end
return false
end
function gui:getLocalCords(mx, my)
x, y, w, h = self:getAbsolutes()
return mx - x, my - y
end
function gui:setParent(parent)
local temp = self.parent:getChildren()
for i = 1, #temp do
if temp[i] == self then
table.remove(self.parent.children, i)
break
end
end
if parent then
table.insert(parent.children, self)
self.parent = parent
end
end
local function processDo(ref) ref.Do[1]() end
function gui:clone(opt)
--[[
{
copyTo: Who to set the parent to
connections: Do we copy connections? (true/false)
}
]]
local temp
local u = self:getUniques()
if self.type == frame then
temp = gui:newFrame(self:getDualDim())
elseif self.type == text + box then
temp = gui:newTextBox(self.text, self:getDualDim())
elseif self.type == text + button then
temp = gui:newTextButton(self.text, self:getDualDim())
elseif self.type == text then
temp = gui:newTextLabel(self.text, self:getDualDim())
elseif self.type == image + button then
temp = gui:newImageButton(u.Do[2], self:getDualDim())
elseif self.type == image then
temp = gui:newImageLabel(u.Do[2], self:getDualDim())
else -- We are dealing with a complex object
temp = processDo(u)
end
for i, v in pairs(u) do temp[i] = v end
local conn
if opt then
temp:setParent(opt.copyTo or gui.virtual)
if opt.connections then
conn = true
for i, v in pairs(self) do
if type(v) == "table" and v.Type == "connector" then
-- We want to copy the connection functions from the original object and bind them to the new one
if not temp[i] then
-- Incase we are dealing with a custom object, create a connection if the custom objects unique declearation didn't
temp[i] = multi:newConnection()
end
temp[i]:Bind(v:getConnections())
end
end
end
end
-- This recursively clones and sets the parent to the temp
for i, v in pairs(self:getChildren()) do
v:clone({copyTo = temp, connections = conn})
end
return temp
end
function gui:isActive()
return self.active and not (self:isDescendantOf(gui.virtual))
end
function gui:isOnScreen()
return
end
-- Base get uniques
function gui:getUniques(tab)
local base = {
active = self.active,
visible = self.visible,
visibility = self.visibility,
color = self.color,
borderColor = self.borderColor,
drawBorder = self.drawborder,
rotation = self.rotation
}
if tab then for i, v in pairs(tab) do base[i] = tab[i] end end
return base
end
-- Base Library
function gui:newBase(typ, x, y, w, h, sx, sy, sw, sh, virtual)
local c = {}
local buildBackBetter
local centerX = false
local centerY = false
local centering = false
local dragbutton = 2
local draggable = false
local hierarchy = false
local function testHierarchy(c, x, y, button, istouch, presses)
if hierarchy then
return not (global_drag or c:isBeingCovered(x, y))
end
return true
end
local function defaultCheck(...)
if not c:isActive() then return false end
local x, y = love.mouse.getPosition()
if c:canPress(x, y) then
return c, ...
end
return false
end
setmetatable(c, self)
c.__index = self.__index
c.__variables = {clip = {false, 0, 0, 0, 0}}
c.active = true
c.type = typ
c.dualDim = self:newDualDim(x, y, w, h, sx, sy, sw, sh)
c.children = {}
c.visible = true
c.visibility = 1
c.color = {.6, .6, .6}
c.borderColor = color.black
c.drawBorder = true
c.rotation = 0
c.OnLoad = multi:newConnection()
c.OnPressed = testHierarchy .. multi:newConnection()
c.OnPressedOuter = multi:newConnection()
c.OnReleased = testHierarchy .. multi:newConnection()
c.OnReleasedOuter = multi:newConnection()
c.OnReleasedOther = multi:newConnection()
c.OnDragStart = multi:newConnection()
c.OnDragging = multi:newConnection()
c.OnDragEnd = multi:newConnection()
c.OnEnter = testHierarchy .. multi:newConnection()
c.OnExit = multi:newConnection()
c.OnMoved = testHierarchy .. multi:newConnection()
c.OnWheelMoved = defaultCheck / gui.Events.OnWheelMoved
c.OnSizeChanged = multi:newConnection()
c.OnPositionChanged = multi:newConnection()
local dragging = false
local entered = false
local moved = false
local pressed = false
gui.Events.OnMouseMoved(function(x, y, dx, dy, istouch)
if not c:isActive() then return end
if c:canPress(x, y) then
c.OnMoved:Fire(c, x, y, dx, dy, istouch)
if entered == false then
c.OnEnter:Fire(c, x, y)
entered = true
end
if dragging then
c.OnDragging:Fire(c, dx, dy, x, y, istouch)
end
elseif entered then
entered = false
c.OnExit:Fire(c, x, y)
end
end)
gui.Events.OnMouseReleased(function(x, y, button, istouch, presses)
if not c:isActive() then return end
if c:canPress(x, y) then
c.OnReleased:Fire(c, x, y, dx, dy, istouch, presses)
elseif pressed then
c.OnReleasedOuter:Fire(c, x, y, button, istouch, presses)
else
c.OnReleasedOther:Fire(c, x, y, button, istouch, presses)
end
pressed = false
if dragging and button == dragbutton then
dragging = false
global_drag = false
c.OnDragEnd:Fire(c, dx, dy, x, y, istouch, presses)
end
end)
gui.Events.OnMousePressed(function(x, y, button, istouch, presses)
if not c:isActive() then return end
if c:canPress(x, y) then
c.OnPressed:Fire(c, x, y, dx, dy, istouch)
pressed = true
-- Only change and trigger the event if it is a different object
if c ~= object_focus then
gui.Events.OnObjectFocusChanged:Fire(object_focus, c)
object_focus = c
end
if draggable and button == dragbutton and not c:isBeingCovered(x, y) and
not global_drag then
dragging = true
global_drag = true
c.OnDragStart:Fire(c, dx, dy, x, y, istouch)
end
else
c.OnPressedOuter:Fire(c, x, y, button, istouch, presses)
end
end)
function c:setRoundness(rx, ry, seg, side)
self.roundness = side or true
self.__rx, self.__ry, self.__segments = rx or 5, ry or 5, seg or 30
end
function c:setRoundnessDirection(hori, vert)
self.__rhori = hori
self.__rvert = vert
end
function c:respectHierarchy(bool) hierarchy = bool end
local function centerthread()
if centerX or centerY then
local x, y, w, h = c:getAbsolutes()
if centerX then
c:rawSetDualDim(-w / 2, nil, nil, nil, .5)
end
if centerY then
c:rawSetDualDim(nil, -h / 2, nil, nil, nil, .5)
end
end
end
function c:enableDragging(but)
if not but then
draggable = false
return
end
dragbutton = but or dragbutton
draggable = true
end
function c:centerX(bool)
centerX = bool
if centering then return end
centering = true
self.OnSizeChanged(centerthread)
self.OnPositionChanged(centerthread)
updater:newLoop(centerthread)
end
function c:centerY(bool)
centerY = bool
if centering then return end
centering = true
self.OnSizeChanged(centerthread)
self.OnPositionChanged(centerthread)
updater:newLoop(centerthread)
end
function c:fullFrame()
self:setDualDim(0,0,0,0,0,0,1,1)
end
-- Add to the parents children table
if virtual then
c.parent = gui.virtual
table.insert(gui.virtual.children, c)
else
c.parent = self
table.insert(self.children, c)
end
local a = 0
return c
end
function gui:newDualDim(x, y, w, h, sx, sy, sw, sh)
local dd = {}
dd.offset = {}
dd.scale = {}
dd.offset.pos = {x = x or 0, y = y or 0}
dd.offset.size = {x = w or 0, y = h or 0}
dd.scale.pos = {x = sx or 0, y = sy or 0}
dd.scale.size = {x = sw or 0, y = sh or 0}
return dd
end
function gui:getDualDim()
local dd = self.dualDim
return dd.offset.pos.x, dd.offset.pos.y, dd.offset.size.x, dd.offset.size.y,
dd.scale.pos.x, dd.scale.pos.y, dd.scale.size.x, dd.scale.size.y
end
-- Frames
function gui:newFrame(x, y, w, h, sx, sy, sw, sh)
return self:newBase(frame, x, y, w, h, sx, sy, sw, sh)
end
function gui:newVirtualFrame(x, y, w, h, sx, sy, sw, sh)
return self:newBase(frame, x, y, w, h, sx, sy, sw, sh, true)
end
local testIMG
-- Texts
function gui:newTextBase(typ, txt, x, y, w, h, sx, sy, sw, sh)
local c = self:newBase(text + typ, x, y, w, h, sx, sy, sw, sh)
c.text = txt
c.align = gui.ALIGN_LEFT
c.adjust = 0
c.textScaleX = 1
c.textScaleY = 1
c.textOffsetX = 0
c.textOffsetY = 0
c.textShearingFactorX = 0
c.textShearingFactorY = 0
c.textVisibility = 1
c.font = love.graphics.newFont(12)
c.textColor = color.black
c.OnFontUpdated = multi:newConnection()
function c:calculateFontOffset(font, adjust)
local adjust = adjust or 20
local x, y, width, height = self:getAbsolutes()
local top = height + adjust
local bottom = 0
local canvas = love.graphics.newCanvas(width, height + adjust)
love.graphics.setCanvas(canvas)
love.graphics.clear(0, 0, 0, .5, false, false)
love.graphics.setColor(1, 1, 1, 1)
love.graphics.setFont(font)
love.graphics.printf(self.text, 0, adjust / 2, width, "left",
self.rotation, self.textScaleX, self.textScaleY, 0,
0, self.textShearingFactorX,
self.textShearingFactorY)
love.graphics.setCanvas()
local data = canvas:newImageData()
local f_top, f_bot = false, false
for yy = 0, height - 1 do
for xx = 0, width - 1 do
local r, g, b, a = data:getPixel(xx, yy)
if r ~= 0 or g ~= 0 or b ~= 0 then
if yy < top and not f_top then
top = yy
f_top = true
break
end
end
end
end
for yy = height - 1, 0, -1 do
for xx = 0, width - 1 do
local r, g, b, a = data:getPixel(xx, yy)
if r ~= 0 or g ~= 0 or b ~= 0 then
if yy > bottom and not f_bot then
bottom = yy
f_bot = false
break
end
end
end
end
return top - adjust, bottom - adjust
end
function c:setFont(font, size)
if type(font) == "number" then
self.font = love.graphics.newFont(font)
elseif type(font) == "string" then
self.fontFile = font
self.font = love.graphics.newFont(font, size)
else
self.font = font
end
self.OnFontUpdated:Fire(self)
end
function c:fitFont(min_size, max_size)
local font
if self.fontFile then
if self.fontFile:match("ttf") then
font = function(n)
return love.graphics.newFont(self.fontFile, n, "normal")
end
else
font = function(n)
return love.graphics.newFont(self.fontFile, n)
end
end
else
font = function(n) return love.graphics.setNewFont(n) end
end
local text = self.text
local x, y, max_width, max_height = self:getAbsolutes()
local min_size = min_size or 1
local max_size = max_size or 100 -- You can adjust the maximum font size as needed
local tolerance = 0.1
local f
while max_size - min_size > tolerance do
local size = (min_size + max_size) / 2
f = font(size)
local text_width = f:getWidth(text)
local text_height = f:getHeight()
if text_width > max_width or text_height > max_height then
max_size = size
else
min_size = size
end
end
self:setFont(f)
return min_size
end
-- function c:fitFont(n, max)
-- local max = max or math.huge
-- local font
-- local isdefault = false
-- if self.fontFile then
-- if self.fontFile:match("ttf") then
-- font = function(n)
-- return love.graphics.newFont(self.fontFile, n, "normal")
-- end
-- else
-- font = function(n)
-- return love.graphics.newFont(self.fontFile, n)
-- end
-- end
-- else
-- isdefault = true
-- font = function(n) return love.graphics.setNewFont(n) end
-- end
-- local x, y, width, height = self:getAbsolutes()
-- local Font, text = self.Font, self.text
-- local s = 3
-- Font = font(s)
-- while height < max and Font:getHeight() < height and Font:getWidth(text) < width do
-- s = s + 1
-- Font = font(s)
-- end
-- Font = font(s - (4 + (n or 0)))
-- Font:setFilter("linear", "nearest", 4)
-- self.font = Font
-- self.textOffsetY = 0
-- local top, bottom = self:calculateFontOffset(Font, 0)
-- self.textOffsetY = floor(((height - bottom) - top) / 2)
-- self.OnFontUpdated:Fire(self)
-- return s - (4 + (n or 0))
-- end
function c:centerFont()
local x, y, width, height = self:getAbsolutes()
local top, bottom = self:calculateFontOffset(self.font, 0)
self.textOffsetY = floor(((height - bottom) - top) / 2)
self.OnFontUpdated:Fire(self)
end
function c:getUniques()
return gui.getUniques(c, {
text = c.text,
align = c.align,
textScaleX = c.textScaleX,
textScaleY = c.textScaleY,
textOffsetX = c.textOffsetX,
textOffsetY = c.textOffsetY,
textShearingFactorX = c.textShearingFactorX,
textShearingFactorY = c.textShearingFactorY,
textVisibility = c.textVisibility,
font = c.font,
textColor = c.textColor
})
end
return c
end
function gui:newTextButton(txt, x, y, w, h, sx, sy, sw, sh)
local c = self:newTextBase(button, txt, x, y, w, h, sx, sy, sw, sh)
c:respectHierarchy(true)
c.OnEnter(function(c, x, y, dx, dy, istouch)
love.mouse.setCursor(cursor_hand)
end)
c.OnExit(function(c, x, y, dx, dy, istouch) love.mouse.setCursor() end)
return c
end
function gui:newTextLabel(txt, x, y, w, h, sx, sy, sw, sh)
return self:newTextBase(frame, txt, x, y, w, h, sx, sy, sw, sh)
end
-- local val used when drawing
local function getTextPosition(text, self, mx, my, exact)
-- Initialize variables
local pos = 0
local font = love.graphics.getFont()
local width = 0
local height = font:getHeight()
-- Loop through each character in the string
for i = 1, #text do
local _w = font:getWidth(text:sub(i, i))
local x, y, w, h = math.floor(width + self.adjust + self.textOffsetX),
0, _w, height
width = width + _w
if not (mx > x + w or mx < x or my > y + h or my < y) then
if not (exact) and
(_w -
(width - (mx - math.floor(self.adjust + self.textOffsetX))) <
_w / 2 and i >= 1) then
return i - 1
else
return i
end
elseif i == #text and mx > x + w then
return #text
end
end
return pos
end
local cur = love.mouse.getCursor()
function gui:newTextBox(txt, x, y, w, h, sx, sy, sw, sh)
local c = self:newTextBase(box, txt, x, y, w, h, sx, sy, sw, sh)
c:respectHierarchy(true)
c.doSelection = false
c.OnReturn = multi:newConnection()
c.cur_pos = 0
c.selection = {0, 0}
function c:getUniques()
return gui.getUniques(c, {
doSelection = c.doSelection,
cur_pos = c.cur_pos,
adjust = c.adjust
})
end
function c:HasSelection()
return c.selection[1] ~= 0 and c.selection[2] ~= 0
end
function c:GetSelection()
local start, stop = c.selection[1], c.selection[2]
if start > stop then start, stop = stop, start end
return start, stop
end
function c:GetSelectedText()
if not c:HasSelection() then return "" end
local sta, sto = c.selection[1], c.selection[2]
if sta > sto then sta, sto = sto, sta end
return c.text:sub(sta, sto)
end
function c:ClearSelection()
c.doSelection = false
c.selection = {0, 0}
end
c.OnEnter(function(c, x, y, dx, dy, istouch)
love.mouse.setCursor(love.mouse.getSystemCursor("ibeam"))
end)
c.OnExit(function(c, x, y, dx, dy, istouch) love.mouse.setCursor(cur) end)
c.OnPressed(function(c, x, y, dx, dy, istouch)
object_focus.bar_show = true
c.cur_pos = getTextPosition(c.text, c, c:getLocalCords(x, y))
c.selection[1] = c.cur_pos
c.doSelection = true
end)
c.OnMoved(function(c, x, y, dx, dy, istouch)
if c.doSelection then
local xx, yy = c:getLocalCords(x, y)
c.selection[2] = getTextPosition(c.text, c, xx, yy, true)
end
end); -- Needed to keep next line from being treated like a function call
-- Connect to both events
(c.OnReleased + c.OnReleasedOuter)(function(c, x, y, dx, dy, istouch)
c.doSelection = false
end);
-- ReleasedOther is different than ReleasedOuter (Other/Outer)
(c.OnReleasedOther + c.OnPressedOuter)(function()
c.doSelection = false
c.selection = {0, 0}
end)
c.OnPressedOuter(function() c.bar_show = false end)
return c
end
local function textBoxThread()
updater:newThread("Textbox Handler", function()
while true do
-- Do nothing if we aren't dealing with a textbox
thread.hold(function() return object_focus:hasType(box) end)
local ref = object_focus
ref.bar_show = true
thread.sleep(.5)
ref.bar_show = false
thread.sleep(.5)
end
end).OnError(textBoxThread)
end
textBoxThread()
local function insert(obj, n_text)
if obj:HasSelection() then
local start, stop = obj:GetSelection()
obj.text = obj.text:sub(1, start - 1) .. n_text ..
obj.text:sub(stop + 1, -1)
obj:ClearSelection()
obj.cur_pos = start
if #n_text > 1 then obj.cur_pos = start + #n_text end
else
obj.text = obj.text:sub(1, obj.cur_pos) .. n_text ..
obj.text:sub(obj.cur_pos + 1, -1)
obj.cur_pos = obj.cur_pos + 1
if #n_text > 1 then obj.cur_pos = obj.cur_pos + #n_text end
end
end
local function delete(obj, cmd)
if obj:HasSelection() then
local start, stop = obj:GetSelection()
obj.text = obj.text:sub(1, start - 1) .. obj.text:sub(stop + 1, -1)
obj:ClearSelection()
obj.cur_pos = start - 1
else
if cmd == "delete" then
obj.text = obj.text:sub(1, obj.cur_pos) ..
obj.text:sub(obj.cur_pos + 2, -1)
else
obj.text = obj.text:sub(1, obj.cur_pos - 1) ..
obj.text:sub(obj.cur_pos + 1, -1)
object_focus.cur_pos = object_focus.cur_pos - 1
if object_focus.cur_pos == 0 then
object_focus.cur_pos = 1
end
end
end
end
gui.Events.OnObjectFocusChanged(function(prev, new)
--
end)
gui.HotKeys.OnSelectAll(function()
if object_focus:hasType(box) then
object_focus.selection = {1, #object_focus.text}
end
end)
gui.Events.OnTextInputed(function(text)
if object_focus:hasType(box) then insert(object_focus, text) end
end)
gui.HotKeys.OnCopy(function()
if object_focus:hasType(box) then
love.system.setClipboardText(object_focus:GetSelectedText())
end
end)
gui.HotKeys.OnPaste(function()
if object_focus:hasType(box) then
insert(object_focus, love.system.getClipboardText())
end
end)
gui.HotKeys.OnCut(function()
if object_focus:hasType(box) and object_focus:HasSelection() then
love.system.setClipboardText(object_focus:GetSelectedText())
delete(object_focus, "backspace")
end
end)
gui.Events.OnKeyPressed(function(key, scancode, isrepeat)
-- Don't process if we aren't dealing with a textbox
if not object_focus:hasType(box) then return end
if key == "left" then
object_focus.cur_pos = object_focus.cur_pos - 1
object_focus.bar_show = true
elseif key == "right" then
object_focus.cur_pos = object_focus.cur_pos + 1
object_focus.bar_show = true
elseif key == "return" then
object_focus.OnReturn:Fire(object_focus, object_focus.text)
elseif key == "backspace" then
delete(object_focus, "backspace")
elseif key == "delete" then
delete(object_focus, "delete")
end
end)
-- Images
local load_image = THREAD:newFunction(function(path)
require("love.image")
return love.image.newImageData(path)
end)
local load_images = THREAD:newFunction(function(paths)
require("love.image")
local images = #paths
for i = 1, #paths do
sThread.pushStatus(i, images, love.image.newImageData(paths[i]))
end
end)
-- Loads a resource and adds it to the cache
gui.cacheImage = thread:newFunction(function(self, path_or_paths)
if type(path_or_paths) == "string" then
-- runs thread to load image then cache it for faster loading
load_image(path_or_paths).OnReturn(function(img)
image_cache[path_or_paths] = img
end)
-- table of paths
elseif type(path_or_paths) == "table" then
local handler = load_images(path_or_paths)
handler.OnStatus(function(part, whole, img)
image_cache[path_or_paths[part]] = img
thread.pushStatus(part, whole, image_cache[path_or_paths[part]])
end)
end
end)
function gui:applyGradient(direction, ...)
local colors = {...}
if direction == "horizontal" then
direction = true
elseif direction == "vertical" then
direction = false
else
error("Invalid direction '" .. tostring(direction) .. "' for gradient. Horizontal or vertical expected.")
end
local result = love.image.newImageData(direction and 1 or #colors, direction and #colors or 1)
for i, color in ipairs(colors) do
local x, y
if direction then
x, y = 0, i - 1
else
x, y = i - 1, 0
end
result:setPixel(x, y, color[1], color[2], color[3], color[4] or 255)
end
local img = love.graphics.newImage(result)
img:setFilter('linear', 'linear')
local x, y, w, h = self:getAbsolutes()
self.imageColor = color.white
self.imageVisibility = 1
self.image = img
self.image:setWrap("repeat", "repeat")
self.imageHeight = img:getHeight()
self.imageWidth = img:getWidth()
self.quad = love.graphics.newQuad(0, 0, self.imageWidth, self.imageHeight, self.imageWidth, self.imageHeight)
if not (band(self.type, image) == image) then
self.type = self.type + image
end
end
function gui:newImageBase(typ, x, y, w, h, sx, sy, sw, sh)
local c = self:newBase(image + typ, x, y, w, h, sx, sy, sw, sh)
c.color = color.white
c.visibility = 0
c.scaleX = 1
c.scaleY = 1
local IMAGE
function c:getUniques()
return gui.getUniques(c, {
-- Recreating the image object using set image is the way to go
DO = {[[setImage]], c.image or IMAGE}
})
end
function c:flip(vert)
if vert then
c.scaleY = c.scaleY * -1
else
c.scaleX = c.scaleX * -1
end
end
c.setImage = function(self, i, x, y, w, h)
if i == nil then return end
img = love.image.newImageData(i)
img = love.graphics.newImage(img)
IMAGE = i
if type(i) == "string" then i = image_cache[i] or i end
if i and x then
c.imageHeight = h
c.imageWidth = w
if type(i) == "string" then
image_cache[i] = img
i = image_cache[i]
end
c.image = i
c.image:setWrap("repeat", "repeat")
c.imageColor = color.white
c.quad = love.graphics.newQuad(x, y, w, h, c.image:getWidth(), c.image:getHeight())
c.imageVisibility = 1
return
end
if type(i) == "userdata" and i:type() == "Image" then
img = i
end
local x, y, w, h = c:getAbsolutes()
c.imageColor = color.white
c.imageVisibility = 1
c.image = img
c.image:setWrap("repeat", "repeat")
c.imageHeight = img:getHeight()
c.imageWidth = img:getWidth()
c.quad = love.graphics.newQuad(0, 0, c.imageWidth, c.imageHeight, c.imageWidth, c.imageHeight)
end
return c
end
function gui:newImageLabel(source, x, y, w, h, sx, sy, sw, sh)
local c = self:newImageBase(frame, x, y, w, h, sx, sy, sw, sh)
c:setImage(source)
return c
end
function gui:newImageButton(source, x, y, w, h, sx, sy, sw, sh)
local c = self:newImageBase(frame, x, y, w, h, sx, sy, sw, sh)
c:respectHierarchy(true)
c:setImage(source)
c.OnEnter(function(c, x, y, dx, dy, istouch)
love.mouse.setCursor(cursor_hand)
end)
c.OnExit(function(c, x, y, dx, dy, istouch) love.mouse.setCursor() end)
return c
end
-- Video
function gui:newVideo(source, x, y, w, h, sx, sy, sw, sh)
local c = self:newImageBase(video, x, y, w, h, sx, sy, sw, sh)
c.OnVideoFinished = multi:newConnection()
c.playing = false
function c:setVideo(v)
if type(v) == "string" then
c.video = love.graphics.newVideo(v)
elseif v then
c.video = v
end
c.audiosource = c.video:getSource()
if c.audiosource then c.audioLength = c.audiosource:getDuration() end
c.videoHeigth = c.video:getHeight()
c.videoWidth = c.video:getWidth()
c.quad = love.graphics.newQuad(0, 0, w, h, c.videoWidth, c.videoHeigth)
end
function c:getVideo() return self.video end
if type(source) == "string" then c:setVideo(source) end
function c:play()
c.playing = true
c.video:play()
end
function c:setVolume(vol)
if self.audiosource then self.audiosource:setVolume(vol) end
end
function c:pause() c.video:pause() end
function c:stop()
c.playing = false
c.video:pause()
c.video:rewind()
end
function c:rewind() c.video:rewind() end
function c:seek(n) c.video:seek(n) end
function c:tell() return c.video:tell() end
c:newThread(function(self)
local testCompletion = function() -- More intensive test
if self.video:tell() == 0 then
self.OnVideoFinished:Fire(self)
return true
end
end
local isplaying = function() -- Less intensive test
return self.video:isPlaying()
end
while true do thread.chain(isplaying, testCompletion) end
end)
c.videoVisibility = 1
c.videoColor = color.white
return c
end
-- Draw Function
-- local label, image, text, button, box, video, animation (spritesheet)
local drawtypes = {
[0] = function(child, x, y, w, h) end,
[1] = function(child, x, y, w, h)
if child.image then
if child.scaleX < 0 or child.scaleY < 0 then
local sx, sy = child.scaleX, child.scaleY
local adjustX, adjustY = child.scaleX * w, child.scaleY * h
love.graphics.setColor(child.imageColor[1], child.imageColor[2],
child.imageColor[3], child.imageVisibility)
if sx < 0 and sy < 0 then
love.graphics.draw(child.image, child.quad, x - adjustX, y - adjustY, rad(child.rotation), (w / child.imageWidth) * child.scaleX, (h / child.imageHeight) * child.scaleY)
elseif sx < 0 then
love.graphics.draw(child.image, child.quad, x - adjustX, y, rad(child.rotation), (w / child.imageWidth) * child.scaleX, h / child.imageHeight)
else
love.graphics.draw(child.image, child.quad, x, y - adjustY, rad(child.rotation), w / child.imageWidth, (h / child.imageHeight) * child.scaleY)
end
else
love.graphics.setColor(child.imageColor[1], child.imageColor[2],
child.imageColor[3], child.imageVisibility)
love.graphics.draw(child.image, child.quad, x, y, rad(child.rotation), w / child.imageWidth, h / child.imageHeight)
end
end
end,
[2] = function(child, x, y, w, h)
love.graphics.setColor(child.textColor[1], child.textColor[2],
child.textColor[3], child.textVisibility)
love.graphics.setFont(child.font)
if child.align == gui.ALIGN_LEFT then
child.adjust = 0
elseif child.align == gui.ALIGN_CENTER then
local fw = child.font:getWidth(child.text)
child.adjust = (w - fw) / 2
elseif child.align == gui.ALIGN_RIGHT then
local fw = child.font:getWidth(child.text)
child.adjust = w - fw - 4
end
love.graphics.printf(child.text, child.adjust + x + child.textOffsetX,
y + child.textOffsetY, w, "left", child.rotation,
child.textScaleX, child.textScaleY, 0, 0,
child.textShearingFactorX,
child.textShearingFactorY)
end,
[4] = function(child, x, y, w, h)
if child.bar_show then
local lw = love.graphics.getLineWidth()
love.graphics.setLineWidth(1)
local font = child.font
local fh = font:getHeight()
local fw = font:getWidth(child.text:sub(1, child.cur_pos))
love.graphics.line(child.textOffsetX + child.adjust + x + fw, y + 4,
child.textOffsetX + child.adjust + x + fw,
y + fh - 2)
love.graphics.setLineWidth(lw)
end
if child:HasSelection() then
local blue = color.highlighter_blue
local start, stop = child.selection[1], child.selection[2]
if start > stop then start, stop = stop, start end
local x1, y1 = child.font:getWidth(child.text:sub(1, start - 1)), 0
local x2, y2 = child.font:getWidth(child.text:sub(1, stop)), h
love.graphics.setColor(blue[1], blue[2], blue[3], .5)
love.graphics.rectangle("fill", x + x1 + child.adjust, y + y1,
x2 - x1, y2 - y1)
end
end,
[8] = function(child, x, y, w, h)
if child.video and child.playing then
love.graphics.setColor(child.videoColor[1], child.videoColor[2],
child.videoColor[3], child.videoVisibility)
if w ~= child.imageWidth and h ~= child.imageHeight then
love.graphics.draw(child.video, x, y, rad(child.rotation),
w / child.videoWidth, h / child.videoHeigth)
else
love.graphics.draw(child.video, child.quad, x, y,
rad(child.rotation), w / child.videoWidth,
h / child.videoHeigth)
end
end
end,
[16] = function(child, x, y, w, h)
--
end
}
local draw_handler = function(child, no_draw)
local bg = child.color
local bbg = child.borderColor
local ctype = child.type
local vis = child.visibility
local x, y, w, h = child:getAbsolutes()
local roundness = child.roundness
local rx, ry, segments = child.__rx or 0, child.__ry or 0,
child.__segments or 0
child.x = x
child.y = y
child.w = w
child.h = h
if no_draw then return end
if child.clipDescendants then
local children = child:getAllChildren()
for c = 1, #children do -- Tell the children to clip themselves
local clip = children[c].__variables.clip
clip[1] = true
clip[2] = x
clip[3] = y
clip[4] = w
clip[5] = h
end
end
if child.__variables.clip[1] then
local clip = child.__variables.clip
love.graphics.setScissor(clip[2], clip[3], clip[4], clip[5])
elseif type(roundness) == "string" then
love.graphics.setScissor(x - 1, y - 2, w + 2, h + 3)
end
local drawB = child.drawBorder
-- Set color
love.graphics.setLineStyle("smooth")
love.graphics.setLineWidth(3)
if drawB then
love.graphics.setColor(bbg[1], bbg[2], bbg[3], vis)
love.graphics.rectangle("line", x, y, w, h, rx, ry, segments)
end
love.graphics.setColor(bg[1], bg[2], bg[3], vis)
love.graphics.rectangle("fill", x, y, w, h, rx, ry, segments)
if drawB then
if roundness == "top" then
love.graphics.rectangle("fill", x, y + ry / 2, w, h - ry / 2 + 1)
love.graphics.setLineStyle("rough")
love.graphics.setColor(bbg[1], bbg[2], bbg[3], 1)
love.graphics.setLineWidth(1)
love.graphics.line(x, y + ry, x, y + h + 1, x + 1 + w, y + h + 1,
x + 1 + w, y + ry)
love.graphics.line(x, y + h, x + 1 + w, y + h)
love.graphics.setScissor()
love.graphics.setColor(bbg[1], bbg[2], bbg[3], .6)
love.graphics.line(x - 1, y + ry / 2 + 2, x - 1, y + h + 2)
love.graphics.line(x + w + 2, y + ry / 2 + 2, x + w + 2, y + h + 2)
elseif roundness == "bottom" then
love.graphics.rectangle("fill", x, y, w, h - ry + 2)
love.graphics.setLineStyle("rough")
love.graphics.setColor(bbg[1], bbg[2], bbg[3], 1)
love.graphics.setLineWidth(2)
love.graphics.line(x - 1, y + ry + 1, x - 1, y - 1, x + w + 1, y - 1,
x + w + 1, y + ry + 1)
love.graphics.setScissor()
love.graphics.line(x - 1, y - 1, x + w + 1, y - 1)
love.graphics.setColor(bbg[1], bbg[2], bbg[3], .6)
love.graphics.setLineWidth(2)
love.graphics.line(x - 1, y + 2, x - 1, y + h - 4 - ry / 2)
love.graphics.line(x + w + 1, y + 2, x + w + 1, y + h - 4 - ry / 2)
end
end
-- Start object specific stuff
drawtypes[band(ctype, video)](child, x, y, w, h)
drawtypes[band(ctype, image)](child, x, y, w, h)
drawtypes[band(ctype, text)](child, x, y, w, h)
drawtypes[band(ctype, box)](child, x, y, w, h)
if child.post then child:post() end
if child.__variables.clip[1] then
love.graphics.setScissor() -- Remove the scissor
end
end
gui.draw_handler = draw_handler
drawer:newLoop(function()
local children = gui:getAllChildren()
for i = 1, #children do
local child = children[i]
if child.effect then
child.effect(function() draw_handler(child) end)
else
draw_handler(child)
end
end
first_loop = true
end)
drawer:newThread(function()
while true do
thread.sleep(.01)
local children = gui.virtual:getAllChildren()
for i = 1, #children do
local child = children[i]
if child.effect then
child.effect(function() draw_handler(child, true) end)
else
draw_handler(child, true)
end
end
first_loop = true
end
end)
local processors = {
updater.run
}
-- Drawing and Updating
gui.draw = drawer.run
gui.update = function()
for i = 1, #processors do
processors[i]()
end
end
function gui:newProcessor(name)
local proc = multi:newProcessor(name or "UnNamedProcess_"..multi.randomString(8), true)
table.insert(processors, proc.run)
return proc
end
-- Virtual gui
gui.virtual.type = frame
gui.virtual.children = {}
gui.virtual.dualDim = gui:newDualDim()
gui.virtual.x = 0
gui.virtual.y = 0
setmetatable(gui.virtual, gui)
local w, h = love.graphics.getDimensions()
gui.virtual.dualDim.offset.size.x = w
gui.virtual.dualDim.offset.size.y = h
gui.virtual.w = w
gui.virtual.h = h
gui.virtual.parent = gui.virtual
-- Root gui
gui.parent = gui
gui.type = frame
gui.children = {}
gui.dualDim = gui:newDualDim()
gui.x = 0
gui.y = 0
local w, h = love.graphics.getDimensions()
gui.dualDim.offset.size.x = w
gui.dualDim.offset.size.y = h
gui.w = w
gui.h = h
local g_width, g_height
local function GetSizeAdjustedToAspectRatio(dWidth, dHeight)
local isLandscape = g_width > g_height
local newHeight = 0
local newWidth = 0
if g_width / g_height > dWidth / dHeight then
newHeight = dWidth * g_height / g_width
newWidth = dWidth
else
newWidth = dHeight * g_width / g_height
newHeight = dHeight
end
return newWidth, newHeight, (dWidth-newWidth)/2, (dHeight-newHeight)/2
end
gui.GetSizeAdjustedToAspectRatio = GetSizeAdjustedToAspectRatio
function gui:setAspectSize(w, h)
if w and h then
g_width, g_height = w, h
gui.aspect_ratio = true
else
gui.aspect_ratio = false
end
end
gui.Events.OnResized(function(w, h)
if gui.aspect_ratio then
local nw, nh, xt, yt = GetSizeAdjustedToAspectRatio(w, h)
gui.x = xt
gui.y = yt
gui.dualDim.offset.size.x = nw
gui.dualDim.offset.size.y = nh
gui.w = nw
gui.h = nh
gui.virtual.x = xt
gui.virtual.y = yt
gui.virtual.dualDim.offset.size.x = nw
gui.virtual.dualDim.offset.size.y = nh
gui.virtual.w = nw
gui.virtual.h = nh
else
gui.dualDim.offset.size.x = w
gui.dualDim.offset.size.y = h
gui.w = w
gui.h = h
gui.virtual.dualDim.offset.size.x = w
gui.virtual.dualDim.offset.size.y = h
gui.virtual.w = w
gui.virtual.h = h
end
end)
return gui