fixed drift in timer
This commit is contained in:
@@ -37,4 +37,36 @@ Events:
|
||||
- Other Events
|
||||
- ~~OnUpdate~~ ✔️
|
||||
- ~~OnDraw~~ ✔️
|
||||
Polish:
|
||||
- ~~gui:newCheckbox()~~
|
||||
- ~~gui:isOnScreen()~~
|
||||
- ~~gui:newRadioGroup()~~
|
||||
- gui:newSlider()
|
||||
- ~~gui:newProgressBar()~~
|
||||
- gui:newTooltip()
|
||||
- gui:newTextArea()
|
||||
- TODO: selection, hotkeys
|
||||
- gui:newListFrame()
|
||||
- gui:newGridFrame()
|
||||
|
||||
Better Transistions:
|
||||
- ease.easeIn(start, stop, time) -- accelerates from start
|
||||
- ease.easeOut(start, stop, time) -- decelerates into stop
|
||||
- ease.easeInOut(start, stop, time) -- smooth S-curve
|
||||
- ease.easeInCubic(...) -- more aggressive acceleration
|
||||
- ease.easeOutCubic(...) -- more aggressive deceleration
|
||||
- ease.bounce(start, stop, time) -- bounces at the end
|
||||
- ease.elastic(start, stop, time) -- overshoots then settles
|
||||
- ease.back(start, stop, time) -- slight pull-back before moving
|
||||
|
||||
Focus Management:
|
||||
- gui.focus.set(element) -- programmatically set focus
|
||||
- gui.focus.get() -- returns currently focused element (or nil)
|
||||
- gui.focus.clear() -- clear focus (no element focused)
|
||||
- gui.focus.setTabOrder(list) -- set a list of elements for Tab navigation
|
||||
- gui.focus.tabNext() -- focus the next element in the tab order
|
||||
- gui.focus.tabPrev() -- focus the previous element
|
||||
|
||||
Z-index Enhancments:
|
||||
- gui:setLayer(n)
|
||||
- gui:getLayer()
|
||||
@@ -0,0 +1,167 @@
|
||||
local gui = require("gui")
|
||||
local theme = require("gui.core.theme")
|
||||
local color = require("gui.core.color")
|
||||
local multi, thread = require("multi"):init()
|
||||
local mediaProc = gui:newProcessor()
|
||||
|
||||
local function noOf(sx,sy,sw,sh)
|
||||
return nil,nil,nil,nil,sx,sy,sw,sh
|
||||
end
|
||||
|
||||
function gui:newVideoPlayer(source, x, y, w, h, sx, sy, sw, sh)
|
||||
local window = gui:newWindow(x, y, w, h, source, true, theme:new({
|
||||
primary = "#000000",
|
||||
primaryDark = "#10465c",
|
||||
primaryText = "#ffffff"
|
||||
}))
|
||||
local video = window:newVideo(source, 0, 0, 0, 0, 0, .05, 1, .75)
|
||||
local play_pause = window:newImageButton("gui/assets/play.png",0,0,0,0,.45,.82,0,.175)
|
||||
local seek = window:newFrame(0,0,0,0,0,.8,1,.015)
|
||||
seek.color = color.new("#3c434c")
|
||||
local seeker = seek:newFrame(0,0,0,0,0,.1,0,.8)
|
||||
seeker.drawBorder = false
|
||||
seeker.color = color.new("#0c278a")
|
||||
play_pause.square = "h"
|
||||
play_pause.isPaused = true
|
||||
play_pause:OnReleased(function(self)
|
||||
if self.isPaused then
|
||||
self:setImage("gui/assets/pause.png")
|
||||
video:play()
|
||||
else
|
||||
self:setImage("gui/assets/play.png")
|
||||
video:pause()
|
||||
end
|
||||
self.isPaused = not self.isPaused
|
||||
end)
|
||||
|
||||
local length = video:getDuration()
|
||||
mediaProc:newThread(function()
|
||||
while true do
|
||||
thread.yield()
|
||||
seeker:setDualDim(nil,nil,nil,nil,nil,nil,video:tell()/length)
|
||||
end
|
||||
end)
|
||||
|
||||
-- print()
|
||||
|
||||
end
|
||||
|
||||
function gui:newCheckbox(label, x, y, size, sx, sy, checked)
|
||||
local checkbox = self:newFrame(x, y, size, size, sx, sy)
|
||||
checkbox.color = color.black
|
||||
local border = checkbox:newVisualFrame(noOf(.1,.1,.8,.8))
|
||||
border.color = color.white
|
||||
local toggle = border:newFrame(noOf(.3,.3,.4,.4))
|
||||
toggle.color = color.black
|
||||
toggle.visible = false
|
||||
|
||||
checkbox:OnReleased(function()
|
||||
checkbox:check(not toggle.visible)
|
||||
end)
|
||||
|
||||
if label ~= "" then
|
||||
local text = checkbox:newTextLabel(label, noOf(1.25,0,15,1))
|
||||
text:OnUpdate(function()
|
||||
text:centerFont()
|
||||
end)
|
||||
text:setFont(size-2)
|
||||
text.visibility = 0
|
||||
end
|
||||
|
||||
function checkbox:check(value)
|
||||
toggle.visible = value
|
||||
self.OnChanged:Fire(value)
|
||||
end
|
||||
|
||||
function checkbox:isChecked()
|
||||
return toggle.visible
|
||||
end
|
||||
|
||||
function checkbox:getLabel()
|
||||
return label or ""
|
||||
end
|
||||
|
||||
checkbox.OnChanged = multi:newConnection()
|
||||
|
||||
return checkbox
|
||||
end
|
||||
|
||||
function gui:newRadioGroup(options, x, y, sx, sy, size)
|
||||
local group = {}
|
||||
local rg = self:newFrame()
|
||||
local selected
|
||||
|
||||
rg.OnSelectionChanged = multi:newConnection()
|
||||
|
||||
for i,v in ipairs(options or {}) do
|
||||
table.insert(group,self:newCheckbox(tostring(v),x,y+((i-1)*size+((options.padding or 0)*(i-1))),size,sx,sy))
|
||||
end
|
||||
|
||||
gui.apply({
|
||||
OnReleased=function(self)
|
||||
gui.apply({check={false}},unpack(group))
|
||||
self:check(true)
|
||||
if selected ~= self then
|
||||
rg.OnSelectionChanged:Fire(rg, self)
|
||||
end
|
||||
selected = self
|
||||
end,
|
||||
},unpack(group))
|
||||
|
||||
function rg:getSelectedOption()
|
||||
return selected
|
||||
end
|
||||
|
||||
return rg
|
||||
end
|
||||
|
||||
function gui:newProgressBar(x, y, w, h, sx, sy, sw, sh, count, value)
|
||||
local value = value or 0
|
||||
local progressbar = self:newFrame(x,y,w,h,sx,sy,sw,sh)
|
||||
local fillframe = progressbar:newFrame(noOf(.025, .1, .95, .8))
|
||||
local fill = fillframe:newFrame(noOf(0, 0, 1, 1))
|
||||
|
||||
fillframe.visibility = 0
|
||||
progressbar.color = color.new("#000000")
|
||||
fill.color = color.new("#ffffff")
|
||||
progressbar.fillframe = fillframe
|
||||
progressbar.fill = fill
|
||||
|
||||
function progressbar:update(value)
|
||||
if value > count then value = count end
|
||||
if value < 0 then value = 0 end
|
||||
local percent = value/count
|
||||
fill:setDualDim(noOf(nil,nil,percent))
|
||||
end
|
||||
|
||||
function progressbar:add(n)
|
||||
if value >= count then
|
||||
return
|
||||
end
|
||||
value = value + n
|
||||
self:update(value)
|
||||
end
|
||||
|
||||
function progressbar:sub(n)
|
||||
if value <= 0 then
|
||||
return
|
||||
end
|
||||
value = value - n
|
||||
self:update(value)
|
||||
end
|
||||
|
||||
function progressbar:max()
|
||||
value = count
|
||||
self:update(value)
|
||||
end
|
||||
|
||||
function progressbar:min()
|
||||
value = 0
|
||||
self:update(value)
|
||||
end
|
||||
|
||||
progressbar:update(value)
|
||||
|
||||
-- to change colors and modify main components
|
||||
return progressbar, fill, fillframe
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
-- Addons modify the gui interface directly and do not return anything.
|
||||
require("gui.addons.extensions")
|
||||
require("gui.addons.system")
|
||||
@@ -1,43 +0,0 @@
|
||||
local gui = require("gui")
|
||||
local theme = require("gui.core.theme")
|
||||
local color = require("gui.core.color")
|
||||
local multi, thread = require("multi"):init()
|
||||
require("gui.addons.system")
|
||||
local proc = gui:newProcessor()
|
||||
function gui:newVideoPlayer(source, x, y, w, h, sx, sy, sw, sh)
|
||||
local window = gui:newWindow(x, y, w, h, source, true, theme:new({
|
||||
primary = "#000000",
|
||||
primaryDark = "#10465c",
|
||||
primaryText = "#ffffff"
|
||||
}))
|
||||
local video = window:newVideo(source, 0, 0, 0, 0, 0, .05, 1, .75)
|
||||
local play_pause = window:newImageButton("gui/assets/play.png",0,0,0,0,.45,.82,0,.175)
|
||||
local seek = window:newFrame(0,0,0,0,0,.8,1,.015)
|
||||
seek.color = color.new("#3c434c")
|
||||
local seeker = seek:newFrame(0,0,0,0,0,.1,0,.8)
|
||||
seeker.drawBorder = false
|
||||
seeker.color = color.new("#0c278a")
|
||||
play_pause.square = "h"
|
||||
play_pause.isPaused = true
|
||||
play_pause:OnReleased(function(self)
|
||||
if self.isPaused then
|
||||
self:setImage("gui/assets/pause.png")
|
||||
video:play()
|
||||
else
|
||||
self:setImage("gui/assets/play.png")
|
||||
video:pause()
|
||||
end
|
||||
self.isPaused = not self.isPaused
|
||||
end)
|
||||
|
||||
local length = video:getDuration()
|
||||
proc:newThread(function()
|
||||
while true do
|
||||
thread.yield()
|
||||
seeker:setDualDim(nil,nil,nil,nil,nil,nil,video:tell()/length)
|
||||
end
|
||||
end)
|
||||
|
||||
-- print()
|
||||
|
||||
end
|
||||
+143
-144
@@ -155,149 +155,6 @@ local function collectTasks()
|
||||
return rows
|
||||
end
|
||||
|
||||
-- ── window constructor (unchanged from original) ──────────────────────────────
|
||||
local windowCount = 0
|
||||
function gui:newWindow(x, y, w, h, text, draggable, theme)
|
||||
local process = gui:newProcessor(text or "window_"..windowCount)
|
||||
windowCount = windowCount + 1
|
||||
local parent = self
|
||||
local pointer = love.mouse.getCursor()
|
||||
local sizewe = love.mouse.getSystemCursor("sizewe")
|
||||
local sizens = love.mouse.getSystemCursor("sizens")
|
||||
local sizenesw = love.mouse.getSystemCursor("sizenesw")
|
||||
local sizenwse = love.mouse.getSystemCursor("sizenwse")
|
||||
local theme = theme or default_theme
|
||||
|
||||
local header = self:newFrame(x, y, w, 35)
|
||||
header:setRoundness(10, 10, nil, "top")
|
||||
local window = header:newFrame(0, 35, 0, h - 35, 0, 0, 1)
|
||||
window.clipDescendants = true
|
||||
local left = window:newFrame(0, -4, 4, 0, 0, 0, 0, 1):tag("left")
|
||||
local right = window:newFrame(-4, -4, 4, 0, 1, 0, 0, 1):tag("right")
|
||||
local bottom = window:newFrame(4, -4, -8, 4, 0, 1, 1):tag("bottom")
|
||||
local bottomleft = window:newFrame(0, -4, 4, 4, 0, 1):tag("bleft")
|
||||
local bottomright = window:newFrame(-4, -4, 4, 4, 1, 1):tag("bright")
|
||||
gui.apply({
|
||||
visibility = 0,
|
||||
I_enableDragging = {gui.MOUSE_PRIMARY},
|
||||
respectHierarchy = {false},
|
||||
OnUpdate = function(self) self:topStack() end,
|
||||
OnDragging = function(self, dx, dy)
|
||||
local ox, oy, ow, oh = header:getAbsolutes()
|
||||
local tag = self:getTag()
|
||||
if tag == "left" or tag == "bleft" then
|
||||
window:size(0, dy)
|
||||
header:move(dx, 0)
|
||||
header:size(-dx, 0)
|
||||
else
|
||||
window:size(0, dy)
|
||||
header:size(dx, 0)
|
||||
end
|
||||
local x, y, w, h = header:getAbsolutes()
|
||||
if w < 200 and (tag == "left" or tag == "bleft") then
|
||||
header:setDualDim(ox, nil, 200)
|
||||
elseif w < 200 then
|
||||
header:setDualDim(nil, nil, 200)
|
||||
end
|
||||
local x, y, w, h = window:getAbsolutes()
|
||||
if h < 100 then window:setDualDim(nil, nil, nil, 100) end
|
||||
end,
|
||||
OnDragEnd = function(self) love.mouse.setCursor(pointer) end,
|
||||
OnEnter = function(self)
|
||||
local tag = self:getTag()
|
||||
if tag == "left" or tag == "right" then
|
||||
love.mouse.setCursor(sizewe)
|
||||
elseif tag == "bleft" then
|
||||
love.mouse.setCursor(sizenesw)
|
||||
elseif tag == "bright" then
|
||||
love.mouse.setCursor(sizenwse)
|
||||
else
|
||||
love.mouse.setCursor(sizens)
|
||||
end
|
||||
end,
|
||||
OnExit = function(self) love.mouse.setCursor(pointer) end,
|
||||
}, left, right, bottom, bottomleft, bottomright)
|
||||
|
||||
local title = header:newTextLabel(text or "", 5, 0, w - 35, 35)
|
||||
title.clipDescendants = true
|
||||
title.visibility = 0
|
||||
title.ignore = true
|
||||
title:setFont(theme.fontPrimary)
|
||||
title:fitFont()
|
||||
|
||||
function window:setTitle(t) title.text = t end
|
||||
|
||||
local X = header:newTextButton("", -25, -25, 20, 20, 1, 1)
|
||||
X:setRoundness(10, 10)
|
||||
X.align = gui.ALIGN_CENTER
|
||||
X.color = color.red
|
||||
local darkenX = color.darken(color.red, .2)
|
||||
X.OnEnter(function(self) self.color = darkenX end)
|
||||
X.OnExit(function(self) self.color = color.red end)
|
||||
|
||||
if draggable then
|
||||
header:enableDragging(gui.MOUSE_PRIMARY)
|
||||
header:OnDragging(function(self, dx, dy) self:move(dx, dy) end)
|
||||
header:OnDragEnd(function(self)
|
||||
local x, y, w, h = self:getAbsolutes()
|
||||
local width, height = love.graphics.getDimensions()
|
||||
if x <= 0 then self:setDualDim(0) end
|
||||
if y <= 0 then self:setDualDim(nil, 0) end
|
||||
if x + w >= width then self:setDualDim(width - w) end
|
||||
if y + h >= height then self:setDualDim(nil, height - 35) end
|
||||
end)
|
||||
end
|
||||
|
||||
window.OnClose = function() return window end % X.OnPressed
|
||||
window.OnClose(function()
|
||||
header:setParent(gui.virtual)
|
||||
love.mouse.setCursor(pointer)
|
||||
end)
|
||||
function window:close() window.OnClose:Fire(self) end
|
||||
function window:open() header:setParent(parent) end
|
||||
|
||||
function window:setTheme(th)
|
||||
theme = th
|
||||
title.textColor = theme.colorPrimaryText
|
||||
header.color = theme.colorPrimaryDark
|
||||
window.color = theme.colorPrimary
|
||||
end
|
||||
function window:getTheme() return theme end
|
||||
|
||||
process:newThread(function() window:setTheme(theme) end)
|
||||
|
||||
window.OnSizeChanged(function() window:refresh() end)
|
||||
function window:refresh() window:setTheme(theme) end
|
||||
|
||||
window.process = process
|
||||
window.OnCreated(function(element)
|
||||
if element:hasType(gui.TYPE_BUTTON) then
|
||||
element:setFont(theme.fontButton)
|
||||
element.color = theme.colorButtonNormal
|
||||
element.textColor = theme.colorButtonText
|
||||
if not element.__registeredTheme then
|
||||
element.OnEnter(function(self) self.color = theme.colorButtonHighlight end)
|
||||
element.OnExit(function(self) self.color = theme.colorButtonNormal end)
|
||||
end
|
||||
element:fitFont()
|
||||
element.__registeredTheme = true
|
||||
elseif element:hasType(gui.TYPE_TEXT) then
|
||||
element.color = theme.colorPrimary
|
||||
element:setFont(theme.fontPrimary)
|
||||
element.textColor = theme.colorPrimaryText
|
||||
element:fitFont()
|
||||
elseif element:hasType(gui.TYPE_FRAME) then
|
||||
if element.__isHeader then
|
||||
element.color = theme.colorPrimaryDark
|
||||
else
|
||||
element.color = theme.colorPrimary
|
||||
end
|
||||
end
|
||||
end)
|
||||
return window
|
||||
end
|
||||
|
||||
-- ── scroll frame (unchanged from original) ────────────────────────────────────
|
||||
function gui:newScrollFrame(x, y, w, h, sx, sy, sw, sh)
|
||||
local viewport = self:newFrame(x, y, w, h, sx, sy, sw, sh)
|
||||
viewport.clipDescendants = true
|
||||
@@ -453,6 +310,148 @@ function gui:newScrollFrame(x, y, w, h, sx, sy, sw, sh)
|
||||
return content
|
||||
end
|
||||
|
||||
-- ── window constructor (unchanged from original) ──────────────────────────────
|
||||
local windowCount = 0
|
||||
function gui:newWindow(x, y, w, h, text, draggable, theme)
|
||||
local process = gui:newProcessor(text or "window_"..windowCount)
|
||||
windowCount = windowCount + 1
|
||||
local parent = self
|
||||
local pointer = love.mouse.getCursor()
|
||||
local sizewe = love.mouse.getSystemCursor("sizewe")
|
||||
local sizens = love.mouse.getSystemCursor("sizens")
|
||||
local sizenesw = love.mouse.getSystemCursor("sizenesw")
|
||||
local sizenwse = love.mouse.getSystemCursor("sizenwse")
|
||||
local theme = theme or default_theme
|
||||
|
||||
local header = self:newFrame(x, y, w, 35)
|
||||
header:setRoundness(10, 10, nil, "top")
|
||||
local window = header:newFrame(0, 35, 0, h - 35, 0, 0, 1)
|
||||
window.clipDescendants = true
|
||||
local left = window:newFrame(0, -4, 4, 0, 0, 0, 0, 1):tag("left")
|
||||
local right = window:newFrame(-4, -4, 4, 0, 1, 0, 0, 1):tag("right")
|
||||
local bottom = window:newFrame(4, -4, -8, 4, 0, 1, 1):tag("bottom")
|
||||
local bottomleft = window:newFrame(0, -4, 4, 4, 0, 1):tag("bleft")
|
||||
local bottomright = window:newFrame(-4, -4, 4, 4, 1, 1):tag("bright")
|
||||
gui.apply({
|
||||
visibility = 0,
|
||||
I_enableDragging = {gui.MOUSE_PRIMARY},
|
||||
respectHierarchy = {false},
|
||||
OnUpdate = function(self) self:topStack() end,
|
||||
OnDragging = function(self, dx, dy)
|
||||
local ox, oy, ow, oh = header:getAbsolutes()
|
||||
local tag = self:getTag()
|
||||
if tag == "left" or tag == "bleft" then
|
||||
window:size(0, dy)
|
||||
header:move(dx, 0)
|
||||
header:size(-dx, 0)
|
||||
else
|
||||
window:size(0, dy)
|
||||
header:size(dx, 0)
|
||||
end
|
||||
local x, y, w, h = header:getAbsolutes()
|
||||
if w < 200 and (tag == "left" or tag == "bleft") then
|
||||
header:setDualDim(ox, nil, 200)
|
||||
elseif w < 200 then
|
||||
header:setDualDim(nil, nil, 200)
|
||||
end
|
||||
local x, y, w, h = window:getAbsolutes()
|
||||
if h < 100 then window:setDualDim(nil, nil, nil, 100) end
|
||||
end,
|
||||
OnDragEnd = function(self) love.mouse.setCursor(pointer) end,
|
||||
OnEnter = function(self)
|
||||
local tag = self:getTag()
|
||||
if tag == "left" or tag == "right" then
|
||||
love.mouse.setCursor(sizewe)
|
||||
elseif tag == "bleft" then
|
||||
love.mouse.setCursor(sizenesw)
|
||||
elseif tag == "bright" then
|
||||
love.mouse.setCursor(sizenwse)
|
||||
else
|
||||
love.mouse.setCursor(sizens)
|
||||
end
|
||||
end,
|
||||
OnExit = function(self) love.mouse.setCursor(pointer) end,
|
||||
}, left, right, bottom, bottomleft, bottomright)
|
||||
|
||||
local title = header:newTextLabel(text or "", 5, 0, w - 35, 35)
|
||||
title.clipDescendants = true
|
||||
title.visibility = 0
|
||||
title.ignore = true
|
||||
title:setFont(theme.fontPrimary)
|
||||
title:fitFont()
|
||||
|
||||
function window:setTitle(t) title.text = t end
|
||||
|
||||
local X = header:newTextButton("", -25, -25, 20, 20, 1, 1)
|
||||
X:setRoundness(10, 10)
|
||||
X.align = gui.ALIGN_CENTER
|
||||
X.color = color.red
|
||||
local darkenX = color.darken(color.red, .2)
|
||||
X.OnEnter(function(self) self.color = darkenX end)
|
||||
X.OnExit(function(self) self.color = color.red end)
|
||||
|
||||
if draggable then
|
||||
header:enableDragging(gui.MOUSE_PRIMARY)
|
||||
header:OnDragging(function(self, dx, dy) self:move(dx, dy) end)
|
||||
header:OnDragEnd(function(self)
|
||||
local x, y, w, h = self:getAbsolutes()
|
||||
local width, height = love.graphics.getDimensions()
|
||||
if x <= 0 then self:setDualDim(0) end
|
||||
if y <= 0 then self:setDualDim(nil, 0) end
|
||||
if x + w >= width then self:setDualDim(width - w) end
|
||||
if y + h >= height then self:setDualDim(nil, height - 35) end
|
||||
end)
|
||||
end
|
||||
|
||||
window.OnClose = function() return window end % X.OnPressed
|
||||
window.OnClose(function()
|
||||
header:setParent(gui.virtual)
|
||||
love.mouse.setCursor(pointer)
|
||||
end)
|
||||
function window:close() window.OnClose:Fire(self) end
|
||||
function window:open() header:setParent(parent) end
|
||||
|
||||
function window:setTheme(th)
|
||||
theme = th
|
||||
title.textColor = theme.colorPrimaryText
|
||||
header.color = theme.colorPrimaryDark
|
||||
window.color = theme.colorPrimary
|
||||
end
|
||||
function window:getTheme() return theme end
|
||||
|
||||
process:newThread(function() window:setTheme(theme) end)
|
||||
|
||||
window.OnSizeChanged(function() window:refresh() end)
|
||||
function window:refresh() window:setTheme(theme) end
|
||||
|
||||
window.process = process
|
||||
window.OnCreated(function(element)
|
||||
if element:hasType(gui.TYPE_BUTTON) then
|
||||
element:setFont(theme.fontButton)
|
||||
element.color = theme.colorButtonNormal
|
||||
element.textColor = theme.colorButtonText
|
||||
if not element.__registeredTheme then
|
||||
element.OnEnter(function(self) self.color = theme.colorButtonHighlight end)
|
||||
element.OnExit(function(self) self.color = theme.colorButtonNormal end)
|
||||
end
|
||||
element:fitFont()
|
||||
element.__registeredTheme = true
|
||||
elseif element:hasType(gui.TYPE_TEXT) then
|
||||
element.color = theme.colorPrimary
|
||||
element:setFont(theme.fontPrimary)
|
||||
element.textColor = theme.colorPrimaryText
|
||||
element:fitFont()
|
||||
elseif element:hasType(gui.TYPE_FRAME) then
|
||||
if element.__isHeader then
|
||||
element.color = theme.colorPrimaryDark
|
||||
else
|
||||
element.color = theme.colorPrimary
|
||||
end
|
||||
end
|
||||
end)
|
||||
return window
|
||||
end
|
||||
|
||||
-- ── row pool ──────────────────────────────────────────────────────────────────
|
||||
local COLOR_PROC_ROW = TM_THEME.colorPrimaryDark
|
||||
local COLOR_ROW_EVEN = TM_THEME.colorPrimary
|
||||
@@ -896,7 +895,7 @@ function gui:showTaskManager()
|
||||
|
||||
-- ── load probe ────────────────────────────────────────────────────────────
|
||||
-- Install once. getLoad() is now non-blocking — just reads the EMA state.
|
||||
local schedulerProbe = require("gui.addons.probe")
|
||||
local schedulerProbe = require("gui.core.probe")
|
||||
schedulerProbe:install(multi)
|
||||
|
||||
-- ── main-thread update ────────────────────────────────────────────────────
|
||||
|
||||
+9
-9
@@ -1,22 +1,22 @@
|
||||
local color={}
|
||||
local mt = {
|
||||
__add = function (c1,c2)
|
||||
return color.new(c1[1]+c2[1],c1[2]+c2[2],c1[2]+c2[2])
|
||||
return color.new(c1[1]+c2[1],c1[2]+c2[2],c1[3]+c2[3])
|
||||
end,
|
||||
__sub = function (c1,c2)
|
||||
return color.new(c1[1]-c2[1],c1[2]-c2[2],c1[2]-c2[2])
|
||||
return color.new(c1[1]-c2[1],c1[2]-c2[2],c1[3]-c2[3])
|
||||
end,
|
||||
__mul = function (c1,c2)
|
||||
return color.new(c1[1]*c2[1],c1[2]*c2[2],c1[2]*c2[2])
|
||||
return color.new(c1[1]*c2[1],c1[2]*c2[2],c1[3]*c2[3])
|
||||
end,
|
||||
__div = function (c1,c2)
|
||||
return color.new(c1[1]/c2[1],c1[2]/c2[2],c1[2]/c2[2])
|
||||
return color.new(c1[1]/c2[1],c1[2]/c2[2],c1[3]/c2[3])
|
||||
end,
|
||||
__mod = function (c1,c2)
|
||||
return color.new(c1[1]%c2[1],c1[2]%c2[2],c1[2]%c2[2])
|
||||
return color.new(c1[1]%c2[1],c1[2]%c2[2],c1[3]%c2[3])
|
||||
end,
|
||||
__pow = function (c1,c2)
|
||||
return color.new(c1[1]^c2[1],c1[2]^c2[2],c1[2]^c2[2])
|
||||
return color.new(c1[1]^c2[1],c1[2]^c2[2],c1[3]^c2[3])
|
||||
end,
|
||||
__unm = function (c1)
|
||||
return color.new(-c1[1],-c1[2],-c1[2])
|
||||
@@ -25,13 +25,13 @@ local mt = {
|
||||
return "("..c[1]..","..c[2]..","..c[3]..",".. (c[4] or "1") ..")"
|
||||
end,
|
||||
__eq = function (c1,c2)
|
||||
return (c1[1]==c2[1] and c1[2]==c2[2] and c1[2]==c2[2])
|
||||
return (c1[1]==c2[1] and c1[2]==c2[2] and c1[3]==c2[3])
|
||||
end,
|
||||
__lt = function (c1,c2)
|
||||
return (c1[1]<c2[1] and c1[2]<c2[2] and c1[2]<c2[2])
|
||||
return (c1[1]<c2[1] and c1[2]<c2[2] and c1[3]<c2[3])
|
||||
end,
|
||||
__le = function (c1,c2)
|
||||
return (c1[1]<=c2[1] and c1[2]<=c2[2] and c1[2]<=c2[2])
|
||||
return (c1[1]<=c2[1] and c1[2]<=c2[2] and c1[3]<=c2[3])
|
||||
end
|
||||
}
|
||||
|
||||
|
||||
@@ -1,458 +1,437 @@
|
||||
-- GIF Loader for Love2D with LZW Decompression
|
||||
-- Note: love.data.compress/decompress don't support LZW, so we implement it
|
||||
|
||||
local GifLoader = {}
|
||||
|
||||
-- Pure Lua LZW decompression for GIF
|
||||
local function decompressLZW(data, minCodeSize)
|
||||
local clearCode = 2 ^ minCodeSize
|
||||
local endCode = clearCode + 1
|
||||
local nextCode = endCode + 1
|
||||
local codeSize = minCodeSize + 1
|
||||
|
||||
local dict = {}
|
||||
for i = 0, clearCode - 1 do
|
||||
dict[i] = {string.byte(string.char(i))}
|
||||
end
|
||||
|
||||
local output = {}
|
||||
local bits = 0
|
||||
local bitBuffer = 0
|
||||
local pos = 1
|
||||
local prevCode = nil
|
||||
|
||||
local function readCode()
|
||||
while bits < codeSize do
|
||||
if pos > #data then return nil end
|
||||
bitBuffer = bitBuffer + bit.lshift(string.byte(data, pos), bits)
|
||||
bits = bits + 8
|
||||
pos = pos + 1
|
||||
end
|
||||
|
||||
local code = bit.band(bitBuffer, bit.lshift(1, codeSize) - 1)
|
||||
bitBuffer = bit.rshift(bitBuffer, codeSize)
|
||||
bits = bits - codeSize
|
||||
return code
|
||||
end
|
||||
|
||||
local first = true
|
||||
|
||||
while true do
|
||||
local code = readCode()
|
||||
if not code or code == endCode then break end
|
||||
|
||||
if code == clearCode then
|
||||
dict = {}
|
||||
for i = 0, clearCode - 1 do
|
||||
dict[i] = {string.byte(string.char(i))}
|
||||
end
|
||||
nextCode = endCode + 1
|
||||
codeSize = minCodeSize + 1
|
||||
prevCode = nil
|
||||
first = true
|
||||
else
|
||||
local entry
|
||||
if dict[code] then
|
||||
entry = dict[code]
|
||||
elseif code == nextCode and prevCode then
|
||||
-- Special case: code not in dict yet
|
||||
entry = {}
|
||||
for i = 1, #dict[prevCode] do
|
||||
entry[i] = dict[prevCode][i]
|
||||
end
|
||||
entry[#entry + 1] = dict[prevCode][1]
|
||||
else
|
||||
-- Invalid code, stop
|
||||
break
|
||||
end
|
||||
|
||||
-- Output the entry
|
||||
for i = 1, #entry do
|
||||
table.insert(output, entry[i])
|
||||
end
|
||||
|
||||
-- Add new entry to dictionary
|
||||
if not first and prevCode and nextCode < 4096 then
|
||||
local newEntry = {}
|
||||
for i = 1, #dict[prevCode] do
|
||||
newEntry[i] = dict[prevCode][i]
|
||||
end
|
||||
newEntry[#newEntry + 1] = entry[1]
|
||||
dict[nextCode] = newEntry
|
||||
nextCode = nextCode + 1
|
||||
|
||||
-- Increase code size when needed
|
||||
if nextCode >= bit.lshift(1, codeSize) and codeSize < 12 then
|
||||
codeSize = codeSize + 1
|
||||
end
|
||||
end
|
||||
|
||||
prevCode = code
|
||||
first = false
|
||||
end
|
||||
end
|
||||
|
||||
-- Convert output bytes to string
|
||||
local result = {}
|
||||
for i = 1, #output do
|
||||
result[i] = string.char(output[i])
|
||||
end
|
||||
return table.concat(result)
|
||||
end
|
||||
|
||||
function GifLoader.load(filepath)
|
||||
local fileData = love.filesystem.read(filepath)
|
||||
if not fileData then
|
||||
error("Could not read GIF file: " .. filepath)
|
||||
end
|
||||
|
||||
local gif = {
|
||||
frames = {},
|
||||
frameData = {}, -- Store ImageData for frame composition
|
||||
delays = {},
|
||||
currentFrame = 1,
|
||||
timer = 0,
|
||||
width = 0,
|
||||
height = 0,
|
||||
playing = true,
|
||||
loop = true,
|
||||
getWidth = function(self)
|
||||
return self.width
|
||||
end,
|
||||
getHeight = function(self)
|
||||
return self.height
|
||||
end,
|
||||
}
|
||||
|
||||
-- Parse GIF header
|
||||
local header = fileData:sub(1, 6)
|
||||
if header ~= "GIF87a" and header ~= "GIF89a" then
|
||||
error("Not a valid GIF file")
|
||||
end
|
||||
|
||||
-- Read logical screen descriptor
|
||||
local pos = 7
|
||||
gif.width = string.byte(fileData, pos) + string.byte(fileData, pos + 1) * 256
|
||||
gif.height = string.byte(fileData, pos + 2) + string.byte(fileData, pos + 3) * 256
|
||||
|
||||
local packed = string.byte(fileData, pos + 4)
|
||||
local hasGlobalColorTable = bit.band(packed, 0x80) ~= 0
|
||||
local backgroundColorIndex = string.byte(fileData, pos + 5)
|
||||
|
||||
pos = pos + 7
|
||||
|
||||
-- Read global color table
|
||||
local globalColorTable = {}
|
||||
if hasGlobalColorTable then
|
||||
local size = 2 ^ (bit.band(packed, 0x07) + 1)
|
||||
for i = 1, size do
|
||||
local r = string.byte(fileData, pos) / 255
|
||||
local g = string.byte(fileData, pos + 1) / 255
|
||||
local b = string.byte(fileData, pos + 2) / 255
|
||||
table.insert(globalColorTable, {r, g, b, 1})
|
||||
pos = pos + 3
|
||||
end
|
||||
end
|
||||
|
||||
-- Parse blocks
|
||||
local delay = 0.1
|
||||
local transparentIndex = nil
|
||||
local disposalMethod = 0
|
||||
local delayForNextFrame = 0.1 -- Track delay for next frame
|
||||
|
||||
while pos <= #fileData do
|
||||
local separator = string.byte(fileData, pos)
|
||||
|
||||
if separator == 0x21 then -- Extension
|
||||
local label = string.byte(fileData, pos + 1)
|
||||
pos = pos + 2
|
||||
|
||||
if label == 0xF9 then -- Graphic Control Extension
|
||||
local blockSize = string.byte(fileData, pos)
|
||||
pos = pos + 1
|
||||
|
||||
local flags = string.byte(fileData, pos)
|
||||
disposalMethod = bit.rshift(bit.band(flags, 0x1C), 2)
|
||||
local hasTransparency = bit.band(flags, 0x01) ~= 0
|
||||
|
||||
local delayTime = string.byte(fileData, pos + 1) + string.byte(fileData, pos + 2) * 256
|
||||
-- GIF delay is in hundredths of a second, convert to seconds
|
||||
-- Many GIFs use 0 or very small delays, set a minimum
|
||||
if delayTime == 0 then
|
||||
delayForNextFrame = 0.1 -- Default 100ms
|
||||
elseif delayTime <= 2 then
|
||||
delayForNextFrame = 0.02 -- Minimum 20ms for very fast animations
|
||||
else
|
||||
delayForNextFrame = delayTime / 100
|
||||
end
|
||||
|
||||
if hasTransparency then
|
||||
transparentIndex = string.byte(fileData, pos + 3)
|
||||
else
|
||||
transparentIndex = nil
|
||||
end
|
||||
|
||||
pos = pos + blockSize + 1
|
||||
else
|
||||
-- Skip other extensions
|
||||
repeat
|
||||
local blockSize = string.byte(fileData, pos)
|
||||
pos = pos + 1
|
||||
if blockSize > 0 then
|
||||
pos = pos + blockSize
|
||||
end
|
||||
until blockSize == 0
|
||||
end
|
||||
|
||||
elseif separator == 0x2C then -- Image Descriptor
|
||||
pos = pos + 1
|
||||
|
||||
local left = string.byte(fileData, pos) + string.byte(fileData, pos + 1) * 256
|
||||
local top = string.byte(fileData, pos + 2) + string.byte(fileData, pos + 3) * 256
|
||||
local width = string.byte(fileData, pos + 4) + string.byte(fileData, pos + 5) * 256
|
||||
local height = string.byte(fileData, pos + 6) + string.byte(fileData, pos + 7) * 256
|
||||
|
||||
local imgPacked = string.byte(fileData, pos + 8)
|
||||
local hasLocalColorTable = bit.band(imgPacked, 0x80) ~= 0
|
||||
local interlaced = bit.band(imgPacked, 0x40) ~= 0
|
||||
|
||||
pos = pos + 9
|
||||
|
||||
local colorTable = globalColorTable
|
||||
if hasLocalColorTable then
|
||||
local size = 2 ^ (bit.band(imgPacked, 0x07) + 1)
|
||||
colorTable = {}
|
||||
for i = 1, size do
|
||||
local r = string.byte(fileData, pos) / 255
|
||||
local g = string.byte(fileData, pos + 1) / 255
|
||||
local b = string.byte(fileData, pos + 2) / 255
|
||||
table.insert(colorTable, {r, g, b, 1})
|
||||
pos = pos + 3
|
||||
end
|
||||
end
|
||||
|
||||
-- Read LZW minimum code size
|
||||
local minCodeSize = string.byte(fileData, pos)
|
||||
pos = pos + 1
|
||||
|
||||
-- Read compressed image data blocks
|
||||
local compressedData = {}
|
||||
while true do
|
||||
local blockSize = string.byte(fileData, pos)
|
||||
pos = pos + 1
|
||||
if blockSize == 0 then break end
|
||||
table.insert(compressedData, fileData:sub(pos, pos + blockSize - 1))
|
||||
pos = pos + blockSize
|
||||
end
|
||||
|
||||
-- Decompress image data
|
||||
local indexStream = decompressLZW(table.concat(compressedData), minCodeSize)
|
||||
|
||||
-- Create image data
|
||||
local imageData = love.image.newImageData(gif.width, gif.height)
|
||||
|
||||
-- Fill with background if first frame
|
||||
if #gif.frames == 0 and backgroundColorIndex and globalColorTable[backgroundColorIndex + 1] then
|
||||
local bg = globalColorTable[backgroundColorIndex + 1]
|
||||
for y = 0, gif.height - 1 do
|
||||
for x = 0, gif.width - 1 do
|
||||
imageData:setPixel(x, y, bg[1], bg[2], bg[3], bg[4])
|
||||
end
|
||||
end
|
||||
elseif #gif.frames > 0 then
|
||||
-- Copy previous frame if needed
|
||||
local prevData = gif.frameData[#gif.frameData]
|
||||
imageData:paste(prevData, 0, 0, 0, 0, gif.width, gif.height)
|
||||
end
|
||||
|
||||
-- Draw current frame
|
||||
if indexStream and #indexStream > 0 then
|
||||
local idx = 1
|
||||
|
||||
-- Use mapPixel for faster pixel operations
|
||||
local function setPixels(x, y, r, g, b, a)
|
||||
if x >= left and x < left + width and y >= top and y < top + height then
|
||||
local pixelIdx = (y - top) * width + (x - left) + 1
|
||||
if pixelIdx <= #indexStream then
|
||||
local colorIndex = string.byte(indexStream, pixelIdx)
|
||||
|
||||
-- Skip transparent pixels
|
||||
if transparentIndex == nil or colorIndex ~= transparentIndex then
|
||||
if colorTable[colorIndex + 1] then
|
||||
local color = colorTable[colorIndex + 1]
|
||||
return color[1], color[2], color[3], color[4]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return r, g, b, a
|
||||
end
|
||||
|
||||
-- Only update the region where the frame is located
|
||||
for y = top, top + height - 1 do
|
||||
for x = left, left + width - 1 do
|
||||
local pixelIdx = (y - top) * width + (x - left) + 1
|
||||
if pixelIdx <= #indexStream then
|
||||
local colorIndex = string.byte(indexStream, pixelIdx)
|
||||
|
||||
-- Skip transparent pixels
|
||||
if transparentIndex == nil or colorIndex ~= transparentIndex then
|
||||
if colorTable[colorIndex + 1] then
|
||||
local color = colorTable[colorIndex + 1]
|
||||
imageData:setPixel(x, y, color[1], color[2], color[3], color[4])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(gif.frameData, imageData)
|
||||
table.insert(gif.frames, love.graphics.newImage(imageData))
|
||||
table.insert(gif.delays, delayForNextFrame)
|
||||
|
||||
-- Reset delay for next frame
|
||||
delayForNextFrame = 0.1
|
||||
|
||||
elseif separator == 0x3B then -- Trailer
|
||||
break
|
||||
else
|
||||
pos = pos + 1
|
||||
end
|
||||
end
|
||||
|
||||
if #gif.frames == 0 then
|
||||
error("No frames found in GIF")
|
||||
end
|
||||
|
||||
-- Ensure all frames have valid delays
|
||||
for i = 1, #gif.delays do
|
||||
if gif.delays[i] <= 0 or gif.delays[i] ~= gif.delays[i] then -- check for 0 or NaN
|
||||
gif.delays[i] = 0.1
|
||||
end
|
||||
end
|
||||
|
||||
return gif
|
||||
end
|
||||
|
||||
function GifLoader.Updater(gif, proc)
|
||||
local wait = function()
|
||||
return gif.playing or gif.kill
|
||||
end
|
||||
proc:newThread("Gif Handler",function()
|
||||
while true do
|
||||
-- Only run if not paused
|
||||
if gif.kill then -- When we want to clean up
|
||||
thread.kill()
|
||||
end
|
||||
thread.hold(wait)
|
||||
thread.sleep(gif.delays[gif.currentFrame] * 4)
|
||||
gif.currentFrame = gif.currentFrame + 1
|
||||
|
||||
if gif.currentFrame > #gif.frames then
|
||||
if gif.loop then
|
||||
gif.currentFrame = 1
|
||||
else
|
||||
gif.currentFrame = #gif.frames
|
||||
gif.playing = false
|
||||
gif.timer = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function GifLoader.update(gif, dt)
|
||||
if not gif.playing or #gif.frames <= 1 then return end
|
||||
|
||||
gif.timer = gif.timer + dt
|
||||
|
||||
-- Simple, accurate frame advancement
|
||||
if gif.timer >= gif.delays[gif.currentFrame] then
|
||||
-- Subtract the current frame's delay
|
||||
gif.timer = gif.timer - gif.delays[gif.currentFrame]
|
||||
|
||||
-- Move to next frame
|
||||
gif.currentFrame = gif.currentFrame + 1
|
||||
|
||||
if gif.currentFrame > #gif.frames then
|
||||
if gif.loop then
|
||||
gif.currentFrame = 1
|
||||
else
|
||||
gif.currentFrame = #gif.frames
|
||||
gif.playing = false
|
||||
gif.timer = 0
|
||||
end
|
||||
end
|
||||
|
||||
-- If we've accumulated too much time (lag spike), cap it
|
||||
if gif.timer > 0.5 then
|
||||
gif.timer = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function GifLoader.draw(gif, x, y, r, sx, sy, ox, oy)
|
||||
if gif.frames[gif.currentFrame] then
|
||||
love.graphics.draw(gif.frames[gif.currentFrame], x, y, r or 0, sx or 1, sy or 1, ox or 0, oy or 0)
|
||||
end
|
||||
end
|
||||
|
||||
function GifLoader.play(gif)
|
||||
gif.playing = true
|
||||
end
|
||||
|
||||
function GifLoader.pause(gif)
|
||||
gif.playing = false
|
||||
end
|
||||
|
||||
function GifLoader.reset(gif)
|
||||
gif.currentFrame = 1
|
||||
gif.timer = 0
|
||||
end
|
||||
|
||||
function GifLoader.setFixedFramerate(gif, fps)
|
||||
local delay = 1 / fps
|
||||
for i = 1, #gif.delays do
|
||||
gif.delays[i] = delay
|
||||
end
|
||||
end
|
||||
|
||||
function GifLoader.getInfo(gif)
|
||||
return {
|
||||
width = gif.width,
|
||||
height = gif.height,
|
||||
frameCount = #gif.frames,
|
||||
delays = gif.delays, -- Show actual delays for debugging
|
||||
totalDuration = (function()
|
||||
local total = 0
|
||||
for i = 1, #gif.delays do
|
||||
total = total + gif.delays[i]
|
||||
end
|
||||
return total
|
||||
end)()
|
||||
}
|
||||
end
|
||||
|
||||
return GifLoader
|
||||
|
||||
-- USAGE:
|
||||
--[[
|
||||
local GifLoader = require("gifloader")
|
||||
|
||||
function love.load()
|
||||
myGif = GifLoader.load("animation.gif")
|
||||
myGif.loop = true
|
||||
end
|
||||
|
||||
function love.update(dt)
|
||||
GifLoader.update(myGif, dt)
|
||||
end
|
||||
|
||||
function love.draw()
|
||||
GifLoader.draw(myGif, 100, 100)
|
||||
|
||||
-- Draw scaled
|
||||
GifLoader.draw(myGif, 300, 100, 0, 2, 2)
|
||||
end
|
||||
]]
|
||||
-- GIF Loader for Love2D with LZW Decompression
|
||||
-- Note: love.data.compress/decompress don't support LZW, so we implement it
|
||||
|
||||
local GifLoader = {}
|
||||
|
||||
-- Pure Lua LZW decompression for GIF
|
||||
local function decompressLZW(data, minCodeSize)
|
||||
local clearCode = 2 ^ minCodeSize
|
||||
local endCode = clearCode + 1
|
||||
local nextCode = endCode + 1
|
||||
local codeSize = minCodeSize + 1
|
||||
|
||||
local dict = {}
|
||||
for i = 0, clearCode - 1 do
|
||||
dict[i] = {string.byte(string.char(i))}
|
||||
end
|
||||
|
||||
local output = {}
|
||||
local bits = 0
|
||||
local bitBuffer = 0
|
||||
local pos = 1
|
||||
local prevCode = nil
|
||||
|
||||
local function readCode()
|
||||
while bits < codeSize do
|
||||
if pos > #data then return nil end
|
||||
bitBuffer = bitBuffer + bit.lshift(string.byte(data, pos), bits)
|
||||
bits = bits + 8
|
||||
pos = pos + 1
|
||||
end
|
||||
|
||||
local code = bit.band(bitBuffer, bit.lshift(1, codeSize) - 1)
|
||||
bitBuffer = bit.rshift(bitBuffer, codeSize)
|
||||
bits = bits - codeSize
|
||||
return code
|
||||
end
|
||||
|
||||
local first = true
|
||||
|
||||
while true do
|
||||
local code = readCode()
|
||||
if not code or code == endCode then break end
|
||||
|
||||
if code == clearCode then
|
||||
dict = {}
|
||||
for i = 0, clearCode - 1 do
|
||||
dict[i] = {string.byte(string.char(i))}
|
||||
end
|
||||
nextCode = endCode + 1
|
||||
codeSize = minCodeSize + 1
|
||||
prevCode = nil
|
||||
first = true
|
||||
else
|
||||
local entry
|
||||
if dict[code] then
|
||||
entry = dict[code]
|
||||
elseif code == nextCode and prevCode then
|
||||
-- Special case: code not in dict yet
|
||||
entry = {}
|
||||
for i = 1, #dict[prevCode] do
|
||||
entry[i] = dict[prevCode][i]
|
||||
end
|
||||
entry[#entry + 1] = dict[prevCode][1]
|
||||
else
|
||||
-- Invalid code, stop
|
||||
break
|
||||
end
|
||||
|
||||
-- Output the entry
|
||||
for i = 1, #entry do
|
||||
table.insert(output, entry[i])
|
||||
end
|
||||
|
||||
-- Add new entry to dictionary
|
||||
if not first and prevCode and nextCode < 4096 then
|
||||
local newEntry = {}
|
||||
for i = 1, #dict[prevCode] do
|
||||
newEntry[i] = dict[prevCode][i]
|
||||
end
|
||||
newEntry[#newEntry + 1] = entry[1]
|
||||
dict[nextCode] = newEntry
|
||||
nextCode = nextCode + 1
|
||||
|
||||
-- Increase code size when needed
|
||||
if nextCode >= bit.lshift(1, codeSize) and codeSize < 12 then
|
||||
codeSize = codeSize + 1
|
||||
end
|
||||
end
|
||||
|
||||
prevCode = code
|
||||
first = false
|
||||
end
|
||||
end
|
||||
|
||||
-- Convert output bytes to string
|
||||
local result = {}
|
||||
for i = 1, #output do
|
||||
result[i] = string.char(output[i])
|
||||
end
|
||||
return table.concat(result)
|
||||
end
|
||||
|
||||
function GifLoader.load(filepath)
|
||||
local fileData = love.filesystem.read(filepath)
|
||||
if not fileData then
|
||||
error("Could not read GIF file: " .. filepath)
|
||||
end
|
||||
|
||||
local gif = {
|
||||
frames = {},
|
||||
frameData = {}, -- Store ImageData for frame composition
|
||||
delays = {},
|
||||
currentFrame = 1,
|
||||
timer = 0,
|
||||
width = 0,
|
||||
height = 0,
|
||||
playing = true,
|
||||
loop = true,
|
||||
getWidth = function(self)
|
||||
return self.width
|
||||
end,
|
||||
getHeight = function(self)
|
||||
return self.height
|
||||
end,
|
||||
}
|
||||
|
||||
-- Parse GIF header
|
||||
local header = fileData:sub(1, 6)
|
||||
if header ~= "GIF87a" and header ~= "GIF89a" then
|
||||
error("Not a valid GIF file")
|
||||
end
|
||||
|
||||
-- Read logical screen descriptor
|
||||
local pos = 7
|
||||
gif.width = string.byte(fileData, pos) + string.byte(fileData, pos + 1) * 256
|
||||
gif.height = string.byte(fileData, pos + 2) + string.byte(fileData, pos + 3) * 256
|
||||
|
||||
local packed = string.byte(fileData, pos + 4)
|
||||
local hasGlobalColorTable = bit.band(packed, 0x80) ~= 0
|
||||
local backgroundColorIndex = string.byte(fileData, pos + 5)
|
||||
|
||||
pos = pos + 7
|
||||
|
||||
-- Read global color table
|
||||
local globalColorTable = {}
|
||||
if hasGlobalColorTable then
|
||||
local size = 2 ^ (bit.band(packed, 0x07) + 1)
|
||||
for i = 1, size do
|
||||
local r = string.byte(fileData, pos) / 255
|
||||
local g = string.byte(fileData, pos + 1) / 255
|
||||
local b = string.byte(fileData, pos + 2) / 255
|
||||
table.insert(globalColorTable, {r, g, b, 1})
|
||||
pos = pos + 3
|
||||
end
|
||||
end
|
||||
|
||||
-- Parse blocks
|
||||
local delay = 0.1
|
||||
local transparentIndex = nil
|
||||
local disposalMethod = 0
|
||||
local delayForNextFrame = 0.1 -- Track delay for next frame
|
||||
|
||||
while pos <= #fileData do
|
||||
local separator = string.byte(fileData, pos)
|
||||
|
||||
if separator == 0x21 then -- Extension
|
||||
local label = string.byte(fileData, pos + 1)
|
||||
pos = pos + 2
|
||||
|
||||
if label == 0xF9 then -- Graphic Control Extension
|
||||
local blockSize = string.byte(fileData, pos)
|
||||
pos = pos + 1
|
||||
|
||||
local flags = string.byte(fileData, pos)
|
||||
disposalMethod = bit.rshift(bit.band(flags, 0x1C), 2)
|
||||
local hasTransparency = bit.band(flags, 0x01) ~= 0
|
||||
|
||||
local delayTime = string.byte(fileData, pos + 1) + string.byte(fileData, pos + 2) * 256
|
||||
-- GIF delay is in hundredths of a second, convert to seconds
|
||||
-- Many GIFs use 0 or very small delays, set a minimum
|
||||
if delayTime == 0 then
|
||||
delayForNextFrame = 0.1 -- Default 100ms
|
||||
elseif delayTime <= 2 then
|
||||
delayForNextFrame = 0.02 -- Minimum 20ms for very fast animations
|
||||
else
|
||||
delayForNextFrame = delayTime / 100
|
||||
end
|
||||
|
||||
if hasTransparency then
|
||||
transparentIndex = string.byte(fileData, pos + 3)
|
||||
else
|
||||
transparentIndex = nil
|
||||
end
|
||||
|
||||
pos = pos + blockSize + 1
|
||||
else
|
||||
-- Skip other extensions
|
||||
repeat
|
||||
local blockSize = string.byte(fileData, pos)
|
||||
pos = pos + 1
|
||||
if blockSize > 0 then
|
||||
pos = pos + blockSize
|
||||
end
|
||||
until blockSize == 0
|
||||
end
|
||||
|
||||
elseif separator == 0x2C then -- Image Descriptor
|
||||
pos = pos + 1
|
||||
|
||||
local left = string.byte(fileData, pos) + string.byte(fileData, pos + 1) * 256
|
||||
local top = string.byte(fileData, pos + 2) + string.byte(fileData, pos + 3) * 256
|
||||
local width = string.byte(fileData, pos + 4) + string.byte(fileData, pos + 5) * 256
|
||||
local height = string.byte(fileData, pos + 6) + string.byte(fileData, pos + 7) * 256
|
||||
|
||||
local imgPacked = string.byte(fileData, pos + 8)
|
||||
local hasLocalColorTable = bit.band(imgPacked, 0x80) ~= 0
|
||||
local interlaced = bit.band(imgPacked, 0x40) ~= 0
|
||||
|
||||
pos = pos + 9
|
||||
|
||||
local colorTable = globalColorTable
|
||||
if hasLocalColorTable then
|
||||
local size = 2 ^ (bit.band(imgPacked, 0x07) + 1)
|
||||
colorTable = {}
|
||||
for i = 1, size do
|
||||
local r = string.byte(fileData, pos) / 255
|
||||
local g = string.byte(fileData, pos + 1) / 255
|
||||
local b = string.byte(fileData, pos + 2) / 255
|
||||
table.insert(colorTable, {r, g, b, 1})
|
||||
pos = pos + 3
|
||||
end
|
||||
end
|
||||
|
||||
-- Read LZW minimum code size
|
||||
local minCodeSize = string.byte(fileData, pos)
|
||||
pos = pos + 1
|
||||
|
||||
-- Read compressed image data blocks
|
||||
local compressedData = {}
|
||||
while true do
|
||||
local blockSize = string.byte(fileData, pos)
|
||||
pos = pos + 1
|
||||
if blockSize == 0 then break end
|
||||
table.insert(compressedData, fileData:sub(pos, pos + blockSize - 1))
|
||||
pos = pos + blockSize
|
||||
end
|
||||
|
||||
-- Decompress image data
|
||||
local indexStream = decompressLZW(table.concat(compressedData), minCodeSize)
|
||||
|
||||
-- Create image data
|
||||
local imageData = love.image.newImageData(gif.width, gif.height)
|
||||
|
||||
-- Fill with background if first frame
|
||||
if #gif.frames == 0 and backgroundColorIndex and globalColorTable[backgroundColorIndex + 1] then
|
||||
local bg = globalColorTable[backgroundColorIndex + 1]
|
||||
for y = 0, gif.height - 1 do
|
||||
for x = 0, gif.width - 1 do
|
||||
imageData:setPixel(x, y, bg[1], bg[2], bg[3], bg[4])
|
||||
end
|
||||
end
|
||||
elseif #gif.frames > 0 then
|
||||
-- Copy previous frame if needed
|
||||
local prevData = gif.frameData[#gif.frameData]
|
||||
imageData:paste(prevData, 0, 0, 0, 0, gif.width, gif.height)
|
||||
end
|
||||
|
||||
-- Draw current frame
|
||||
if indexStream and #indexStream > 0 then
|
||||
local idx = 1
|
||||
|
||||
-- Use mapPixel for faster pixel operations
|
||||
local function setPixels(x, y, r, g, b, a)
|
||||
if x >= left and x < left + width and y >= top and y < top + height then
|
||||
local pixelIdx = (y - top) * width + (x - left) + 1
|
||||
if pixelIdx <= #indexStream then
|
||||
local colorIndex = string.byte(indexStream, pixelIdx)
|
||||
|
||||
-- Skip transparent pixels
|
||||
if transparentIndex == nil or colorIndex ~= transparentIndex then
|
||||
if colorTable[colorIndex + 1] then
|
||||
local color = colorTable[colorIndex + 1]
|
||||
return color[1], color[2], color[3], color[4]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return r, g, b, a
|
||||
end
|
||||
|
||||
-- Only update the region where the frame is located
|
||||
for y = top, top + height - 1 do
|
||||
for x = left, left + width - 1 do
|
||||
local pixelIdx = (y - top) * width + (x - left) + 1
|
||||
if pixelIdx <= #indexStream then
|
||||
local colorIndex = string.byte(indexStream, pixelIdx)
|
||||
|
||||
-- Skip transparent pixels
|
||||
if transparentIndex == nil or colorIndex ~= transparentIndex then
|
||||
if colorTable[colorIndex + 1] then
|
||||
local color = colorTable[colorIndex + 1]
|
||||
imageData:setPixel(x, y, color[1], color[2], color[3], color[4])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(gif.frameData, imageData)
|
||||
table.insert(gif.frames, love.graphics.newImage(imageData))
|
||||
table.insert(gif.delays, delayForNextFrame)
|
||||
|
||||
-- Reset delay for next frame
|
||||
delayForNextFrame = 0.1
|
||||
|
||||
elseif separator == 0x3B then -- Trailer
|
||||
break
|
||||
else
|
||||
pos = pos + 1
|
||||
end
|
||||
end
|
||||
|
||||
if #gif.frames == 0 then
|
||||
error("No frames found in GIF")
|
||||
end
|
||||
|
||||
-- Ensure all frames have valid delays
|
||||
for i = 1, #gif.delays do
|
||||
if gif.delays[i] <= 0 or gif.delays[i] ~= gif.delays[i] then -- check for 0 or NaN
|
||||
gif.delays[i] = 0.1
|
||||
end
|
||||
end
|
||||
|
||||
return gif
|
||||
end
|
||||
|
||||
function GifLoader.Updater(gif, proc)
|
||||
local wait = function()
|
||||
return gif.playing or gif.kill
|
||||
end
|
||||
proc:newThread("Gif Handler",function()
|
||||
while true do
|
||||
-- Only run if not paused
|
||||
if gif.kill then -- When we want to clean up
|
||||
thread.kill()
|
||||
end
|
||||
thread.hold(wait)
|
||||
thread.sleep(gif.delays[gif.currentFrame] * 4)
|
||||
gif.currentFrame = gif.currentFrame + 1
|
||||
|
||||
if gif.currentFrame > #gif.frames then
|
||||
if gif.loop then
|
||||
gif.currentFrame = 1
|
||||
else
|
||||
gif.currentFrame = #gif.frames
|
||||
gif.playing = false
|
||||
gif.timer = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function GifLoader.update(gif, dt)
|
||||
if not gif.playing or #gif.frames <= 1 then return end
|
||||
|
||||
gif.timer = gif.timer + dt
|
||||
|
||||
-- Simple, accurate frame advancement
|
||||
if gif.timer >= gif.delays[gif.currentFrame] then
|
||||
-- Subtract the current frame's delay
|
||||
gif.timer = gif.timer - gif.delays[gif.currentFrame]
|
||||
|
||||
-- Move to next frame
|
||||
gif.currentFrame = gif.currentFrame + 1
|
||||
|
||||
if gif.currentFrame > #gif.frames then
|
||||
if gif.loop then
|
||||
gif.currentFrame = 1
|
||||
else
|
||||
gif.currentFrame = #gif.frames
|
||||
gif.playing = false
|
||||
gif.timer = 0
|
||||
end
|
||||
end
|
||||
|
||||
-- If we've accumulated too much time (lag spike), cap it
|
||||
if gif.timer > 0.5 then
|
||||
gif.timer = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function GifLoader.draw(gif, x, y, r, sx, sy, ox, oy)
|
||||
if gif.frames[gif.currentFrame] then
|
||||
love.graphics.draw(gif.frames[gif.currentFrame], x, y, r or 0, sx or 1, sy or 1, ox or 0, oy or 0)
|
||||
end
|
||||
end
|
||||
|
||||
function GifLoader.play(gif)
|
||||
gif.playing = true
|
||||
end
|
||||
|
||||
function GifLoader.pause(gif)
|
||||
gif.playing = false
|
||||
end
|
||||
|
||||
function GifLoader.reset(gif)
|
||||
gif.currentFrame = 1
|
||||
gif.timer = 0
|
||||
end
|
||||
|
||||
function GifLoader.setFixedFramerate(gif, fps)
|
||||
local delay = 1 / fps
|
||||
for i = 1, #gif.delays do
|
||||
gif.delays[i] = delay
|
||||
end
|
||||
end
|
||||
|
||||
function GifLoader.getInfo(gif)
|
||||
return {
|
||||
width = gif.width,
|
||||
height = gif.height,
|
||||
frameCount = #gif.frames,
|
||||
delays = gif.delays, -- Show actual delays for debugging
|
||||
totalDuration = (function()
|
||||
local total = 0
|
||||
for i = 1, #gif.delays do
|
||||
total = total + gif.delays[i]
|
||||
end
|
||||
return total
|
||||
end)()
|
||||
}
|
||||
end
|
||||
|
||||
return GifLoader
|
||||
@@ -1,6 +1,6 @@
|
||||
local gui = require("gui")
|
||||
local multi, thread = require("multi"):init()
|
||||
local transition = require("gui.elements.transitions")
|
||||
local transition = require("gui.core.transitions")
|
||||
|
||||
-- Triggers press then release
|
||||
local function getPosition(obj, x, y)
|
||||
|
||||
@@ -1,78 +1,78 @@
|
||||
local gui = require("gui")
|
||||
local multi, thread = require("multi"):init()
|
||||
local processor = gui:newProcessor("Transistion Processor")
|
||||
local transition = {}
|
||||
|
||||
local width, height, flags = love.window.getMode()
|
||||
local fps = 60
|
||||
|
||||
if flags.refreshrate > 0 then
|
||||
fps = flags.refreshrate
|
||||
end
|
||||
|
||||
transition.__index = transition
|
||||
transition.__call = function(t, start, stop, time, ...)
|
||||
local args = {...}
|
||||
return function(st, sp, ti) -- allow these values to be overridden
|
||||
if not (st or start) or not (sp or stop) then return multi.error("start and stop must be supplied") end
|
||||
if start == stop then
|
||||
local temp = {
|
||||
OnStep = function() end,
|
||||
OnStop = multi:newConnection()
|
||||
}
|
||||
proc:newTask(function()
|
||||
temp.OnStop:Fire()
|
||||
end)
|
||||
return temp
|
||||
end
|
||||
local handle = t.func(t, start, stop, (ti or time) or 1, unpack(args))
|
||||
return {
|
||||
OnStep = handle.OnStatus,
|
||||
OnStop = handle.OnReturn + handle.OnError,
|
||||
Kill = t.Kill
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
function transition:newTransition(func)
|
||||
local c = {}
|
||||
setmetatable(c, self)
|
||||
|
||||
c.fps = fps
|
||||
c.func = processor:newFunction(func)
|
||||
c.OnStop = multi:newConnection()
|
||||
c.kill = false
|
||||
|
||||
function c:SetFPS(fps)
|
||||
self.fps = fps
|
||||
end
|
||||
|
||||
function c:GetFPS(fps)
|
||||
return self.fps
|
||||
end
|
||||
|
||||
function c:Kill()
|
||||
if c.running then
|
||||
c.kill = true
|
||||
end
|
||||
end
|
||||
|
||||
return c
|
||||
end
|
||||
|
||||
transition.glide = transition:newTransition(function(t, start, stop, time, ...)
|
||||
local steps = t.fps*time
|
||||
local piece = time/steps
|
||||
local split = stop-start
|
||||
t.running = true
|
||||
for i = 0, steps do
|
||||
if not(t.kill) then
|
||||
thread.sleep(piece)
|
||||
thread.pushStatus(start + i*(split/steps),piece*i)
|
||||
end
|
||||
end
|
||||
t.running = false
|
||||
t.kill = false
|
||||
end)
|
||||
|
||||
local gui = require("gui")
|
||||
local multi, thread = require("multi"):init()
|
||||
local processor = gui:newProcessor("Transistion Processor")
|
||||
local transition = {}
|
||||
|
||||
local width, height, flags = love.window.getMode()
|
||||
local fps = 60
|
||||
|
||||
if flags.refreshrate > 0 then
|
||||
fps = flags.refreshrate
|
||||
end
|
||||
|
||||
transition.__index = transition
|
||||
transition.__call = function(t, start, stop, time, ...)
|
||||
local args = {...}
|
||||
return function(st, sp, ti) -- allow these values to be overridden
|
||||
if not (st or start) or not (sp or stop) then return multi.error("start and stop must be supplied") end
|
||||
if start == stop then
|
||||
local temp = {
|
||||
OnStep = function() end,
|
||||
OnStop = multi:newConnection()
|
||||
}
|
||||
proc:newTask(function()
|
||||
temp.OnStop:Fire()
|
||||
end)
|
||||
return temp
|
||||
end
|
||||
local handle = t.func(t, start, stop, (ti or time) or 1, unpack(args))
|
||||
return {
|
||||
OnStep = handle.OnStatus,
|
||||
OnStop = handle.OnReturn + handle.OnError,
|
||||
Kill = t.Kill
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
function transition:newTransition(func)
|
||||
local c = {}
|
||||
setmetatable(c, self)
|
||||
|
||||
c.fps = fps
|
||||
c.func = processor:newFunction(func)
|
||||
c.OnStop = multi:newConnection()
|
||||
c.kill = false
|
||||
|
||||
function c:SetFPS(fps)
|
||||
self.fps = fps
|
||||
end
|
||||
|
||||
function c:GetFPS(fps)
|
||||
return self.fps
|
||||
end
|
||||
|
||||
function c:Kill()
|
||||
if c.running then
|
||||
c.kill = true
|
||||
end
|
||||
end
|
||||
|
||||
return c
|
||||
end
|
||||
|
||||
transition.glide = transition:newTransition(function(t, start, stop, time, ...)
|
||||
local steps = t.fps*time
|
||||
local piece = time/steps
|
||||
local split = stop-start
|
||||
t.running = true
|
||||
for i = 0, steps do
|
||||
if not(t.kill) then
|
||||
thread.sleep(piece)
|
||||
thread.pushStatus(start + i*(split/steps),piece*i)
|
||||
end
|
||||
end
|
||||
t.running = false
|
||||
t.kill = false
|
||||
end)
|
||||
|
||||
return transition
|
||||
+904
-648
File diff suppressed because it is too large
Load Diff
@@ -1,69 +0,0 @@
|
||||
local gui = require("gui")
|
||||
local color = require("gui.core.color")
|
||||
local theme = require("gui.core.theme")
|
||||
local transition = require("gui.elements.transitions")
|
||||
|
||||
function gui:newMenu(title, sx, position, trans)
|
||||
if not title then multi.error("Argument 1 string('title') is required") end
|
||||
if not sx then multi.error("Argument 2 number('sx') is required") end
|
||||
|
||||
local position = position or gui.ALIGN_LEFT
|
||||
local trans = trans or transition.glide
|
||||
|
||||
local menu, to, tc, open
|
||||
if position == gui.ALIGN_LEFT then
|
||||
menu = self:newFrame(0, 0, 0, 0, -sx, 0, sx, 1)
|
||||
to = trans(-sx, 0, .25)
|
||||
tc = trans(0, -sx, .25)
|
||||
elseif position == gui.ALIGN_CENTER then
|
||||
menu = self:newFrame(0, 0, 0, 0, .5 -sx/2, 1.1, sx, 1)
|
||||
to = trans(1.1, 0, .35)
|
||||
tc = trans(0, 1.1, .35)
|
||||
elseif position == gui.ALIGN_RIGHT then
|
||||
menu = self:newFrame(0, 0, 0, 0, 1, 0, sx, 1)
|
||||
to = trans(1, 1 - sx, .25)
|
||||
tc = trans(1 - sx, 1, .25)
|
||||
end
|
||||
|
||||
function menu:isOpen()
|
||||
return open
|
||||
end
|
||||
|
||||
function menu:Open(show)
|
||||
if show then
|
||||
if not menu.lock then
|
||||
menu.lock = true
|
||||
local t = to()
|
||||
t.OnStop(function()
|
||||
open = true
|
||||
menu.lock = false
|
||||
end)
|
||||
t.OnStep(function(p)
|
||||
if position == gui.ALIGN_CENTER then
|
||||
menu:setDualDim(nil, nil, nil, nil, nil, p)
|
||||
else
|
||||
menu:setDualDim(nil, nil, nil, nil, p)
|
||||
end
|
||||
end)
|
||||
end
|
||||
else
|
||||
if not menu.lock then
|
||||
menu.lock = true
|
||||
local t = tc()
|
||||
t.OnStop(function()
|
||||
open = false
|
||||
menu.lock = false
|
||||
end)
|
||||
t.OnStep(function(p)
|
||||
if position == gui.ALIGN_CENTER then
|
||||
menu:setDualDim(nil, nil, nil, nil, nil, p)
|
||||
else
|
||||
menu:setDualDim(nil, nil, nil, nil, p)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return menu
|
||||
end
|
||||
+302
-4
@@ -2,10 +2,9 @@ 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 gif = require("gui.addons.gifloader")
|
||||
local gif = require("gui.core.gifloader")
|
||||
local gui = {}
|
||||
local updater = multi:newProcessor("UpdateManager", true)
|
||||
|
||||
local drawer = multi:newProcessor("DrawManager", true)
|
||||
|
||||
local bit = require("bit")
|
||||
@@ -139,6 +138,10 @@ end)
|
||||
|
||||
-- Hotkeys
|
||||
|
||||
local function noOf(sx,sy,sw,sh)
|
||||
return nil,nil,nil,nil,sx,sy,sw,sh
|
||||
end
|
||||
|
||||
local has_hotkey = false
|
||||
local hot_keys = {}
|
||||
|
||||
@@ -575,8 +578,7 @@ function gui:isActive()
|
||||
end
|
||||
|
||||
function gui:isOnScreen()
|
||||
|
||||
return
|
||||
return not self:isOffScreen()
|
||||
end
|
||||
|
||||
-- Base get uniques
|
||||
@@ -1123,6 +1125,302 @@ function gui:newTextBase(typ, txt, x, y, w, h, sx, sy, sw, sh)
|
||||
return c
|
||||
end
|
||||
|
||||
function gui:newTextArea(initialText, x, y, w, h, sx, sy, sw, sh)
|
||||
-- Outer viewport (clips content)
|
||||
local viewport = self:newFrame(x, y, w or 0, h or 0, sx, sy, sw, sh)
|
||||
viewport.clipDescendants = true
|
||||
viewport.color = color.new("#f9f9f9")
|
||||
viewport:setRoundness(3, 3)
|
||||
|
||||
-- Inner content frame (scrolled by offsetting its y)
|
||||
local content = viewport:newFrame(2, 2, -4, -4, 0, 0, 1, 0)
|
||||
content.drawBorder = false
|
||||
content.color = {0, 0, 0, 0}
|
||||
content.visibility = 0
|
||||
|
||||
-- Cursor line rendering happens via a separate frame
|
||||
local cursorBar = viewport:newFrame(0, 0, 1, 0)
|
||||
cursorBar.color = color.new("#222222")
|
||||
cursorBar.drawBorder = false
|
||||
cursorBar.ignore = true
|
||||
cursorBar.visibility = 0
|
||||
|
||||
local lines = {}
|
||||
local lineObjs = {} -- TextLabel per line
|
||||
local LINE_H = 18
|
||||
local scrollY = 0
|
||||
local cursorLine = 1
|
||||
local cursorCol = 0
|
||||
local blinkOn = true
|
||||
local blinkTimer = 0
|
||||
local BLINK_RATE = 0.5
|
||||
local focused = false
|
||||
|
||||
viewport.OnChanged = multi:newConnection()
|
||||
viewport.readOnly = false
|
||||
|
||||
-- Split a string into lines
|
||||
local function splitLines(s)
|
||||
local result = {}
|
||||
local pos = 1
|
||||
while true do
|
||||
local nl = s:find("\n", pos, true)
|
||||
if nl then
|
||||
result[#result + 1] = s:sub(pos, nl - 1)
|
||||
pos = nl + 1
|
||||
else
|
||||
result[#result + 1] = s:sub(pos)
|
||||
break
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Join lines back to a single string
|
||||
local function joinLines()
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
-- Rebuild all line label objects
|
||||
local function rebuildLabels()
|
||||
for _, obj in ipairs(lineObjs) do
|
||||
obj:destroy()
|
||||
end
|
||||
lineObjs = {}
|
||||
|
||||
for i, lineText in ipairs(lines) do
|
||||
local lbl = content:newTextLabel(lineText, 0, (i-1)*LINE_H, 0, LINE_H, 0, 0, 1)
|
||||
lbl.drawBorder = false
|
||||
lbl.color = {0, 0, 0, 0}
|
||||
lbl.visibility = 0
|
||||
lbl.textColor = color.new("#222222")
|
||||
lbl.align = gui.ALIGN_LEFT
|
||||
lbl.ignore = true
|
||||
lbl:setFont(13)
|
||||
lineObjs[i] = lbl
|
||||
end
|
||||
|
||||
-- Resize content frame to fit all lines
|
||||
local totalH = #lines * LINE_H + 4
|
||||
content:setDualDim(nil, nil, nil, totalH)
|
||||
end
|
||||
|
||||
-- Apply vertical scroll so the cursor stays visible
|
||||
local function applyScroll()
|
||||
local _, _, _, vh = viewport:getAbsolutes()
|
||||
local contentH = #lines * LINE_H + 4
|
||||
local maxScroll = math.max(0, contentH - vh)
|
||||
scrollY = math.max(0, math.min(scrollY, maxScroll))
|
||||
content:rawSetDualDim(2, 2 - scrollY)
|
||||
end
|
||||
|
||||
local function ensureCursorVisible()
|
||||
local _, _, _, vh = viewport:getAbsolutes()
|
||||
local cursorY = (cursorLine - 1) * LINE_H
|
||||
if cursorY < scrollY then
|
||||
scrollY = cursorY
|
||||
elseif cursorY + LINE_H > scrollY + vh then
|
||||
scrollY = cursorY + LINE_H - vh
|
||||
end
|
||||
applyScroll()
|
||||
end
|
||||
|
||||
-- Update the cursor bar position
|
||||
local function updateCursor()
|
||||
if not focused then
|
||||
cursorBar.visibility = 0
|
||||
return
|
||||
end
|
||||
local ax, ay = viewport:getAbsolutes()
|
||||
local lineText = lines[cursorLine] or ""
|
||||
local font = love.graphics.newFont(13)
|
||||
local cx = 2 + font:getWidth(lineText:sub(1, cursorCol))
|
||||
local cy = 2 + (cursorLine - 1) * LINE_H - scrollY
|
||||
cursorBar:rawSetDualDim(cx, cy, 1, LINE_H)
|
||||
cursorBar.visibility = blinkOn and 1 or 0
|
||||
end
|
||||
|
||||
local function setText(s)
|
||||
lines = splitLines(s or "")
|
||||
if #lines == 0 then lines = {""} end
|
||||
rebuildLabels()
|
||||
cursorLine = math.min(cursorLine, #lines)
|
||||
cursorCol = math.min(cursorCol, #lines[cursorLine])
|
||||
applyScroll()
|
||||
updateCursor()
|
||||
end
|
||||
|
||||
function viewport:getText()
|
||||
return joinLines()
|
||||
end
|
||||
|
||||
function viewport:setText(s)
|
||||
setText(s)
|
||||
self.OnChanged:Fire(self, joinLines())
|
||||
end
|
||||
|
||||
function viewport:appendLine(s)
|
||||
lines[#lines + 1] = s
|
||||
rebuildLabels()
|
||||
applyScroll()
|
||||
end
|
||||
|
||||
function viewport:scrollToBottom()
|
||||
scrollY = math.huge
|
||||
applyScroll()
|
||||
end
|
||||
|
||||
-- Insert text at cursor
|
||||
local function insertText(s)
|
||||
if viewport.readOnly then return end
|
||||
local line = lines[cursorLine] or ""
|
||||
-- Handle newlines in inserted text
|
||||
if s == "\n" then
|
||||
local before = line:sub(1, cursorCol)
|
||||
local after = line:sub(cursorCol + 1)
|
||||
lines[cursorLine] = before
|
||||
table.insert(lines, cursorLine + 1, after)
|
||||
cursorLine = cursorLine + 1
|
||||
cursorCol = 0
|
||||
else
|
||||
lines[cursorLine] = line:sub(1, cursorCol) .. s .. line:sub(cursorCol + 1)
|
||||
cursorCol = cursorCol + #s
|
||||
end
|
||||
rebuildLabels()
|
||||
ensureCursorVisible()
|
||||
updateCursor()
|
||||
viewport.OnChanged:Fire(viewport, joinLines())
|
||||
end
|
||||
|
||||
local function deleteBack()
|
||||
if viewport.readOnly then return end
|
||||
if cursorCol > 0 then
|
||||
local line = lines[cursorLine]
|
||||
lines[cursorLine] = line:sub(1, cursorCol - 1) .. line:sub(cursorCol + 1)
|
||||
cursorCol = cursorCol - 1
|
||||
elseif cursorLine > 1 then
|
||||
-- merge with previous line
|
||||
local prevLine = lines[cursorLine - 1]
|
||||
cursorCol = #prevLine
|
||||
lines[cursorLine - 1] = prevLine .. lines[cursorLine]
|
||||
table.remove(lines, cursorLine)
|
||||
cursorLine = cursorLine - 1
|
||||
end
|
||||
rebuildLabels()
|
||||
ensureCursorVisible()
|
||||
updateCursor()
|
||||
viewport.OnChanged:Fire(viewport, joinLines())
|
||||
end
|
||||
|
||||
local function deleteForward()
|
||||
if viewport.readOnly then return end
|
||||
local line = lines[cursorLine]
|
||||
if cursorCol < #line then
|
||||
lines[cursorLine] = line:sub(1, cursorCol) .. line:sub(cursorCol + 2)
|
||||
elseif cursorLine < #lines then
|
||||
lines[cursorLine] = line .. lines[cursorLine + 1]
|
||||
table.remove(lines, cursorLine + 1)
|
||||
end
|
||||
rebuildLabels()
|
||||
updateCursor()
|
||||
viewport.OnChanged:Fire(viewport, joinLines())
|
||||
end
|
||||
|
||||
-- Mouse click to position cursor
|
||||
viewport.OnPressed(function(self, mx, my)
|
||||
focused = true
|
||||
local _, vy = viewport:getAbsolutes()
|
||||
local relY = my - vy + scrollY - 2
|
||||
cursorLine = math.max(1, math.min(#lines, math.floor(relY / LINE_H) + 1))
|
||||
local lineText = lines[cursorLine] or ""
|
||||
local font = love.graphics.newFont(13)
|
||||
local _, vx = viewport:getAbsolutes()
|
||||
local relX = mx - vx - 2
|
||||
-- binary-search for cursor column
|
||||
local col = 0
|
||||
for i = 1, #lineText do
|
||||
local w = font:getWidth(lineText:sub(1, i))
|
||||
if w > relX then break end
|
||||
col = i
|
||||
end
|
||||
cursorCol = col
|
||||
updateCursor()
|
||||
end)
|
||||
|
||||
viewport.OnPressedOuter(function()
|
||||
focused = false
|
||||
updateCursor()
|
||||
end)
|
||||
|
||||
-- Keyboard input (only when focused)
|
||||
gui.Events.OnTextInputed(function(t)
|
||||
if not focused then return end
|
||||
insertText(t)
|
||||
end)
|
||||
|
||||
gui.Events.OnKeyPressed(function(key)
|
||||
if not focused then return end
|
||||
if key == "return" or key == "kpenter" then
|
||||
insertText("\n")
|
||||
elseif key == "backspace" then
|
||||
deleteBack()
|
||||
elseif key == "delete" then
|
||||
deleteForward()
|
||||
elseif key == "up" then
|
||||
cursorLine = math.max(1, cursorLine - 1)
|
||||
cursorCol = math.min(cursorCol, #(lines[cursorLine] or ""))
|
||||
ensureCursorVisible(); updateCursor()
|
||||
elseif key == "down" then
|
||||
cursorLine = math.min(#lines, cursorLine + 1)
|
||||
cursorCol = math.min(cursorCol, #(lines[cursorLine] or ""))
|
||||
ensureCursorVisible(); updateCursor()
|
||||
elseif key == "left" then
|
||||
if cursorCol > 0 then
|
||||
cursorCol = cursorCol - 1
|
||||
elseif cursorLine > 1 then
|
||||
cursorLine = cursorLine - 1
|
||||
cursorCol = #lines[cursorLine]
|
||||
end
|
||||
ensureCursorVisible(); updateCursor()
|
||||
elseif key == "right" then
|
||||
local lineLen = #(lines[cursorLine] or "")
|
||||
if cursorCol < lineLen then
|
||||
cursorCol = cursorCol + 1
|
||||
elseif cursorLine < #lines then
|
||||
cursorLine = cursorLine + 1
|
||||
cursorCol = 0
|
||||
end
|
||||
ensureCursorVisible(); updateCursor()
|
||||
elseif key == "home" then
|
||||
cursorCol = 0; updateCursor()
|
||||
elseif key == "end" then
|
||||
cursorCol = #(lines[cursorLine] or ""); updateCursor()
|
||||
end
|
||||
end)
|
||||
|
||||
-- Scroll wheel
|
||||
viewport.OnWheelMoved(function(_, dy)
|
||||
scrollY = scrollY - dy * 30
|
||||
applyScroll()
|
||||
updateCursor()
|
||||
end)
|
||||
|
||||
-- Cursor blink
|
||||
viewport:OnUpdate(function(self, dt)
|
||||
blinkTimer = blinkTimer + dt
|
||||
if blinkTimer >= BLINK_RATE then
|
||||
blinkTimer = 0
|
||||
blinkOn = not blinkOn
|
||||
if focused then
|
||||
cursorBar.visibility = blinkOn and 1 or 0
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
setText(initialText or "")
|
||||
return viewport
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user