gui/init.lua

1446 lines
44 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
--
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
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
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)
self.dualDim.offset = {
pos = {
x = x or self.dualDim.offset.pos.x,
y = y or self.dualDim.offset.pos.y
},
size = {
x = w or self.dualDim.offset.size.x,
y = h or self.dualDim.offset.size.y
}
}
self.dualDim.scale = {
pos = {
x = sx or self.dualDim.scale.pos.x,
y = sy or self.dualDim.scale.pos.y
},
size = {
x = sw or self.dualDim.scale.size.x,
y = 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
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)
}
]]
-- DO = {[[setImage]], c.image or IMAGE}
-- Connections are used greatly throughout do we copy those
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 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
-- Base get uniques
function gui:getUniques(tab)
local base = {
active = self.active,
visible = self.visible,
visibility = self.visibility,
color = self.color,
borderColor = self.borderColor,
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, gui)
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.rotation = 0
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
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
function c: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 function centerthread()
local centerfunc = function()
return centerX or centerY -- If the condition is true it acts like a yield
end
c:newThread(function()
while true do
thread.hold(centerfunc)
local x, y, w, h = c:getAbsolutes()
if centerX then
c:setDualDim(-w / 2, nil, nil, nil, .5)
end
if centerY then
c:setDualDim(nil, -h / 2, nil, nil, nil, .5)
end
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
centerthread()
end
function c:centerY(bool)
centerY = bool
if centering then return end
centering = true
centerthread()
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
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) == "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(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: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
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
c.setImage = function(self, i, x, y, w, h)
if i == nil then return end
local img = load_image(i)
load_image(i).OnReturn(function(img)
img = love.graphics.newImage(img)
IMAGE = i
if type(i) == "string" then i = image_cache[i] or i end
if i and x then
self.imageHeigth = h
self.imageWidth = w
if type(i) == "string" then
image_cache[i] = img
i = image_cache[i]
end
self.image = i
self.image:setWrap("repeat", "repeat")
self.imageColor = color.white
self.quad = love.graphics.newQuad(x, y, w, h, self.image:getWidth(), self.image:getHeight())
self.imageVisibility = 1
return
end
if type(i) == "userdata" and i:type() == "Image" then
img = i
end
local x, y, w, h = self:getAbsolutes()
self.imageColor = color.white
self.imageVisibility = 1
self.image = img
self.image:setWrap("repeat", "repeat")
self.imageHeigth = img:getHeight()
self.imageWidth = img:getWidth()
self.quad = love.graphics.newQuad(0, 0, self.imageWidth, self.imageHeigth, self.imageWidth, self.imageHeigth)
end)
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
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.imageHeigth)
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.imageHeigth 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)
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 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
-- Set color
love.graphics.setLineStyle("smooth")
love.graphics.setLineWidth(3)
love.graphics.setColor(bbg[1], bbg[2], bbg[3], vis)
love.graphics.rectangle("line", x, y, w, h, rx, ry, segments)
love.graphics.setColor(bg[1], bg[2], bg[3], vis)
love.graphics.rectangle("fill", x, y, w, h, rx, ry, segments)
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
-- 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
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)
-- Drawing and Updating
gui.draw = drawer.run
gui.update = updater.run
-- 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
-- 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
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)
print(nw, nh, xt, yt)
gui.x = xt
gui.y = yt
gui.dualDim.offset.size.x = nw
gui.dualDim.offset.size.y = nh
gui.w = nw
gui.h = nh
else
gui.dualDim.offset.size.x = w
gui.dualDim.offset.size.y = h
gui.w = w
gui.h = h
end
end)
return gui