diff --git a/.gitmodules b/.gitmodules index 2034a46..50eba49 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "multi"] path = multi - url = https://github.com/rayaman/multi.git + url = https://github.com/rayaman/multi.git \ No newline at end of file diff --git a/gui/README.md b/gui/README.md deleted file mode 100644 index 761438c..0000000 --- a/gui/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# GuiManager - -This library due to the changes in love2d. Too many things are broken and instead of doing patch work, I've decided to do a total rewrite. Also I'll be able to make use of the new multi manager features and build a better library from the ground up. - -Core Objects: -- ~~Frame~~ ✔️ -- Text: - - ~~Label~~ ✔️ - - ~~Box~~ ✔️ - - ~~Button~~ ✔️ - - utf8 support with textbox (Forgot about this, will have to rework some things) -- Image: - - ~~Label~~ ✔️ - - ~~Button~~ ✔️ - - Animation -- ~~Video~~ ✔️ - -Events: -- Mouse Events - - ~~Enter~~ ✔️ - - ~~Exit~~ ✔️ - - ~~Pressed~~ ✔️ - - ~~Released~~ ✔️ - - ~~Moved~~ ✔️ - - ~~WheelMoved~~ ✔️ - - ~~DragStart~~ ✔️ - - ~~Dragging~~ ✔️ - - ~~DragEnd~~ ✔️ -- Keyboard Events - - ~~Hotkey~~ ✔️ Refer to [KeyConstants](https://love2d.org/wiki/KeyConstant) wiki page - - Some default hotkeys have been added: - - ~~(conn)gui.HotKeys.OnSelectAll~~ ✔️ `Ctrl + A` - - ~~(conn)gui.HotKeys.OnCopy~~ ✔️ `Ctrl + C` - - ~~(conn)gui.HotKeys.OnPaste~~ ✔️ `Ctrl + V` - - ~~(conn)gui.HotKeys.OnUndo~~ ✔️ `Ctrl + Z` - - ~~(conn)gui.HotKeys.OnRedo~~ ✔️ `Ctrl + Y, Ctrl + Shift + Z` -- 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() \ No newline at end of file diff --git a/gui/addons/extensions.lua b/gui/addons/extensions.lua deleted file mode 100644 index 6540a9b..0000000 --- a/gui/addons/extensions.lua +++ /dev/null @@ -1,249 +0,0 @@ -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 miscProc = 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, draggable, th) - local window = self:newWindow(x, y, w, h, sx, sy, sw, sh, "", draggable, th or theme:new({ - primary = "#000000", - primaryDark = "#10465c", - primaryText = "#ffffff" - })) - local video = window:newVideo(source, 0, 0, 0, 0, 0, .05, 1, .75) - local length = video:getDuration() - local play_pause = window:newImageButton("gui/assets/play.png",0,0,0,0,.45,.86,0,.14) - local seek = window:newProgressBar(0,0,0,0,0,.8,1,.05,length*100,0) - seek.OnProgressUpdated(function(self,_,value, drag) - if drag then - local status = video:isPlaying() - video:seek(value/100) - if status then - video:play() - else - video:stop() - end - end - end) - seek:isClickable(true) - 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) - - mediaProc:newThread(function() - while true do - thread.hold(function() return video:isPlaying() end) - local p = video:tell() - seek:update(p*100) - end - end) - - function window:seek(...) - video.video:seek(...) - end - - function window:play() - video:play() - end - - function window:stop() - video:stop() - end - - window.OnClose(function() - video:pause() - end) - - return window -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 count = count or 100 - local progressbar = self:newFrame(x,y,w,h,sx,sy,sw,sh) - local fillframe = progressbar:newFrame(2,2,-4,-4,0,0,1,1) - local fill = fillframe:newFrame(noOf(0, 0, 1, 1)) - local percentDisplay = fillframe:newTextLabel("",noOf(0,0,1,1)) - percentDisplay.align = gui.ALIGN_CENTER - percentDisplay.textColor = color.new("#CC5500") - fillframe.visibility = 0 - progressbar.color = color.new("#000000") - fill.color = color.new("#ffffff") - progressbar.fillframe = fillframe - progressbar.fill = fill - progressbar.display = percentDisplay - progressbar.OnProgressUpdated = multi:newConnection() - percentDisplay.visibility = 0 - local displayPercent = false - - function progressbar:showPercent(bool) - displayPercent = bool - if bool then - miscProc:newThread(function() - thread.skip(2) - _,_,_h = percentDisplay:getAbsolutes() - percentDisplay:setFont(math.floor(h/1.3)) - self:update() - end) - end - end - - function progressbar:isClickable(bool) - fillframe:respectHierarchy(not bool) - -- Makes the bar updatable by clicking on it - fillframe:enableDragging(bool and gui.MOUSE_PRIMARY) - end - - local calcFunc = function(self, dx, dy, x, y, istouch) - local sx, sy, sw, sh = self:getAbsolutes() - if x >= sx and x <= sx + sw then - progressbar:update((((x - sx)/sw) * count), true) - end - end - - fillframe:OnDragStart(calcFunc) - - fillframe.OnDragging(calcFunc) - - fillframe.OnPressed(function(self, x, y, dx, dy, istouch) - calcFunc(self, dx, dy, x, y, istouch) - end) - - function progressbar:update(v, drag) - v = v or value - if v > count then v = count end - if v < 0 then v = 0 end - local percent = value/count - fill:setDualDim(noOf(nil,nil,percent)) - if displayPercent then - percentDisplay.text = math.floor((percent*100)+.5).. "%" - percentDisplay:centerFont() - end - value = v - self.OnProgressUpdated:Fire(self, percent, value, drag) - 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() - self:update(count) - end - - function progressbar:min() - self:update(0) - end - - function progressbar:half() - self:update(math.floor(count/2)) - end - - function progressbar:getPercent() - return math.floor(((value/count)*100)+.5) - end - - function progressbar:getValue() - return value - end - - progressbar:update(value) - - -- to change colors and modify main components - return progressbar, fill, percentDisplay, fillframe -end \ No newline at end of file diff --git a/gui/addons/extensions_old.lua b/gui/addons/extensions_old.lua deleted file mode 100644 index 3f1b34e..0000000 --- a/gui/addons/extensions_old.lua +++ /dev/null @@ -1,1376 +0,0 @@ ---[[ - gui_extensions.lua - ------------------ - Drop-in enhancements for the gui library. Patches bugs, fills gaps, - and adds new widgets without modifying any original source files. - - USAGE - require("gui_extensions") -- after requiring "gui" - - CONTENTS - BUG FIXES - 1. color arithmetic blue-channel bug - 2. gui:isOnScreen() stub - - NEW WIDGETS - 3. Checkbox - 4. RadioGroup - 5. Slider (horizontal or vertical) - 6. ProgressBar - 7. Tooltip - 8. TextArea (multiline input) - - LAYOUT CONTAINERS - 9. ListFrame (auto-stacking vertical/horizontal list) - 10. GridFrame (fixed-column grid) - - SCROLL FRAME AUTO-SIZING - 11. Auto-measure patch for newScrollFrame content - - EASING LIBRARY - 12. transition.easings (ease-in/out, cubic, bounce, elastic, back) - - FOCUS MANAGEMENT - 13. gui.focus.* (set/get focus, OnFocusGained, OnFocusLost per element) - - Z-INDEX / LAYER SYSTEM - 14. gui:setLayer(n) / gui:getLayer() -]] - -local gui = require("gui") -local color = require("gui.core.color") -local transition = require("gui.elements.transitions") -local multi, thread = require("multi"):init() - -local ext = {} - --- ───────────────────────────────────────────────────────────────────────────── --- 1. BUG FIX: color arithmetic metamethods (blue channel was always c[2]) --- ───────────────────────────────────────────────────────────────────────────── - --- We reach into the metatable that color.new sets on every color table and --- replace the broken metamethods with correct ones. All existing color objects --- share this metatable so the fix is retroactive. -do - local probe = color.new(1, 0, 0) -- make a throwaway color to get the MT - local mt = getmetatable(probe) - if mt then - mt.__add = function(a, b) return color.new(a[1]+b[1], a[2]+b[2], a[3]+b[3]) end - mt.__sub = function(a, b) return color.new(a[1]-b[1], a[2]-b[2], a[3]-b[3]) end - mt.__mul = function(a, b) return color.new(a[1]*b[1], a[2]*b[2], a[3]*b[3]) end - mt.__div = function(a, b) return color.new(a[1]/b[1], a[2]/b[2], a[3]/b[3]) end - mt.__mod = function(a, b) return color.new(a[1]%b[1], a[2]%b[2], a[3]%b[3]) end - mt.__pow = function(a, b) return color.new(a[1]^b[1], a[2]^b[2], a[3]^b[3]) end - mt.__unm = function(a) return color.new(-a[1], -a[2], -a[3]) end - mt.__eq = function(a, b) return a[1]==b[1] and a[2]==b[2] and a[3]==b[3] end - mt.__lt = function(a, b) return a[1]< b[1] and a[2]< b[2] and a[3]< b[3] end - mt.__le = function(a, b) return a[1]<=b[1] and a[2]<=b[2] and a[3]<=b[3] end - end -end - --- ───────────────────────────────────────────────────────────────────────────── --- 2. BUG FIX: gui:isOnScreen() was an empty stub --- ───────────────────────────────────────────────────────────────────────────── - -function gui:isOnScreen() - return not self:isOffScreen() -end - --- ───────────────────────────────────────────────────────────────────────────── --- 3. WIDGET: Checkbox --- --- local cb = parent:newCheckbox(label, x, y, size, sx, sy, checked) --- --- Properties cb.checked (boolean) --- Events cb.OnChanged(function(self, checked) end) --- Methods cb:setValue(bool) cb:toggle() --- ───────────────────────────────────────────────────────────────────────────── - -function gui:newCheckbox(label, x, y, size, sx, sy, checked) - size = size or 20 - - local container = self:newFrame(x, y, 0, size, sx, sy, 0, 0) - container.drawBorder = false - container.color = {0, 0, 0, 0} - container.visibility = 0 - - -- The clickable box - local box = container:newTextButton("", 0, 0, size, size) - box:setRoundness(3, 3) - box.color = color.new("#dddddd") - - -- The checkmark label (drawn inside the box) - local check = box:newTextLabel("✓", 0, 0, size, size) - check.align = gui.ALIGN_CENTER - check.textColor = color.new("#ffffff") - check.visibility = 0 - check.ignore = true - check:setFont(math.floor(size * 0.7)) - - -- The text label to the right of the box - local lbl = container:newTextLabel(label or "", size + 6, 0, 0, size, 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 - - container.OnChanged = multi:newConnection() - container.checked = checked or false - - local normalColor = color.new("#dddddd") - local checkedColor = color.new("#4a90d9") - - local function refresh() - if container.checked then - box.color = checkedColor - check.visibility = 1 - else - box.color = normalColor - check.visibility = 0 - end - end - - function container:setValue(v) - self.checked = v - refresh() - self.OnChanged:Fire(self, self.checked) - end - - function container:toggle() - self:setValue(not self.checked) - end - - box.OnReleased(function() - container:toggle() - end) - - refresh() - return container -end - --- ───────────────────────────────────────────────────────────────────────────── --- 4. WIDGET: RadioGroup --- --- local rg = parent:newRadioGroup(options, x, y, w, itemHeight, sx, sy) --- -- options: {"Option A", "Option B", ...} --- --- Properties rg.selected (1-based index of the selected option, or nil) --- Events rg.OnChanged(function(self, index, label) end) --- Methods rg:select(index) --- ───────────────────────────────────────────────────────────────────────────── - -function gui:newRadioGroup(options, x, y, w, itemHeight, sx, sy) - itemHeight = itemHeight or 28 - local totalH = #options * itemHeight - - local container = self:newFrame(x, y, w or 0, totalH, sx, sy, w and 0 or 1) - container.drawBorder = false - container.color = {0, 0, 0, 0} - container.visibility = 0 - - container.OnChanged = multi:newConnection() - container.selected = nil - - local dotSize = math.floor(itemHeight * 0.55) - local dotRadius = math.floor(dotSize / 2) - local normalBorder = color.new("#aaaaaa") - local selectedBorder = color.new("#4a90d9") - local dotColor = color.new("#4a90d9") - - local buttons = {} - - for i, optLabel in ipairs(options) do - local row = container:newFrame(0, (i-1)*itemHeight, 0, itemHeight, 0, 0, 1) - row.drawBorder = false - row.color = {0, 0, 0, 0} - row.visibility = 0 - - -- Outer ring - local ring = row:newTextButton("", 0, math.floor((itemHeight - dotSize)/2), dotSize, dotSize) - ring:makeCircle(0, math.floor((itemHeight - dotSize)/2), dotRadius) - ring.color = {1, 1, 1} - ring.borderColor = normalBorder - - -- Inner filled dot (hidden when unselected) - local innerR = math.floor(dotRadius * 0.5) - local innerOff = dotRadius - innerR - local dot = ring:newFrame(innerOff, innerOff, innerR*2, innerR*2) - dot:makeCircle(innerOff, innerOff, innerR) - dot.color = dotColor - dot.drawBorder = false - dot.visibility = 0 - dot.ignore = true - - local lbl = row:newTextLabel(optLabel, dotSize + 8, 0, 0, itemHeight, 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 - - buttons[i] = {ring = ring, dot = dot, label = optLabel} - - local idx = i - ring.OnReleased(function() - container:select(idx) - end) - end - - function container:select(index) - -- deselect previous - if self.selected and buttons[self.selected] then - local prev = buttons[self.selected] - prev.ring.borderColor = normalBorder - prev.dot.visibility = 0 - end - self.selected = index - if buttons[index] then - local cur = buttons[index] - cur.ring.borderColor = selectedBorder - cur.dot.visibility = 1 - end - self.OnChanged:Fire(self, index, options[index]) - end - - return container -end - --- ───────────────────────────────────────────────────────────────────────────── --- 5. WIDGET: Slider --- --- local sl = parent:newSlider(min, max, value, x, y, w, h, sx, sy, vertical) --- --- Properties sl.value (current value, clamped to [min,max]) --- Events sl.OnChanged(function(self, value) end) --- Methods sl:setValue(n) sl:setRange(min, max) --- ───────────────────────────────────────────────────────────────────────────── - -function gui:newSlider(min, max, value, x, y, w, h, sx, sy, vertical) - min = min or 0 - max = max or 100 - value = value or min - - local container = self:newFrame(x, y, w or 0, h or (vertical and 0 or 20), sx, sy, w and 0 or 1, h and 0 or 0) - container.drawBorder = false - container.color = {0, 0, 0, 0} - container.visibility = 0 - - -- Track - local track = container:newFrame(0, 0, 0, 0, 0, 0, 1, 1) - track.drawBorder = false - track.color = color.new("#cccccc") - if vertical then - track:setDualDim(math.floor((w or 20)/2)-3, 0, 6, 0, 0, 0, 0, 1) - else - track:setDualDim(0, math.floor((h or 20)/2)-3, 0, 6, 0, 0, 1, 0) - end - track:setRoundness(3, 3) - - -- Fill (colored part up to the thumb) - local fill = track:newFrame(0, 0, 0, 0, 0, 0, 0, 1) - fill.drawBorder = false - fill.color = color.new("#4a90d9") - fill:setRoundness(3, 3) - if vertical then - fill:setDualDim(0, 0, 0, 0, 0, 0, 1, 0) -- will be sized by setValue - end - - -- Thumb - local thumbSize = vertical and (w or 20) or (h or 20) - local thumb = container:newFrame(0, 0, thumbSize, thumbSize) - thumb:makeCircle(0, 0, math.floor(thumbSize/2)) - thumb.color = color.new("#ffffff") - thumb.borderColor = color.new("#4a90d9") - thumb:enableDragging(gui.MOUSE_PRIMARY) - thumb:respectHierarchy(true) - - container.OnChanged = multi:newConnection() - container.value = value - container._min = min - container._max = max - - local function normalize(v) - return (v - container._min) / (container._max - container._min) - end - - local function positionThumb(v) - local t = normalize(v) - local ax, ay, aw, ah = container:getAbsolutes() - if vertical then - local travel = ah - thumbSize - local py = travel * (1 - t) -- 0 = top = max - thumb:rawSetDualDim(math.floor((aw - thumbSize)/2), math.floor(py)) - fill:setDualDim(0, math.floor(py + thumbSize/2), 0, 0, 0, 0, 1, 1 - t) - else - local travel = aw - thumbSize - local px = travel * t - thumb:rawSetDualDim(math.floor(px), math.floor((ah - thumbSize)/2)) - fill:setDualDim(0, 0, 0, 0, 0, 0, t, 1) - end - end - - function container:setValue(v) - v = math.max(self._min, math.min(self._max, v)) - self.value = v - positionThumb(v) - self.OnChanged:Fire(self, v) - end - - function container:setRange(newMin, newMax) - self._min = newMin - self._max = newMax - self:setValue(math.max(newMin, math.min(newMax, self.value))) - end - - -- Drag handling - thumb.OnDragging(function(self, dx, dy) - local ax, ay, aw, ah = container:getAbsolutes() - local t - if vertical then - local travel = ah - thumbSize - if travel <= 0 then return end - local _, ty = thumb:getAbsolutes() - local newY = ty - ay + dy - t = 1 - math.max(0, math.min(1, newY / travel)) - else - local travel = aw - thumbSize - if travel <= 0 then return end - local tx, _ = thumb:getAbsolutes() - local newX = tx - ax + dx - t = math.max(0, math.min(1, newX / travel)) - end - local newVal = container._min + t * (container._max - container._min) - container:setValue(newVal) - end) - - -- Click on track to jump - track.OnReleased(function(self, mx, my) - local ax, ay, aw, ah = container:getAbsolutes() - local t - if vertical then - t = 1 - math.max(0, math.min(1, (my - ay) / ah)) - else - t = math.max(0, math.min(1, (mx - ax) / aw)) - end - local newVal = container._min + t * (container._max - container._min) - container:setValue(newVal) - end) - - -- Initial position (after first layout pass) - container.OnSizeChanged(function() positionThumb(container.value) end) - container:OnUpdate(function() - -- only run once to set initial position - positionThumb(container.value) - -- replace with no-op after first run - container.OnUpdate = function() end - end) - - return container -end - --- ───────────────────────────────────────────────────────────────────────────── --- 6. WIDGET: ProgressBar --- --- local pb = parent:newProgressBar(min, max, value, x, y, w, h, sx, sy) --- --- Properties pb.value --- Methods pb:setValue(n) pb:setRange(min, max) --- pb:setColors(bg, fill) --- ───────────────────────────────────────────────────────────────────────────── - -function gui:newProgressBar(min, max, value, x, y, w, h, sx, sy) - min = min or 0 - max = max or 100 - value = value or min - - local bar = self:newFrame(x, y, w or 0, h or 20, sx, sy, w and 0 or 1) - bar.color = color.new("#cccccc") - bar:setRoundness(4, 4) - - local fill = bar:newFrame(0, 0, 0, 0, 0, 0, 0, 1) - fill.drawBorder = false - fill.color = color.new("#4a90d9") - fill:setRoundness(4, 4) - - bar._min = min - bar._max = max - bar.value = value - - local function refresh() - local t = (bar.value - bar._min) / (bar._max - bar._min) - t = math.max(0, math.min(1, t)) - fill:setDualDim(0, 0, 0, 0, 0, 0, t, 1) - end - - function bar:setValue(v) - self.value = math.max(self._min, math.min(self._max, v)) - refresh() - end - - function bar:setRange(newMin, newMax) - self._min = newMin - self._max = newMax - self:setValue(self.value) - end - - function bar:setColors(bg, fg) - if bg then self.color = color.new(bg) end - if fg then fill.color = color.new(fg) end - end - - refresh() - return bar -end - --- ───────────────────────────────────────────────────────────────────────────── --- 7. WIDGET: Tooltip --- --- local tt = gui:newTooltip(text, delay) --- tt:attach(someElement) -- show tooltip when hovering someElement --- --- Or attach inline: --- gui:attachTooltip(element, text, delay) --- --- The tooltip lives at the root and always renders on top. --- ───────────────────────────────────────────────────────────────────────────── - -do - -- Shared tooltip panel (one global instance) - local _panel, _label - local _visible = false - local _timer = nil - - local function ensurePanel() - if _panel then return end - _panel = gui:newFrame(0, 0, 0, 0) - _panel.color = color.new("#333333") - _panel.visibility = 0 - _panel.drawBorder = false - _panel:setRoundness(4, 4) - _panel:topStack() - - _label = _panel:newTextLabel("", 6, 4, 0, 0, 0, 0, 1, 1) - _label.textColor = color.new("#ffffff") - _label.drawBorder = false - _label.color = {0, 0, 0, 0} - _label.visibility = 0 - _label.ignore = true - _label:setFont(13) - end - - local function showAt(text, x, y) - ensurePanel() - _label.text = text - local fw = _label.font:getWidth(text) + 12 - local fh = _label.font:getHeight() + 8 - local sw, sh = love.graphics.getDimensions() - -- keep tooltip on screen - local px = math.min(x + 14, sw - fw - 4) - local py = math.min(y + 14, sh - fh - 4) - _panel:rawSetDualDim(px, py, fw, fh) - _panel.visibility = 0.92 - _panel:topStack() - _visible = true - end - - local function hide() - if _panel then _panel.visibility = 0 end - _visible = false - end - - function gui:attachTooltip(element, text, delay) - delay = delay or 0.6 - ensurePanel() - - local hoverTimer = nil - local timerDone = false - - element.OnEnter(function(self, mx, my) - timerDone = false - hoverTimer = 0 - end) - - element.OnMoved(function(self, mx, my) - if not timerDone then - -- show once timer expires; track mouse position - if hoverTimer and hoverTimer >= delay then - showAt(text, mx, my) - timerDone = true - end - end - end) - - element.OnExit(function() - hoverTimer = nil - timerDone = false - hide() - end) - - -- We need a per-frame tick to advance the hover timer. - -- OnUpdate is on the element itself so it cleans up when the element dies. - local elapsed = 0 - element:OnUpdate(function(self, dt) - if hoverTimer ~= nil and not timerDone then - hoverTimer = hoverTimer + dt - if hoverTimer >= delay then - local mx, my = love.mouse.getPosition() - showAt(text, mx, my) - timerDone = true - end - end - end) - - element.OnDestroy(function() hide() end) - end - - -- Standalone tooltip object (attach to multiple targets) - function gui:newTooltip(text, delay) - local tt = { text = text, delay = delay or 0.6 } - function tt:attach(element) - gui:attachTooltip(element, self.text, self.delay) - end - function tt:setText(t) - self.text = t - end - return tt - end -end - --- ───────────────────────────────────────────────────────────────────────────── --- 8. WIDGET: TextArea (multiline input) --- --- local ta = parent:newTextArea(text, x, y, w, h, sx, sy, sw, sh) --- --- Properties ta.text (string, may contain "\n") --- ta.readOnly (boolean, default false) --- Events ta.OnChanged(function(self, text) end) --- Methods ta:getText() ta:setText(s) ta:appendLine(s) --- ta:scrollToBottom() --- ───────────────────────────────────────────────────────────────────────────── - -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 - --- ───────────────────────────────────────────────────────────────────────────── --- 9. LAYOUT: ListFrame (auto-stacking container) --- --- local list = parent:newListFrame(x, y, w, h, sx, sy, sw, sh, opts) --- opts = { direction = "vertical"|"horizontal", gap = 4, padding = 6 } --- --- The list watches OnSizeChanged on each child and re-stacks automatically. --- Methods list:reflow() list:setGap(n) list:setPadding(n) --- ───────────────────────────────────────────────────────────────────────────── - -function gui:newListFrame(x, y, w, h, sx, sy, sw, sh, opts) - opts = opts or {} - local direction = opts.direction or "vertical" - local gap = opts.gap or 4 - local padding = opts.padding or 0 - - local frame = self:newFrame(x, y, w or 0, h or 0, sx, sy, sw, sh) - frame.drawBorder = false - frame.color = {0, 0, 0, 0} - frame.visibility = 0 - - -- Track insertion order separately from frame.children (which can be - -- reordered by topStack/bottomStack). - local managed = {} - - local function reflow() - local cursor = padding - for _, child in ipairs(managed) do - if child.visible ~= false then - local _, _, cw, ch = child:getAbsolutes() - if direction == "vertical" then - child:rawSetDualDim(padding, cursor) - cursor = cursor + ch + gap - else - child:rawSetDualDim(cursor, padding) - cursor = cursor + cw + gap - end - end - end - -- Auto-size the frame to fit its content if no fractional scale given - if (sw or 0) == 0 and direction == "vertical" then - -- don't auto-resize width; height grows to fit - frame:rawSetDualDim(nil, nil, nil, cursor + padding - gap) - elseif (sh or 0) == 0 and direction == "horizontal" then - frame:rawSetDualDim(nil, nil, cursor + padding - gap) - end - end - - frame.reflow = reflow - - function frame:setGap(n) - gap = n - reflow() - end - - function frame:setPadding(n) - padding = n - reflow() - end - - -- Override newFrame etc. on this frame so children auto-register - local function wrapChild(child) - managed[#managed + 1] = child - child.OnSizeChanged(reflow) - child.OnDestroy(function() - for i, v in ipairs(managed) do - if v == child then table.remove(managed, i); break end - end - reflow() - end) - reflow() - return child - end - - -- Intercept common constructors so anything added to this list is tracked. - -- We do this by wrapping the frame's methods after creation. - local _newFrame = frame.newFrame - local _newTextButton = frame.newTextButton - local _newTextLabel = frame.newTextLabel - local _newTextBox = frame.newTextBox - local _newImageLabel = frame.newImageLabel - local _newImageButton = frame.newImageButton - local _newProgressBar = frame.newProgressBar - local _newCheckbox = frame.newCheckbox - local _newSlider = frame.newSlider - - function frame:newFrame(...) return wrapChild(_newFrame(self, ...)) end - function frame:newTextButton(...) return wrapChild(_newTextButton(self, ...)) end - function frame:newTextLabel(...) return wrapChild(_newTextLabel(self, ...)) end - function frame:newTextBox(...) return wrapChild(_newTextBox(self, ...)) end - function frame:newImageLabel(...) return wrapChild(_newImageLabel(self, ...)) end - function frame:newImageButton(...) return wrapChild(_newImageButton(self, ...)) end - function frame:newProgressBar(...) return wrapChild(_newProgressBar(self, ...)) end - function frame:newCheckbox(...) return wrapChild(_newCheckbox(self, ...)) end - function frame:newSlider(...) return wrapChild(_newSlider(self, ...)) end - - -- Allow manually registering an externally created child - function frame:register(child) - return wrapChild(child) - end - - reflow() - return frame -end - --- ───────────────────────────────────────────────────────────────────────────── --- 10. LAYOUT: GridFrame (fixed-column auto grid) --- --- local grid = parent:newGridFrame(cols, cellW, cellH, x, y, sx, sy, gap, padding) --- --- Methods grid:reflow() grid:setColumns(n) --- Children are placed left-to-right, wrapping at `cols` columns. --- ───────────────────────────────────────────────────────────────────────────── - -function gui:newGridFrame(cols, cellW, cellH, x, y, sx, sy, gap, padding) - cols = cols or 3 - cellW = cellW or 100 - cellH = cellH or 100 - gap = gap or 4 - padding = padding or 0 - - local totalW = cols * cellW + (cols - 1) * gap + padding * 2 - local frame = self:newFrame(x or 0, y or 0, totalW, 0, sx, sy) - frame.drawBorder = false - frame.color = {0, 0, 0, 0} - frame.visibility = 0 - - local managed = {} - - local function reflow() - local row, col = 0, 0 - for _, child in ipairs(managed) do - local px = padding + col * (cellW + gap) - local py = padding + row * (cellH + gap) - child:rawSetDualDim(px, py, cellW, cellH) - col = col + 1 - if col >= cols then - col = 0 - row = row + 1 - end - end - local rows = math.ceil(#managed / cols) - frame:rawSetDualDim(nil, nil, totalW, rows * (cellH + gap) - gap + padding * 2) - end - - frame.reflow = reflow - - function frame:setColumns(n) - cols = n - totalW = cols * cellW + (cols - 1) * gap + padding * 2 - self:rawSetDualDim(nil, nil, totalW) - reflow() - end - - local function wrapChild(child) - managed[#managed + 1] = child - child.OnDestroy(function() - for i, v in ipairs(managed) do - if v == child then table.remove(managed, i); break end - end - reflow() - end) - reflow() - return child - end - - local _newFrame = frame.newFrame - local _newTextButton = frame.newTextButton - local _newTextLabel = frame.newTextLabel - local _newImageLabel = frame.newImageLabel - local _newImageButton = frame.newImageButton - - function frame:newFrame(...) return wrapChild(_newFrame(self, ...)) end - function frame:newTextButton(...) return wrapChild(_newTextButton(self, ...)) end - function frame:newTextLabel(...) return wrapChild(_newTextLabel(self, ...)) end - function frame:newImageLabel(...) return wrapChild(_newImageLabel(self, ...)) end - function frame:newImageButton(...) return wrapChild(_newImageButton(self, ...)) end - - function frame:register(child) return wrapChild(child) end - - return frame -end - --- ───────────────────────────────────────────────────────────────────────────── --- 11. SCROLL FRAME AUTO-SIZING PATCH --- --- Patches gui:newScrollFrame so the content frame automatically measures its --- children and grows to fit them. No manual setContentSize() needed. --- --- The original newScrollFrame is preserved as gui:newScrollFrameRaw if you --- need the unpatched version. --- ───────────────────────────────────────────────────────────────────────────── - -do - -- Store the original (defined in addons/system.lua after require) - -- We wrap it after the module is loaded. Using a post-load hook via - -- the next iteration of the updater is not practical here, so we patch - -- directly. If addons.system hasn't been required yet this is a no-op and - -- the patch is applied lazily on first call. - local _original - - local function wrap() - if _original then return end - _original = gui.newScrollFrame - if not _original then return end -- system addon not loaded yet - - gui.newScrollFrameRaw = _original - - gui.newScrollFrame = function(self, x, y, w, h, sx, sy, sw, sh) - local content = _original(self, x, y, w, h, sx, sy, sw, sh) - - -- After each child is created under content, measure and resize. - local function measure() - local maxH = 0 - local maxW = 0 - for _, child in ipairs(content:getChildren()) do - local cx, cy, cw, ch = child:getAbsolutes() - local _, contentY = content:getAbsolutes() - local relBottom = (cy - contentY) + ch - local relRight = (cx - (select(1, content:getAbsolutes()))) + cw - if relBottom > maxH then maxH = relBottom end - if relRight > maxW then maxW = relRight end - end - -- Only grow, don't shrink (prevents thrash while items are being added) - local _, _, curW, curH = content:getAbsolutes() - if maxH > curH then content:setDualDim(nil, nil, nil, maxH + 4) end - end - - content.OnCreated(function(child) - child.OnSizeChanged(measure) - child.OnDestroy(measure) - measure() - end) - - return content - end - end - - -- Attempt to patch immediately; if the addon isn't loaded yet this becomes - -- a one-shot deferred patch triggered on first call. - wrap() - - -- Deferred fallback: wrap on first call if not already done - local _deferred = gui.newScrollFrame - if _deferred == _original or _original == nil then - gui.newScrollFrame = function(self, ...) - wrap() -- try again now that more modules may be loaded - return gui.newScrollFrame(self, ...) - end - end -end - --- ───────────────────────────────────────────────────────────────────────────── --- 12. EASING LIBRARY --- --- Extended transition easings to complement transition.glide. --- --- USAGE (same pattern as glide): --- --- local ease = transition.easings --- --- local t = ease.easeInOut(0, 100, 0.4)() --- t.OnStep(function(v) panel:setDualDim(v) end) --- t.OnStop(function() print("done") end) --- --- Available: --- 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 --- ───────────────────────────────────────────────────────────────────────────── - -do - local easings = {} - transition.easings = easings - - -- Helper: build a transition from an easing function f(t) where t is 0→1 - local function makeEasing(f) - return transition:newTransition(function(t, start, stop, time) - local steps = t.fps * time - local piece = time / steps - local range = stop - start - t.running = true - for i = 0, steps do - if not t.kill then - thread.sleep(piece) - local progress = i / steps - thread.pushStatus(start + f(progress) * range) - end - end - t.running = false - t.kill = false - end) - end - - easings.easeIn = makeEasing(function(t) - return t * t - end) - - easings.easeOut = makeEasing(function(t) - return t * (2 - t) - end) - - easings.easeInOut = makeEasing(function(t) - return t < 0.5 and (2 * t * t) or (-1 + (4 - 2*t) * t) - end) - - easings.easeInCubic = makeEasing(function(t) - return t * t * t - end) - - easings.easeOutCubic = makeEasing(function(t) - local u = t - 1 - return u * u * u + 1 - end) - - easings.bounce = makeEasing(function(t) - if t < 1/2.75 then - return 7.5625 * t * t - elseif t < 2/2.75 then - t = t - 1.5/2.75 - return 7.5625 * t * t + 0.75 - elseif t < 2.5/2.75 then - t = t - 2.25/2.75 - return 7.5625 * t * t + 0.9375 - else - t = t - 2.625/2.75 - return 7.5625 * t * t + 0.984375 - end - end) - - easings.elastic = makeEasing(function(t) - if t == 0 or t == 1 then return t end - local p = 0.3 - local s = p / 4 - local u = t - 1 - return -(math.pow(2, 10 * u) * math.sin((u - s) * (2 * math.pi) / p)) - end) - - easings.back = makeEasing(function(t) - local s = 1.70158 - return t * t * ((s + 1) * t - s) - end) -end - --- ───────────────────────────────────────────────────────────────────────────── --- 13. 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 --- --- Per-element events (added to every element at creation via OnCreated): --- element.OnFocusGained(function(self) end) --- element.OnFocusLost(function(self) end) --- --- NOTE: gui.focus tracks focus independently of the internal object_focus. --- It is the public, programmable layer on top. --- ───────────────────────────────────────────────────────────────────────────── - -do - gui.focus = {} - - local _current = nil - local _tabOrder = {} - - -- Attach focus events to every new element - gui.Events.OnCreated(function(element) - if not element.OnFocusGained then - element.OnFocusGained = multi:newConnection() - end - if not element.OnFocusLost then - element.OnFocusLost = multi:newConnection() - end - end) - - function gui.focus.set(element) - if _current == element then return end - if _current and _current.OnFocusLost then - _current.OnFocusLost:Fire(_current) - end - _current = element - if element and element.OnFocusGained then - element.OnFocusGained:Fire(element) - end - end - - function gui.focus.get() - return _current - end - - function gui.focus.clear() - gui.focus.set(nil) - end - - function gui.focus.setTabOrder(list) - _tabOrder = list - end - - function gui.focus.tabNext() - if #_tabOrder == 0 then return end - local idx = 1 - for i, v in ipairs(_tabOrder) do - if v == _current then idx = i + 1; break end - end - if idx > #_tabOrder then idx = 1 end - gui.focus.set(_tabOrder[idx]) - end - - function gui.focus.tabPrev() - if #_tabOrder == 0 then return end - local idx = #_tabOrder - for i, v in ipairs(_tabOrder) do - if v == _current then idx = i - 1; break end - end - if idx < 1 then idx = #_tabOrder end - gui.focus.set(_tabOrder[idx]) - end - - -- Wire Tab / Shift+Tab globally - gui.Events.OnKeyPressed(function(key) - if key == "tab" then - if love.keyboard.isDown("lshift") or love.keyboard.isDown("rshift") then - gui.focus.tabPrev() - else - gui.focus.tabNext() - end - end - end) - - -- Keep gui.focus in sync with mouse clicks on interactive elements. - -- We listen to the global ObjectFocusChanged which fires whenever any - -- element is pressed. - gui.Events.OnObjectFocusChanged(function(prev, next) - gui.focus.set(next) - end) - - -- Elements that are destroyed should be removed from the tab order - -- and should lose focus if they currently hold it. - gui.Events.OnCreated(function(element) - element.OnDestroy(function(self) - if _current == self then gui.focus.clear() end - for i, v in ipairs(_tabOrder) do - if v == self then table.remove(_tabOrder, i); break end - end - end) - end) -end - --- ───────────────────────────────────────────────────────────────────────────── --- 14. Z-INDEX / LAYER SYSTEM --- --- obj:setLayer(n) -- set a numeric z-index (higher = drawn on top) --- obj:getLayer() -- returns current layer (default 0) --- --- The draw order within each parent is sorted by layer every frame. --- Elements with the same layer retain their insertion order (stable sort). --- This replaces the need to call topStack() in OnUpdate for persistent --- ordering requirements. --- --- NOTE: Sorting the children table every frame is O(n log n) in the number --- of siblings. For large flat hierarchies, assign layers once and call --- gui.layers.pause() to skip re-sorting when nothing has changed. --- ───────────────────────────────────────────────────────────────────────────── - -do - gui.layers = {} - local _dirty = false -- set to true when any layer changes - - function gui:setLayer(n) - self.__layer = n - _dirty = true - end - - function gui:getLayer() - return self.__layer or 0 - end - - -- Stable sort: preserve relative order for equal layers - local function stableSort(t, lt) - -- insertion sort (stable and fast for small-to-medium sibling counts) - for i = 2, #t do - local key = t[i] - local j = i - 1 - while j >= 1 and lt(key, t[j]) do - t[j + 1] = t[j] - j = j - 1 - end - t[j + 1] = key - end - end - - local function sortChildren(parent) - stableSort(parent.children, function(a, b) - return (a.__layer or 0) < (b.__layer or 0) - end) - for _, child in ipairs(parent.children) do - if #child.children > 0 then - sortChildren(child) - end - end - end - - function gui.layers.pause() _dirty = false end - function gui.layers.resume() _dirty = true end - - -- Re-sort the tree when layers change. We piggy-back on OnUpdate so this - -- runs before the draw loop. - gui:OnUpdate(function(_, dt) - if not _dirty then return end - _dirty = false - sortChildren(gui) - end) -end - --- ───────────────────────────────────────────────────────────────────────────── --- Return the extension table for optional introspection --- ───────────────────────────────────────────────────────────────────────────── - -return ext diff --git a/gui/addons/init.lua b/gui/addons/init.lua deleted file mode 100644 index 84cae69..0000000 --- a/gui/addons/init.lua +++ /dev/null @@ -1,3 +0,0 @@ --- Addons modify the gui interface directly and do not return anything. -require("gui.addons.extensions") -require("gui.addons.system") \ No newline at end of file diff --git a/gui/addons/system.lua b/gui/addons/system.lua deleted file mode 100644 index fbe77ee..0000000 --- a/gui/addons/system.lua +++ /dev/null @@ -1,955 +0,0 @@ -local multi, thread = require("multi"):init() -local gui = require("gui") -local theme = require("gui.core.theme") -local color = require("gui.core.color") - -local TM_THEME = theme:new({ - primary = "#124559", - primaryDark = "#01161E", - primaryText = "#AEC3B0" -}) - -local default_theme = TM_THEME --- ── layout constants ────────────────────────────────────────────────────────── --- Columns: Indent+Name, State, Status, Uptime, Priority, Pause, Kill --- Name shrunk so data columns have room to breathe -local COL_WIDTHS = { 300, 130, 120, 100, 130, 100, 100, 70 } -local COL_KEYS = { "name", "kind", "state", "status", "uptime", "priority", "pause", "kill" } -local COL_LABELS = { "Name", "Type", "State", "Status", "Uptime", "Priority", "–", "–" } -local SORT_COLS = { "name", "kind", "state", "status", "uptime", "priority" } - -local ROW_H = 28 -local COL_X = {} -do - local acc = 0 - for i, w in ipairs(COL_WIDTHS) do - COL_X[i] = acc - acc = acc + w - end -end -local TOTAL_W = COL_X[#COL_X] + COL_WIDTHS[#COL_WIDTHS] -- 700 - --- ── thread state names ──────────────────────────────────────────────────────── -local STATE_NAMES = { - [1] = "holding", - [2] = "sleeping", - [3] = "hold+time", - [4] = "skipping", - [5] = "hold+cyc", - [6] = "yielding", - [7] = "running", -} -local function fmtState(obj) - if obj._isPaused then return "paused" end - local t = obj.task - if t == nil then return "running" end - return STATE_NAMES[t] or ("state:"..tostring(t)) -end - -local PRIORITY_NAMES = { - [1] = "Core", - [4] = "V.High", - [16] = "High", - [64] = "Above", - [256] = "Normal", - [1024] = "Below", - [4096] = "Low", - [16384] = "V.Low", - [65536] = "Idle", -} -local PRIORITY_CYCLE = { 1, 4, 16, 64, 256, 1024, 4096, 16384, 65536 } -local function fmtPriority(obj) - local p = rawget(obj, "Priority") or rawget(obj, "priority") - if not p then return "n/a" end - return PRIORITY_NAMES[p] or tostring(p) -end -local function nextPriority(current) - for i, v in ipairs(PRIORITY_CYCLE) do - if v == current then - return PRIORITY_CYCLE[(i % #PRIORITY_CYCLE) + 1] - end - end - return 256 -end - --- ── helpers ─────────────────────────────────────────────────────────────────── -local function fmtUptime(secs) - secs = math.floor(secs) - if secs < 60 then return secs .. "s" end - if secs < 3600 then return math.floor(secs/60).."m "..(secs%60).."s" end - return math.floor(secs/3600).."h "..math.floor((secs%3600)/60).."m" -end - -local function nowClock() return os.clock() end - --- ── data collection ─────────────────────────────────────────────────────────── --- Returns a flat list of rows with depth so the UI can indent names. -local function collectTasks() - local rows = {} - local stats = multi:getStats() - - local function addProc(fullname, proc, depth) - -- Processor header row - rows[#rows+1] = { - isProc = true, - depth = depth, - name = proc.name or fullname, - fullname = fullname, - kind = "processor", - conns = proc.connections or 0, - subs = proc.subscriptions or 0, - } - -- Tasks (Mainloop actors) - local tasks = proc.tasks or {} - for _, task in pairs(tasks) do - if not task.isProcessThread then - rows[#rows+1] = { - isProc = false, - depth = depth + 1, - name = task:getName() or "?", - fullname = fullname, - kind = tostring(task.Type), - state = fmtState(task), - active = not task:isPaused(), - uptime = nowClock() - (task.UPTIME or nowClock()), - priority = rawget(task, "Priority") or 256, - fmtPri = fmtPriority(task), - obj = task, - isThread = false, - } - end - end - -- Threads - local threads = proc.threads or {} - for _, th in pairs(threads) do - rows[#rows+1] = { - isProc = false, - depth = depth + 1, - name = th:getName() or "?", - fullname = fullname, - kind = tostring(th.Type), - state = fmtState(th), - active = not th:isPaused(), - uptime = nowClock() - (th.UPTIME or nowClock()), - priority = rawget(th, "Priority") or 256, - fmtPri = fmtPriority(th), - obj = th, - isThread = true, - } - end - end - - -- Root first, then sub-processors sorted - if stats["root"] then - addProc("root", stats["root"], 0) - end - local procNames = {} - for k in pairs(stats) do - if k ~= "root" then procNames[#procNames+1] = k end - end - table.sort(procNames) - for _, k in ipairs(procNames) do - addProc(k, stats[k], 1) - end - - return rows -end - -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 - viewport.drawBorder = false - - local content = viewport:newFrame(0, 0, w, 0) - content.drawBorder = false - - local scrollY = 0 - local maxScrollY = 0 - local scrollX = 0 - local maxScrollX = 0 - local SCROLL_SPEED = 40 - local SCROLL_BAR_W = 8 - - local vBar = viewport:newFrame(-SCROLL_BAR_W, 0, SCROLL_BAR_W, 0, 1, 0, 0, 1) - vBar.color = {0.3, 0.3, 0.3} - vBar.drawBorder = false - vBar.visible = false - - local vThumb = vBar:newFrame(0, 0, SCROLL_BAR_W, 40) - vThumb.color = {0.6, 0.6, 0.6} - vThumb.drawBorder = false - - local hBar = viewport:newFrame(0, -SCROLL_BAR_W, 0, SCROLL_BAR_W, 0, 1, 1) - hBar.color = {0.3, 0.3, 0.3} - hBar.drawBorder = false - hBar.visible = false - - local hThumb = hBar:newFrame(0, 0, 40, SCROLL_BAR_W) - hThumb.color = {0.6, 0.6, 0.6} - hThumb.drawBorder = false - - local applying = false - - local function getViewSize() - local _, _, vw, vh = viewport:getAbsolutes() - return vw, vh - end - - local function clamp(val, lo, hi) - return math.max(lo, math.min(hi, val)) - end - - local function updateScrollbars() - if applying then return end - local vw, vh = getViewSize() - local _, _, cw, ch = content:getAbsolutes() - - maxScrollY = math.max(0, ch - vh) - if maxScrollY > 0 then - vBar.visible = true - local thumbH = math.max(20, vh * (vh / ch)) - local thumbY = (scrollY / maxScrollY) * (vh - thumbH) - vThumb:setDualDim(0, thumbY, SCROLL_BAR_W, thumbH) - else - vBar.visible = false - scrollY = 0 - end - - maxScrollX = math.max(0, cw - vw) - if maxScrollX > 0 then - hBar.visible = true - local thumbW = math.max(20, vw * (vw / cw)) - local thumbX = (scrollX / maxScrollX) * (vw - thumbW) - hThumb:setDualDim(thumbX, 0, thumbW, SCROLL_BAR_W) - else - hBar.visible = false - scrollX = 0 - end - end - - local function applyScroll() - if applying then return end - applying = true - scrollY = clamp(scrollY, 0, maxScrollY) - scrollX = clamp(scrollX, 0, maxScrollX) - content:setDualDim(-scrollX, -scrollY) - updateScrollbars() - applying = false - end - - viewport.OnWheelMoved(function(x, y) - scrollY = scrollY - y * SCROLL_SPEED - applyScroll() - end) - - vThumb:enableDragging(gui.MOUSE_PRIMARY) - vThumb.OnDragging(function(self, dx, dy) - local _, vh = getViewSize() - local _, _, _, thumbH = vThumb:getAbsolutes() - local trackH = vh - thumbH - if trackH <= 0 then return end - scrollY = scrollY + dy * (maxScrollY / trackH) - applyScroll() - end) - - hThumb:enableDragging(gui.MOUSE_PRIMARY) - hThumb.OnDragging(function(self, dx, dy) - local vw, _ = getViewSize() - local _, _, thumbW = hThumb:getAbsolutes() - local trackW = vw - thumbW - if trackW <= 0 then return end - scrollX = scrollX + dx * (maxScrollX / trackW) - applyScroll() - end) - - content.OnSizeChanged(function() - if applying then return end - local _, _, cw, ch = content:getAbsolutes() - local vw, vh = getViewSize() - maxScrollY = math.max(0, ch - vh) - maxScrollX = math.max(0, cw - vw) - scrollY = clamp(scrollY, 0, maxScrollY) - scrollX = clamp(scrollX, 0, maxScrollX) - updateScrollbars() - end) - - viewport.OnSizeChanged(function() - if applying then return end - applyScroll() - end) - - function content:scrollTo(sy, sx) - scrollY = sy or scrollY - scrollX = sx or scrollX - applyScroll() - end - function content:scrollBy(dy, dx) - scrollY = scrollY + (dy or 0) - scrollX = scrollX + (dx or 0) - applyScroll() - end - function content:scrollToBottom() scrollY = maxScrollY; applyScroll() end - function content:scrollToTop() scrollY = 0; applyScroll() end - function content:setScrollSpeed(speed) SCROLL_SPEED = speed end - function content:getScrollPos() return scrollX, scrollY end - function content:getMaxScroll() return maxScrollX, maxScrollY end - - local _baseSDD = content.setDualDim - function content:setContentSize(cw, ch) - _baseSDD(self, nil, nil, cw or select(3, self:getAbsolutes()), ch) - applyScroll() - end - - local _baseDestroy = viewport.destroy - function viewport:destroy() - content:destroy() - _baseDestroy(self) - end - - applyScroll() - return content -end - --- ── window constructor (unchanged from original) ────────────────────────────── -local windowCount = 0 -function gui:newWindow(x, y, w, h, sx, sy, sw, sh, 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, sx, sy, sw) - header:setRoundness(10, 10, nil, "top") - local window = header:newFrame(0, 35, 0, h, sx, sy, 1, sh) - 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:respectHierarchy(false) - X.align = gui.ALIGN_CENTER - X.color = color.red - window.XButton = X - - 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 = multi:newConnection() - X.OnPressed(function(self, ...) - window.OnClose:Fire(window, ...) - end) - 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 -local COLOR_ROW_ODD = TM_THEME.colorPrimaryDark -local COLOR_DEAD = { 0.5, 0.1, 0.1 } - -local function makeRowPool(scrollFrame) - local pool = { rows = {}, active = 0 } - - local function makeRow(idx) - local yOff = (idx - 1) * ROW_H - local bg = scrollFrame:newFrame(0, yOff, TOTAL_W, ROW_H) - bg.drawBorder = false - - -- Name label (col 1) with indent support - local nameLabel = bg:newTextLabel("", COL_X[1] + 4, 0, COL_WIDTHS[1] - 4, ROW_H) - nameLabel.align = gui.ALIGN_LEFT - nameLabel.ignore = true - - -- Type, State, Status, Uptime, Priority labels - local kindLbl = bg:newTextLabel("", COL_X[2], 0, COL_WIDTHS[2], ROW_H) - local stateLbl = bg:newTextLabel("", COL_X[3], 0, COL_WIDTHS[3], ROW_H) - local statusLbl = bg:newTextLabel("", COL_X[4], 0, COL_WIDTHS[4], ROW_H) - local uptimeLbl = bg:newTextLabel("", COL_X[5], 0, COL_WIDTHS[5], ROW_H) - local priorityLbl = bg:newTextLabel("", COL_X[6], 0, COL_WIDTHS[6], ROW_H) - for _, lbl in ipairs({kindLbl, stateLbl, statusLbl, uptimeLbl, priorityLbl}) do - lbl.align = gui.ALIGN_CENTER - lbl.ignore = true - end - - -- Pause / Resume button - local pauseBtn = bg:newTextButton("", COL_X[7] + 2, 2, COL_WIDTHS[7] - 4, ROW_H - 4) - pauseBtn.align = gui.ALIGN_CENTER - - -- Kill button (red) - local killBtn = bg:newTextButton("Kill", COL_X[8] + 2, 2, COL_WIDTHS[8] - 4, ROW_H - 4) - killBtn.align = gui.ALIGN_CENTER - killBtn.color = color.darken(color.red, .1) - - local row = { - bg = bg, - nameLabel = nameLabel, - kindLbl = kindLbl, - stateLbl = stateLbl, - statusLbl = statusLbl, - uptimeLbl = uptimeLbl, - priorityLbl = priorityLbl, - pauseBtn = pauseBtn, - killBtn = killBtn, - obj = nil, - isProc = false, - } - - pauseBtn.OnReleased(function() - if not row.obj then return end - if row.obj:isPaused() then - row.obj:Resume() - else - row.obj:Pause() - end - statusLbl.text = row.obj:isPaused() and "Paused" or "Running" - pauseBtn.text = row.obj:isPaused() and "Resume" or "Pause" - end) - - killBtn.OnReleased(function() - if not row.obj or row.isProc then return end - if row.obj.Kill then - row.obj:Kill() - elseif row.obj.Destroy then - row.obj:Destroy() - end - bg.color = COLOR_DEAD - end) - - -- Priority label is clickable to cycle priority - priorityLbl.ignore = false - priorityLbl.OnReleased(function() - if not row.obj or row.isProc then return end - local cur = rawget(row.obj, "Priority") or 256 - local next = nextPriority(cur) - if row.obj.setPriority then - row.obj:setPriority(next) - priorityLbl.text = PRIORITY_NAMES[next] or tostring(next) - end - end) - - return row - end - - function pool:ensure(n) - while #self.rows < n do - self.rows[#self.rows + 1] = makeRow(#self.rows + 1) - end - end - - function pool:apply(data) - self:ensure(#data) - self.active = #data - - for i, d in ipairs(data) do - local row = self.rows[i] - row.isProc = d.isProc - row.obj = d.isProc and nil or d.obj - - -- Row y position - row.bg:setDualDim(nil, (i - 1) * ROW_H) - row.bg.visible = true - - if d.isProc then - -- Processor header row - row.bg.color = COLOR_PROC_ROW - row.nameLabel.text = string.rep(" ", d.depth) .. "[" .. d.name .. "]" - row.kindLbl.text = "processor" - row.stateLbl.text = "" - row.statusLbl.text = "" - row.uptimeLbl.text = d.conns .. "c/" .. d.subs .. "s" - row.priorityLbl.text = "" - row.pauseBtn.text = "" - row.pauseBtn.visible = false - row.killBtn.visible = false - else - row.bg.color = (i % 2 == 0) and COLOR_ROW_EVEN or COLOR_ROW_ODD - row.nameLabel.text = string.rep(" ", d.depth) .. d.name - row.kindLbl.text = d.kind or "" - row.stateLbl.text = d.state or "" - row.statusLbl.text = d.active and "Running" or "Paused" - row.uptimeLbl.text = fmtUptime(d.uptime) - row.priorityLbl.text = d.fmtPri or "" - row.pauseBtn.text = d.active and "Pause" or "Resume" - row.pauseBtn.visible = true - row.killBtn.visible = true - end - end - - -- Hide unused rows - for i = #data + 1, #self.rows do - self.rows[i].bg.visible = false - self.rows[i].obj = nil - end - - scrollFrame:setDualDim(nil, nil, nil, math.max(#data * ROW_H, 1)) - end - - return pool -end - --- ── error log pool ──────────────────────────────────────────────────────────── -local ERROR_ROW_H = 22 -local MAX_ERRORS = 200 - -local function makeErrorPool(scrollFrame) - local pool = { rows = {}, entries = {} } - - local function makeRow(idx) - local yOff = (idx - 1) * ERROR_ROW_H - local bg = scrollFrame:newFrame(0, yOff, TOTAL_W, ERROR_ROW_H) - bg.color = (idx % 2 == 0) and COLOR_ROW_EVEN or COLOR_ROW_ODD - bg.drawBorder = false - local lbl = bg:newTextLabel("", 4, 0, TOTAL_W - 4, ERROR_ROW_H) - lbl.align = gui.ALIGN_LEFT - lbl.ignore = true - return { bg = bg, lbl = lbl } - end - - function pool:ensure(n) - while #self.rows < n do - self.rows[#self.rows + 1] = makeRow(#self.rows + 1) - end - end - - function pool:addEntry(msg, source) - if #self.entries >= MAX_ERRORS then - table.remove(self.entries, 1) - end - local ts = string.format("[%.1fs]", os.clock()) - self.entries[#self.entries + 1] = ts .. " [" .. (source or "?") .. "] " .. tostring(msg) - self:refresh() - end - - function pool:refresh() - local n = #self.entries - self:ensure(n) - for i, entry in ipairs(self.entries) do - local row = self.rows[i] - row.bg:setDualDim(nil, (i - 1) * ERROR_ROW_H) - row.bg.visible = true - row.lbl.text = entry - end - for i = n + 1, #self.rows do - self.rows[i].bg.visible = false - end - scrollFrame:setDualDim(nil, nil, nil, math.max(n * ERROR_ROW_H, 1)) - scrollFrame:scrollToBottom() - end - - function pool:clear() - self.entries = {} - self:refresh() - end - - return pool -end - --- ── column header row ───────────────────────────────────────────────────────── -local function makeHeader(parent, onSort) - local hdr = parent:newFrame(0, 0, TOTAL_W, ROW_H) - hdr.color = TM_THEME.colorPrimaryDark - hdr.drawBorder = false - - local sortCol = nil - local sortAsc = true - local indicators = {} - - for i, t in ipairs(COL_LABELS) do - local isSortable = false - for _, k in ipairs(SORT_COLS) do - if k == COL_KEYS[i] then isSortable = true; break end - end - - if isSortable then - local btn = hdr:newTextButton(t, COL_X[i], 0, COL_WIDTHS[i], ROW_H) - btn.align = (i == 1) and gui.ALIGN_LEFT or gui.ALIGN_CENTER - indicators[COL_KEYS[i]] = btn - local key = COL_KEYS[i] - btn.OnReleased(function() - if sortCol == key then - sortAsc = not sortAsc - else - sortCol = key - sortAsc = true - end - -- Reset all sortable headers, then mark the active one - for j, label in ipairs(COL_LABELS) do - local b = indicators[COL_KEYS[j]] - if b then - if COL_KEYS[j] == sortCol then - b.text = label .. (sortAsc and " ▲" or " ▼") - else - b.text = label - end - end - end - if onSort then onSort(key, sortAsc) end - end) - else - local lbl = hdr:newTextLabel(t, COL_X[i], 0, COL_WIDTHS[i], ROW_H) - lbl.align = gui.ALIGN_CENTER - lbl.ignore = true - lbl.textColor = TM_THEME.colorPrimaryText - end - end - return hdr -end - --- ── tab bar ─────────────────────────────────────────────────────────────────── -local TAB_H = 28 -local function makeTabBar(parent, tabs, onSwitch) - local bar = parent:newFrame(0, 0, 0, TAB_H, 0, 0, 1) - bar.color = TM_THEME.colorPrimaryDark - bar.drawBorder = false - local tabW = math.floor(TOTAL_W / #tabs) - local btns = {} - for i, label in ipairs(tabs) do - local btn = bar:newTextButton(label, (i-1)*tabW, 0, tabW, TAB_H) - btn.align = gui.ALIGN_CENTER - btns[i] = btn - btn.OnReleased(function() - onSwitch(i) - end) - end - return bar, btns -end - --- ── sort helper ─────────────────────────────────────────────────────────────── -local function sortRows(rows, key, asc) - local function cmp(a, b) - -- Processor rows always float to top within their group; we keep them stable - if a.isProc and b.isProc then return a.fullname < b.fullname end - if a.isProc then return true end - if b.isProc then return false end - local va, vb - if key == "name" then va, vb = a.name or "", b.name or "" - elseif key == "kind" then va, vb = a.kind or "", b.kind or "" - elseif key == "state" then va, vb = a.state or "", b.state or "" - elseif key == "status" then va, vb = (a.active and 0 or 1), (b.active and 0 or 1) - elseif key == "uptime" then va, vb = a.uptime or 0, b.uptime or 0 - elseif key == "priority" then va, vb = a.priority or 256, b.priority or 256 - else va, vb = tostring(a[key] or ""), tostring(b[key] or "") - end - if asc then return va < vb else return va > vb end - end - -- Stable-ish sort: keep proc header immediately before its children - -- For simplicity we sort the flat list but keep proc rows pinned before - -- the first non-proc row that shares the same fullname. - table.sort(rows, cmp) -end - --- ── public API ──────────────────────────────────────────────────────────────── -local taskManager - -function gui:showTaskManager() - if taskManager then return end - - local WIN_W = TOTAL_W + 20 - local WIN_H = 620 - - taskManager = gui:newWindow(0, 0, WIN_W, WIN_H, nil, nil, nil, nil, "Task Manager", true, TM_THEME) - taskManager.clipDescendants = true - - -- ── tab bar ────────────────────────────────────────────────────────────── - local currentTab = 1 -- 1 = tasks, 2 = errors - local taskPanel, errorPanel - - local tabBar, tabBtns = makeTabBar(taskManager, {"Tasks", "Errors"}, function(idx) - currentTab = idx - taskPanel.visible = (idx == 1) - errorPanel.visible = (idx == 2) - end) - - -- ── tasks panel ────────────────────────────────────────────────────────── - taskPanel = taskManager:newFrame(0, TAB_H, 0, -TAB_H, 0, 0, 1, 1) - taskPanel.drawBorder = false - taskPanel.clipDescendants = true - - -- Load bar strip (sits below tab bar, above column headers) - local LOAD_H = 20 - local loadStrip = taskPanel:newFrame(0, 0, 0, LOAD_H, 0, 0, 1) - loadStrip.color = TM_THEME.colorPrimaryDark - loadStrip.drawBorder = false - - local loadFill = loadStrip:newFrame(0, 2, 1, LOAD_H - 4) -- absolute w=1, no relative anchors - loadFill.color = { 0.1, 0.6, 0.3 } - loadFill.drawBorder = false - - -- Label is created AFTER fill so it draws on top of it - local loadLbl = loadStrip:newTextLabel("Load: …", 4, 0, TOTAL_W - 8, LOAD_H) - loadLbl.align = gui.ALIGN_LEFT - loadLbl.ignore = true - - local sortKey = nil - local sortAsc = true - - local colHdr = makeHeader(taskPanel, function(key, asc) - sortKey = key - sortAsc = asc - end) - colHdr:setDualDim(nil, LOAD_H) - - local scrollFrame = taskPanel:newScrollFrame(0, LOAD_H + ROW_H, 0, -(LOAD_H + ROW_H), 0, 0, 1, 1) - local pool = makeRowPool(scrollFrame) - - -- ── error panel ────────────────────────────────────────────────────────── - errorPanel = taskManager:newFrame(0, TAB_H, 0, -TAB_H, 0, 0, 1, 1) - errorPanel.drawBorder = false - errorPanel.clipDescendants = true - errorPanel.visible = false - - local errHdr = errorPanel:newFrame(0, 0, 0, ROW_H, 0, 0, 1) - errHdr.color = TM_THEME.colorPrimaryDark - errHdr.drawBorder = false - local errTitle = errHdr:newTextLabel("Error Log", 4, 0, 200, ROW_H) - errTitle.align = gui.ALIGN_LEFT - errTitle.ignore = true - - local clearBtn = errHdr:newTextButton("Clear", -70, 2, 66, ROW_H - 4, 1) - clearBtn.align = gui.ALIGN_CENTER - - local errScroll = errorPanel:newScrollFrame(0, ROW_H, 0, -ROW_H, 0, 0, 1, 1) - local errorPool = makeErrorPool(errScroll) - - clearBtn.OnReleased(function() errorPool:clear() end) - - -- ── wire up error capture ───────────────────────────────────────────────── - -- Thread errors fire on the *thread's own* OnError, not the processor's. - -- We use OnObjectCreated to hook every thread as it is born, on every - -- processor (including ones created after the task manager opens). - -- We also walk existing threads retroactively for processors already running. - - local hookedThreads = {} -- weak set so we don't prevent GC - - local function hookThread(th, procName) - if not th.OnError then return end - if hookedThreads[th] then return end - hookedThreads[th] = true - th.OnError(function(self, err,t) - local msg = type(err) == "string" and err or tostring(err or "unknown error") - local name = (th.getName and th:getName()) or "?" - errorPool:addEntry(msg, procName .. "/" .. name) - end) - end - - local function hookProc(proc, procName) - -- Hook threads already alive on this processor - local threads = proc.threads or {} - for _, th in ipairs(threads) do - hookThread(th, procName) - end - -- Hook threads created in future on this processor - proc.OnObjectCreated(function(obj) - if obj.Type == multi.registerType("thread", "threads") then - hookThread(obj, procName) - end - end) - end - - -- Root process - hookProc(multi, "root") - -- All processors currently registered - for _, proc in ipairs(multi:getProcessors()) do - hookProc(proc, proc:getName()) - end - -- Any processors created after this point - multi.OnObjectCreated(function(obj) - if obj.Type == multi.registerType("process", "processes") then - hookProc(obj, obj:getName()) - end - end) - - -- ── stat line ───────────────────────────────────────────────────────────── - local function setStatLine(n) - taskManager:setTitle("Task Manager — " .. n .. " objects") - end - - -- ── background thread: collect tasks ────────────────────────────────────── - local function isOpen() - return not taskManager:isDescendantOf(gui.virtual) - end - - local pendingData = nil - local dirty = false - - taskManager.process:newThread("TM_collect", function() - while true do - thread.hold(isOpen) - local data = collectTasks() - pendingData = data - dirty = true - thread.sleep(1) - end - end) - - -- ── load probe ──────────────────────────────────────────────────────────── - -- Install once. getLoad() is now non-blocking — just reads the EMA state. - local schedulerProbe = require("gui.core.probe") - schedulerProbe:install(multi) - - -- ── main-thread update ──────────────────────────────────────────────────── - taskManager.OnUpdate(function() - taskManager:topStack() - -- Apply task data - if dirty and pendingData then - dirty = false - local data = pendingData - pendingData = nil - - if sortKey then - sortRows(data, sortKey, sortAsc) - end - - pool:apply(data) - setStatLine(#data) - end - - -- Load bar — getLoad() is now non-blocking, safe to call every frame - local pct, lagMs = multi:getLoad() - local _, _, barW, _ = loadStrip:getAbsolutes() - local fillW = math.max(1, math.floor(barW * pct / 100)) - loadFill:setDualDim(nil, nil, fillW) - if pct < 50 then - loadFill.color = { 0.1, 0.6, 0.3 } - elseif pct < 80 then - loadFill.color = { 0.8, 0.6, 0.1 } - else - loadFill.color = { 0.8, 0.15, 0.1 } - end - loadLbl.text = string.format("Load: %d%% Lag: %.1fms", pct, lagMs) - end) -end - --- ── hotkey ──────────────────────────────────────────────────────────────────── -ToggleTaskManager = gui:setHotKey({"lctrl","t"}) + - gui:setHotKey({"rctrl","t"}) - -ToggleTaskManager(function() - if not taskManager then - gui:showTaskManager() - elseif taskManager:isActive() then - taskManager:close() - else - taskManager:open() - end -end) - -ToggleTaskManager:Fire() -taskManager:close() diff --git a/gui/assets/pause.png b/gui/assets/pause.png deleted file mode 100644 index 4af84bc..0000000 Binary files a/gui/assets/pause.png and /dev/null differ diff --git a/gui/assets/play.png b/gui/assets/play.png deleted file mode 100644 index a231853..0000000 Binary files a/gui/assets/play.png and /dev/null differ diff --git a/gui/changes.md b/gui/changes.md deleted file mode 100644 index 4e768b5..0000000 --- a/gui/changes.md +++ /dev/null @@ -1 +0,0 @@ -# \ No newline at end of file diff --git a/gui/core/canvas.lua b/gui/core/canvas.lua deleted file mode 100644 index c600705..0000000 --- a/gui/core/canvas.lua +++ /dev/null @@ -1,26 +0,0 @@ -local gui = require("gui") - -function newCanvas(domain) - local c - if domain == "visual" then - c = gui:newVisualFrame() - else - c = gui:newVirtualFrame() - end - c:fullFrame() - function c:swap(c1, c2) - local temp = c1.children - c1.children = c2.children - c2.children = temp - for i,v in pairs(c1.children) do - v.parent = c1 - end - for i,v in pairs(c2.children) do - v.parent = c2 - end - end - - return c -end - -return newCanvas \ No newline at end of file diff --git a/gui/core/color.lua b/gui/core/color.lua deleted file mode 100644 index d0b191c..0000000 --- a/gui/core/color.lua +++ /dev/null @@ -1,1660 +0,0 @@ -local color={} -local mt = { - __add = function (c1,c2) - 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[3]-c2[3]) - end, - __mul = function (c1,c2) - 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[3]/c2[3]) - end, - __mod = function (c1,c2) - 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[3]^c2[3]) - end, - __unm = function (c1) - return color.new(-c1[1],-c1[2],-c1[2]) - end, - __tostring = function(c) - 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[3]==c2[3]) - end, - __lt = function (c1,c2) - return (c1[1]= .5 -end - -function color.getAverageLightness(r, g, b) - if type(r) == "string" then - r, g, b = tonumber(string.sub(r,1,2),16),tonumber(string.sub(r,3,4),16),tonumber(string.sub(r,5,6),16) - elseif type(r) == "table" then - r, g, b = r[1], r[2], r[3] - end - return (r + g + b) / 3 -end - -function color.rgbToHex(r, g, b) - if type(r) == "string" then - r, g, b = tonumber(string.sub(r,1,2),16),tonumber(string.sub(r,3,4),16),tonumber(string.sub(r,5,6),16) - elseif type(r) == "table" then - r, g, b = r[1], r[2], r[3] - end - - r, g, b = love.math.colorToBytes(r,g,b,0) - - local rgb = (r * 0x10000) + (g * 0x100) + b - return string.format("%06x", rgb) - end - -function color.new(r, g, b, a) - local temp - - if type(r) == "string" then - r = r:gsub("%s",""):gsub("%%","") - if r:sub(1,4) == "rgba" then - local sr,sg,sb,sa = r:match("rgba%((%d-),(%d-),(%d-),(%d*%.?%d+)%)") - r,g,b = love.math.colorFromBytes(tonumber(sr),tonumber(sg),tonumber(sb)) - a = tonumber(sa) - elseif r:sub(1,4) == "hsla" then - local sh,ss,sl,sa = r:match("hsla%((%d-),(%d-),(%d-),(%d*%.?%d+)%)") - r,g,b,a = color.hsl(tonumber(sh), tonumber(ss), tonumber(sl), tonumber(sa)) - elseif r:sub(1,3) == "hsl" then - local sh,ss,sl = r:match("hsl%((%d-),(%d-),(%d-)%)") - r,g,b,a = color.hsl(tonumber(sh), tonumber(ss), tonumber(sl)) - elseif r:sub(1,3) == "rgb" then - local sr,sg,sb = r:match("rgb%((%d-),(%d-),(%d-)%)") - r,g,b = love.math.colorFromBytes(tonumber(sr),tonumber(sg),tonumber(sb)) - else - if r:sub(1,1) == "#" then - r = r:sub(2,-1) - end - if #r == 8 then - r, g, b, a = tonumber(string.sub(r,1,2),16),tonumber(string.sub(r,3,4),16),tonumber(string.sub(r,5,6),16),tonumber(string.sub(r,7,8),16) - else - r, g, b = tonumber(string.sub(r,1,2),16),tonumber(string.sub(r,3,4),16),tonumber(string.sub(r,5,6),16) - end - temp = {love.math.colorFromBytes(r, g, b, a or 255)} - end - elseif type(r) == "table" then - return r - end - - if not temp then - temp = {r, b, g, a} - end - - setmetatable(temp, mt) - return temp -end - -function color.random() - return color.new(math.random(0,10000000)/10000000, math.random(0,10000000)/10000000, math.random(0,10000000)/10000000, 1) -end - -function color.indexColor(name,r, g, b) - local c = color.new(r,g,b) - -- Other ways to index a color - color[string.lower(name)] = c - color[string.upper(name)] = c - color[string.upper(string.sub(name,1,1))..string.lower(string.sub(name,2))] = c -end - -function color.darken(c, v) - local currentR,currentG,currentB=c[1],c[2],c[3] - return color.new((currentR*255) * (1 - v),(currentG*255) * (1 - v),(currentB*255) * (1 - v)) -end - -function color.lighten(c, v) - local currentR,currentG,currentB=c[1],c[2],c[3] - return color.new(currentR*255 + (255 - (currentR*255)) * v,currentG*255 + (255 - (currentG*255)) * v,currentB*255 + (255 - (currentB*255)) * v) -end - --- Add a ton of colors sourced from online - -color.indexColor("WHITE","rgb(255,255,255)") -color.indexColor("MAROON","rgb(128,20,20)") -color.indexColor("DARK_RED","rgb(139,20,20)") -color.indexColor("BROWN","rgb(165,42,42)") -color.indexColor("FIREBRICK","rgb(178,34,34)") -color.indexColor("CRIMSON","rgb(220,20,60)") -color.indexColor("RED","rgb(255,20,20)") -color.indexColor("TOMATO","rgb(255,99,71)") -color.indexColor("CORAL","rgb(255,127,80)") -color.indexColor("INDIAN_RED","rgb(205,92,92)") -color.indexColor("LIGHT_CORAL","rgb(240,128,128)") -color.indexColor("DARK_SALMON","rgb(233,150,122)") -color.indexColor("SALMON","rgb(250,128,114)") -color.indexColor("LIGHT_SALMON","rgb(255,160,122)") -color.indexColor("ORANGE_RED","rgb(255,69,20)") -color.indexColor("DARK_ORANGE","rgb(255,140,20)") -color.indexColor("ORANGE","rgb(255,165,20)") -color.indexColor("GOLD","rgb(255,215,20)") -color.indexColor("DARK_GOLDEN_ROD","rgb(184,134,11)") -color.indexColor("GOLDEN_ROD","rgb(218,165,32)") -color.indexColor("PALE_GOLDEN_ROD","rgb(238,232,170)") -color.indexColor("DARK_KHAKI","rgb(189,183,107)") -color.indexColor("KHAKI","rgb(240,230,140)") -color.indexColor("OLIVE","rgb(128,128,20)") -color.indexColor("YELLOW_GREEN","rgb(154,205,50)") -color.indexColor("DARK_OLIVE_GREEN","rgb(85,107,47)") -color.indexColor("OLIVE_DRAB","rgb(107,142,35)") -color.indexColor("LAWN_GREEN","rgb(124,252,20)") -color.indexColor("CHART_REUSE","rgb(127,255,20)") -color.indexColor("GREEN_YELLOW","rgb(173,255,47)") -color.indexColor("DARK_GREEN","rgb(20,100,20)") -color.indexColor("FOREST_GREEN","rgb(34,139,34)") -color.indexColor("LIME","rgb(20,255,20)") -color.indexColor("LIME_GREEN","rgb(50,205,50)") -color.indexColor("LIGHT_GREEN","rgb(144,238,144)") -color.indexColor("PALE_GREEN","rgb(152,251,152)") -color.indexColor("DARK_SEA_GREEN","rgb(143,188,143)") -color.indexColor("MEDIUM_SPRING_GREEN","rgb(20,250,154)") -color.indexColor("SPRING_GREEN","rgb(20,255,127)") -color.indexColor("SEA_GREEN","rgb(46,139,87)") -color.indexColor("MEDIUM_AQUA_MARINE","rgb(102,205,170)") -color.indexColor("MEDIUM_SEA_GREEN","rgb(60,179,113)") -color.indexColor("LIGHT_SEA_GREEN","rgb(32,178,170)") -color.indexColor("DARK_SLATE_GRAY","rgb(47,79,79)") -color.indexColor("TEAL","rgb(20,128,128)") -color.indexColor("DARK_CYAN","rgb(20,139,139)") -color.indexColor("LIGHT_CYAN","rgb(224,255,255)") -color.indexColor("DARK_TURQUOISE","rgb(20,206,209)") -color.indexColor("TURQUOISE","rgb(64,224,208)") -color.indexColor("MEDIUM_TURQUOISE","rgb(72,209,204)") -color.indexColor("PALE_TURQUOISE","rgb(175,238,238)") -color.indexColor("AQUA_MARINE","rgb(127,255,212)") -color.indexColor("POWDER_BLUE","rgb(176,224,230)") -color.indexColor("CADET_BLUE","rgb(95,158,160)") -color.indexColor("STEEL_BLUE","rgb(70,130,180)") -color.indexColor("CORN_FLOWER_BLUE","rgb(100,149,237)") -color.indexColor("DEEP_SKY_BLUE","rgb(20,191,255)") -color.indexColor("DODGER_BLUE","rgb(30,144,255)") -color.indexColor("LIGHT_BLUE","rgb(173,216,230)") -color.indexColor("SKY_BLUE","rgb(135,206,235)") -color.indexColor("LIGHT_SKY_BLUE","rgb(135,206,250)") -color.indexColor("MIDNIGHT_BLUE","rgb(25,25,112)") -color.indexColor("NAVY","rgb(20,20,128)") -color.indexColor("DARK_BLUE","rgb(20,20,139)") -color.indexColor("MEDIUM_BLUE","rgb(20,20,205)") -color.indexColor("BLUE","rgb(20,20,255)") -color.indexColor("ROYAL_BLUE","rgb(65,105,225)") -color.indexColor("BLUE_VIOLET","rgb(138,43,226)") -color.indexColor("INDIGO","rgb(75,20,130)") -color.indexColor("DARK_SLATE_BLUE","rgb(72,61,139)") -color.indexColor("SLATE_BLUE","rgb(106,90,205)") -color.indexColor("MEDIUM_SLATE_BLUE","rgb(123,104,238)") -color.indexColor("MEDIUM_PURPLE","rgb(147,112,219)") -color.indexColor("DARK_MAGENTA","rgb(139,20,139)") -color.indexColor("DARK_VIOLET","rgb(148,20,211)") -color.indexColor("DARK_ORCHID","rgb(153,50,204)") -color.indexColor("MEDIUM_ORCHID","rgb(186,85,211)") -color.indexColor("PURPLE","rgb(128,20,128)") -color.indexColor("THISTLE","rgb(216,191,216)") -color.indexColor("PLUM","rgb(221,160,221)") -color.indexColor("VIOLET","rgb(238,130,238)") -color.indexColor("MAGENTA","rgb(255,20,255)") -color.indexColor("ORCHID","rgb(218,112,214)") -color.indexColor("MEDIUM_VIOLET_RED","rgb(199,21,133)") -color.indexColor("PALE_VIOLET_RED","rgb(219,112,147)") -color.indexColor("DEEP_PINK","rgb(255,20,147)") -color.indexColor("HOT_PINK","rgb(255,105,180)") -color.indexColor("LIGHT_PINK","rgb(255,182,193)") -color.indexColor("PINK","rgb(255,192,203)") -color.indexColor("ANTIQUE_WHITE","rgb(250,235,215)") -color.indexColor("BEIGE","rgb(245,245,220)") -color.indexColor("BISQUE","rgb(255,228,196)") -color.indexColor("BLANCHED_ALMOND","rgb(255,235,205)") -color.indexColor("WHEAT","rgb(245,222,179)") -color.indexColor("CORN_SILK","rgb(255,248,220)") -color.indexColor("LEMON_CHIFFON","rgb(255,250,205)") -color.indexColor("LIGHT_GOLDEN_ROD_YELLOW","rgb(250,250,210)") -color.indexColor("LIGHT_YELLOW","rgb(255,255,224)") -color.indexColor("SADDLE_BROWN","rgb(139,69,19)") -color.indexColor("CALM_PURPLE","rgb(85,85,127)") -color.indexColor("SIENNA","rgb(160,82,45)") -color.indexColor("CHOCOLATE","rgb(210,105,30)") -color.indexColor("PERU","rgb(205,133,63)") -color.indexColor("SANDY_BROWN","rgb(244,164,96)") -color.indexColor("BURLY_WOOD","rgb(222,184,135)") -color.indexColor("TAN","rgb(210,180,140)") -color.indexColor("ROSY_BROWN","rgb(188,143,143)") -color.indexColor("MOCCASIN","rgb(255,228,181)") -color.indexColor("NAVAJO_WHITE","rgb(255,222,173)") -color.indexColor("PEACH_PUFF","rgb(255,218,185)") -color.indexColor("MISTY_ROSE","rgb(255,228,225)") -color.indexColor("LAVENDER_BLUSH","rgb(255,240,245)") -color.indexColor("LINEN","rgb(250,240,230)") -color.indexColor("OLD_LACE","rgb(253,245,230)") -color.indexColor("PAPAYA_WHIP","rgb(255,239,213)") -color.indexColor("SEA_SHELL","rgb(255,245,238)") -color.indexColor("MINT_CREAM","rgb(245,255,250)") -color.indexColor("SLATE_GRAY","rgb(112,128,144)") -color.indexColor("LIGHT_SLATE_GRAY","rgb(119,136,153)") -color.indexColor("LIGHT_STEEL_BLUE","rgb(176,196,222)") -color.indexColor("LAVENDEr(","rgb(230,230,250)") -color.indexColor("FLORAL_WHITE","rgb(255,250,240)") -color.indexColor("ALICE_BLUE","rgb(240,248,255)") -color.indexColor("GHOST_WHITE","rgb(248,248,255)") -color.indexColor("HONEYDEW","rgb(240,255,240)") -color.indexColor("IVORY","rgb(255,255,240)") -color.indexColor("AZURE","rgb(240,255,255)") -color.indexColor("SNOW","rgb(255,250,250)") -color.indexColor("DIM_GRAY","rgb(105,105,105)") -color.indexColor("GRAY","rgb(128,128,128)") -color.indexColor("DARK_GRAY","rgb(169,169,169)") -color.indexColor("SILVEr(","rgb(192,192,192)") -color.indexColor("LIGHT_GRAY","rgb(211,211,211)") -color.indexColor("GAINSBORO","rgb(220,220,220)") -color.indexColor("WHITE_SMOKE","rgb(245,245,245)") -color.indexColor("AliceBlue","#f0f8ff") -color.indexColor("AntiqueWhite","#faebd7") -color.indexColor("AntiqueWhite1","#ffefdb") -color.indexColor("AntiqueWhite2","#eedfcc") -color.indexColor("AntiqueWhite3","#cdc0b0") -color.indexColor("AntiqueWhite4","#8b8378") -color.indexColor("aquamarine1","#7fffd4") -color.indexColor("aquamarine2","#76eec6") -color.indexColor("aquamarine4","#458b74") -color.indexColor("azure1","#f0ffff") -color.indexColor("azure2","#e0eeee") -color.indexColor("azure3","#c1cdcd") -color.indexColor("azure4","#838b8b") -color.indexColor("beige","#f5f5dc") -color.indexColor("bisque1","#ffe4c4") -color.indexColor("bisque2","#eed5b7") -color.indexColor("bisque3","#cdb79e") -color.indexColor("bisque4","#8b7d6b") -color.indexColor("BlanchedAlmond","#ffebcd") -color.indexColor("blue1","#0000ff") -color.indexColor("blue2","#0000ee") -color.indexColor("blue4","#00008b") -color.indexColor("BlueViolet","#8a2be2") -color.indexColor("brown","#a52a2a") -color.indexColor("brown1","#ff4040") -color.indexColor("brown2","#ee3b3b") -color.indexColor("brown3","#cd3333") -color.indexColor("brown4","#8b2323") -color.indexColor("burlywood","#deb887") -color.indexColor("burlywood1","#ffd39b") -color.indexColor("burlywood2","#eec591") -color.indexColor("burlywood3","#cdaa7d") -color.indexColor("burlywood4","#8b7355") -color.indexColor("CadetBlue","#5f9ea0") -color.indexColor("CadetBlue1","#98f5ff") -color.indexColor("CadetBlue2","#8ee5ee") -color.indexColor("CadetBlue3","#7ac5cd") -color.indexColor("CadetBlue4","#53868b") -color.indexColor("chartreuse1","#7fff00") -color.indexColor("chartreuse2","#76ee00") -color.indexColor("chartreuse3","#66cd00") -color.indexColor("chartreuse4","#458b00") -color.indexColor("chocolate","#d2691e") -color.indexColor("chocolate1","#ff7f24") -color.indexColor("chocolate2","#ee7621") -color.indexColor("chocolate3","#cd661d") -color.indexColor("coral","#ff7f50") -color.indexColor("coral1","#ff7256") -color.indexColor("coral2","#ee6a50") -color.indexColor("coral3","#cd5b45") -color.indexColor("coral4","#8b3e2f") -color.indexColor("CornflowerBlue","#6495ed") -color.indexColor("cornsilk1","#fff8dc") -color.indexColor("cornsilk2","#eee8cd") -color.indexColor("cornsilk3","#cdc8b1") -color.indexColor("cornsilk4","#8b8878") -color.indexColor("cyan1","#00ffff") -color.indexColor("cyan2","#00eeee") -color.indexColor("cyan3","#00cdcd") -color.indexColor("cyan4","#008b8b") -color.indexColor("DarkGoldenrod","#b8860b") -color.indexColor("DarkGoldenrod1","#ffb90f") -color.indexColor("DarkGoldenrod2","#eead0e") -color.indexColor("DarkGoldenrod3","#cd950c") -color.indexColor("DarkGoldenrod4","#8b6508") -color.indexColor("DarkGreen","#006400") -color.indexColor("DarkKhaki","#bdb76b") -color.indexColor("DarkOliveGreen","#556b2f") -color.indexColor("DarkOliveGreen1","#caff70") -color.indexColor("DarkOliveGreen2","#bcee68") -color.indexColor("DarkOliveGreen3","#a2cd5a") -color.indexColor("DarkOliveGreen4","#6e8b3d") -color.indexColor("DarkOrange","#ff8c00") -color.indexColor("DarkOrange1","#ff7f00") -color.indexColor("DarkOrange2","#ee7600") -color.indexColor("DarkOrange3","#cd6600") -color.indexColor("DarkOrange4","#8b4500") -color.indexColor("DarkOrchid","#9932cc") -color.indexColor("DarkOrchid1","#bf3eff") -color.indexColor("DarkOrchid2","#b23aee") -color.indexColor("DarkOrchid3","#9a32cd") -color.indexColor("DarkOrchid4","#68228b") -color.indexColor("DarkSalmon","#e9967a") -color.indexColor("DarkSeaGreen","#8fbc8f") -color.indexColor("DarkSeaGreen1","#c1ffc1") -color.indexColor("DarkSeaGreen2","#b4eeb4") -color.indexColor("DarkSeaGreen3","#9bcd9b") -color.indexColor("DarkSeaGreen4","#698b69") -color.indexColor("DarkSlateBlue","#483d8b") -color.indexColor("DarkSlateGray","#2f4f4f") -color.indexColor("DarkSlateGray1","#97ffff") -color.indexColor("DarkSlateGray2","#8deeee") -color.indexColor("DarkSlateGray3","#79cdcd") -color.indexColor("DarkSlateGray4","#528b8b") -color.indexColor("DarkTurquoise","#00ced1") -color.indexColor("DarkViolet","#9400d3") -color.indexColor("DeepPink1","#ff1493") -color.indexColor("DeepPink2","#ee1289") -color.indexColor("DeepPink3","#cd1076") -color.indexColor("DeepPink4","#8b0a50") -color.indexColor("DeepSkyBlue1","#00bfff") -color.indexColor("DeepSkyBlue2","#00b2ee") -color.indexColor("DeepSkyBlue3","#009acd") -color.indexColor("DeepSkyBlue4","#00688b") -color.indexColor("DimGray","#696969") -color.indexColor("DodgerBlue1","#1e90ff") -color.indexColor("DodgerBlue2","#1c86ee") -color.indexColor("DodgerBlue3","#1874cd") -color.indexColor("DodgerBlue4","#104e8b") -color.indexColor("firebrick","#b22222") -color.indexColor("firebrick1","#ff3030") -color.indexColor("firebrick2","#ee2c2c") -color.indexColor("firebrick3","#cd2626") -color.indexColor("firebrick4","#8b1a1a") -color.indexColor("FloralWhite","#fffaf0") -color.indexColor("ForestGreen","#228b22") -color.indexColor("gainsboro","#dcdcdc") -color.indexColor("GhostWhite","#f8f8ff") -color.indexColor("gold1","#ffd700") -color.indexColor("gold2","#eec900") -color.indexColor("gold3","#cdad00") -color.indexColor("gold4","#8b7500") -color.indexColor("goldenrod","#daa520") -color.indexColor("goldenrod1","#ffc125") -color.indexColor("goldenrod2","#eeb422") -color.indexColor("goldenrod3","#cd9b1d") -color.indexColor("goldenrod4","#8b6914") -color.indexColor("gray","#bebebe") -color.indexColor("green1","#00ff00") -color.indexColor("green2","#00ee00") -color.indexColor("green3","#00cd00") -color.indexColor("green4","#008b00") -color.indexColor("GreenYellow","#adff2f") -color.indexColor("honeydew1","#f0fff0") -color.indexColor("honeydew2","#e0eee0") -color.indexColor("honeydew3","#c1cdc1") -color.indexColor("honeydew4","#838b83") -color.indexColor("HotPink","#ff69b4") -color.indexColor("HotPink1","#ff6eb4") -color.indexColor("HotPink2","#ee6aa7") -color.indexColor("HotPink3","#cd6090") -color.indexColor("HotPink4","#8b3a62") -color.indexColor("IndianRed","#cd5c5c") -color.indexColor("IndianRed1","#ff6a6a") -color.indexColor("IndianRed2","#ee6363") -color.indexColor("IndianRed3","#cd5555") -color.indexColor("IndianRed4","#8b3a3a") -color.indexColor("ivory1","#fffff0") -color.indexColor("ivory2","#eeeee0") -color.indexColor("ivory3","#cdcdc1") -color.indexColor("ivory4","#8b8b83") -color.indexColor("khaki","#f0e68c") -color.indexColor("khaki1","#fff68f") -color.indexColor("khaki2","#eee685") -color.indexColor("khaki3","#cdc673") -color.indexColor("khaki4","#8b864e") -color.indexColor("lavender","#e6e6fa") -color.indexColor("LavenderBlush1","#fff0f5") -color.indexColor("LavenderBlush2","#eee0e5") -color.indexColor("LavenderBlush3","#cdc1c5") -color.indexColor("LavenderBlush4","#8b8386") -color.indexColor("LawnGreen","#7cfc00") -color.indexColor("LemonChiffon1","#fffacd") -color.indexColor("LemonChiffon2","#eee9bf") -color.indexColor("LemonChiffon3","#cdc9a5") -color.indexColor("LemonChiffon4","#8b8970") -color.indexColor("light","#eedd82") -color.indexColor("LightBlue","#add8e6") -color.indexColor("LightBlue1","#bfefff") -color.indexColor("LightBlue2","#b2dfee") -color.indexColor("LightBlue3","#9ac0cd") -color.indexColor("LightBlue4","#68838b") -color.indexColor("LightCoral","#f08080") -color.indexColor("LightCyan1","#e0ffff") -color.indexColor("LightCyan2","#d1eeee") -color.indexColor("LightCyan3","#b4cdcd") -color.indexColor("LightCyan4","#7a8b8b") -color.indexColor("LightGoldenrod1","#ffec8b") -color.indexColor("LightGoldenrod2","#eedc82") -color.indexColor("LightGoldenrod3","#cdbe70") -color.indexColor("LightGoldenrod4","#8b814c") -color.indexColor("LightGoldenrodYellow","#fafad2") -color.indexColor("LightGray","#d3d3d3") -color.indexColor("LightPink","#ffb6c1") -color.indexColor("LightPink1","#ffaeb9") -color.indexColor("LightPink2","#eea2ad") -color.indexColor("LightPink3","#cd8c95") -color.indexColor("LightPink4","#8b5f65") -color.indexColor("LightSalmon1","#ffa07a") -color.indexColor("LightSalmon2","#ee9572") -color.indexColor("LightSalmon3","#cd8162") -color.indexColor("LightSalmon4","#8b5742") -color.indexColor("LightSeaGreen","#20b2aa") -color.indexColor("LightSkyBlue","#87cefa") -color.indexColor("LightSkyBlue1","#b0e2ff") -color.indexColor("LightSkyBlue2","#a4d3ee") -color.indexColor("LightSkyBlue3","#8db6cd") -color.indexColor("LightSkyBlue4","#607b8b") -color.indexColor("LightSlateBlue","#8470ff") -color.indexColor("LightSlateGray","#778899") -color.indexColor("LightSteelBlue","#b0c4de") -color.indexColor("LightSteelBlue1","#cae1ff") -color.indexColor("LightSteelBlue2","#bcd2ee") -color.indexColor("LightSteelBlue3","#a2b5cd") -color.indexColor("LightSteelBlue4","#6e7b8b") -color.indexColor("LightYellow1","#ffffe0") -color.indexColor("LightYellow2","#eeeed1") -color.indexColor("LightYellow3","#cdcdb4") -color.indexColor("LightYellow4","#8b8b7a") -color.indexColor("LimeGreen","#32cd32") -color.indexColor("linen","#faf0e6") -color.indexColor("magenta","#ff00ff") -color.indexColor("magenta2","#ee00ee") -color.indexColor("magenta3","#cd00cd") -color.indexColor("magenta4","#8b008b") -color.indexColor("maroon","#b03060") -color.indexColor("maroon1","#ff34b3") -color.indexColor("maroon2","#ee30a7") -color.indexColor("maroon3","#cd2990") -color.indexColor("maroon4","#8b1c62") -color.indexColor("medium","#66cdaa") -color.indexColor("MediumAquamarine","#66cdaa") -color.indexColor("MediumBlue","#0000cd") -color.indexColor("MediumOrchid","#ba55d3") -color.indexColor("MediumOrchid1","#e066ff") -color.indexColor("MediumOrchid2","#d15fee") -color.indexColor("MediumOrchid3","#b452cd") -color.indexColor("MediumOrchid4","#7a378b") -color.indexColor("MediumPurple","#9370db") -color.indexColor("MediumPurple1","#ab82ff") -color.indexColor("MediumPurple2","#9f79ee") -color.indexColor("MediumPurple3","#8968cd") -color.indexColor("MediumPurple4","#5d478b") -color.indexColor("MediumSeaGreen","#3cb371") -color.indexColor("MediumSlateBlue","#7b68ee") -color.indexColor("MediumSpringGreen","#00fa9a") -color.indexColor("MediumTurquoise","#48d1cc") -color.indexColor("MediumVioletRed","#c71585") -color.indexColor("MidnightBlue","#191970") -color.indexColor("MintCream","#f5fffa") -color.indexColor("MistyRose1","#ffe4e1") -color.indexColor("MistyRose2","#eed5d2") -color.indexColor("MistyRose3","#cdb7b5") -color.indexColor("MistyRose4","#8b7d7b") -color.indexColor("moccasin","#ffe4b5") -color.indexColor("NavajoWhite1","#ffdead") -color.indexColor("NavajoWhite2","#eecfa1") -color.indexColor("NavajoWhite3","#cdb38b") -color.indexColor("NavajoWhite4","#8b795e") -color.indexColor("NavyBlue","#000080") -color.indexColor("OldLace","#fdf5e6") -color.indexColor("OliveDrab","#6b8e23") -color.indexColor("OliveDrab1","#c0ff3e") -color.indexColor("OliveDrab2","#b3ee3a") -color.indexColor("OliveDrab4","#698b22") -color.indexColor("orange1","#ffa500") -color.indexColor("orange2","#ee9a00") -color.indexColor("orange3","#cd8500") -color.indexColor("orange4","#8b5a00") -color.indexColor("OrangeRed1","#ff4500") -color.indexColor("OrangeRed2","#ee4000") -color.indexColor("OrangeRed3","#cd3700") -color.indexColor("OrangeRed4","#8b2500") -color.indexColor("orchid","#da70d6") -color.indexColor("orchid1","#ff83fa") -color.indexColor("orchid2","#ee7ae9") -color.indexColor("orchid3","#cd69c9") -color.indexColor("orchid4","#8b4789") -color.indexColor("pale","#db7093") -color.indexColor("PaleGoldenrod","#eee8aa") -color.indexColor("PaleGreen","#98fb98") -color.indexColor("PaleGreen1","#9aff9a") -color.indexColor("PaleGreen2","#90ee90") -color.indexColor("PaleGreen3","#7ccd7c") -color.indexColor("PaleGreen4","#548b54") -color.indexColor("PaleTurquoise","#afeeee") -color.indexColor("PaleTurquoise1","#bbffff") -color.indexColor("PaleTurquoise2","#aeeeee") -color.indexColor("PaleTurquoise3","#96cdcd") -color.indexColor("PaleTurquoise4","#668b8b") -color.indexColor("PaleVioletRed","#db7093") -color.indexColor("PaleVioletRed1","#ff82ab") -color.indexColor("PaleVioletRed2","#ee799f") -color.indexColor("PaleVioletRed3","#cd6889") -color.indexColor("PaleVioletRed4","#8b475d") -color.indexColor("PapayaWhip","#ffefd5") -color.indexColor("PeachPuff1","#ffdab9") -color.indexColor("PeachPuff2","#eecbad") -color.indexColor("PeachPuff3","#cdaf95") -color.indexColor("PeachPuff4","#8b7765") -color.indexColor("pink","#ffc0cb") -color.indexColor("pink1","#ffb5c5") -color.indexColor("pink2","#eea9b8") -color.indexColor("pink3","#cd919e") -color.indexColor("pink4","#8b636c") -color.indexColor("plum","#dda0dd") -color.indexColor("plum1","#ffbbff") -color.indexColor("plum2","#eeaeee") -color.indexColor("plum3","#cd96cd") -color.indexColor("plum4","#8b668b") -color.indexColor("PowderBlue","#b0e0e6") -color.indexColor("purple","#a020f0") -color.indexColor("purple1","#9b30ff") -color.indexColor("purple2","#912cee") -color.indexColor("purple3","#7d26cd") -color.indexColor("purple4","#551a8b") -color.indexColor("red1","#ff0000") -color.indexColor("red2","#ee0000") -color.indexColor("red3","#cd0000") -color.indexColor("red4","#8b0000") -color.indexColor("RosyBrown","#bc8f8f") -color.indexColor("RosyBrown1","#ffc1c1") -color.indexColor("RosyBrown2","#eeb4b4") -color.indexColor("RosyBrown3","#cd9b9b") -color.indexColor("RosyBrown4","#8b6969") -color.indexColor("RoyalBlue","#4169e1") -color.indexColor("RoyalBlue1","#4876ff") -color.indexColor("RoyalBlue2","#436eee") -color.indexColor("RoyalBlue3","#3a5fcd") -color.indexColor("RoyalBlue4","#27408b") -color.indexColor("SaddleBrown","#8b4513") -color.indexColor("salmon","#fa8072") -color.indexColor("salmon1","#ff8c69") -color.indexColor("salmon2","#ee8262") -color.indexColor("salmon3","#cd7054") -color.indexColor("salmon4","#8b4c39") -color.indexColor("SandyBrown","#f4a460") -color.indexColor("SeaGreen1","#54ff9f") -color.indexColor("SeaGreen2","#4eee94") -color.indexColor("SeaGreen3","#43cd80") -color.indexColor("SeaGreen4","#2e8b57") -color.indexColor("seashell1","#fff5ee") -color.indexColor("seashell2","#eee5de") -color.indexColor("seashell3","#cdc5bf") -color.indexColor("seashell4","#8b8682") -color.indexColor("sienna","#a0522d") -color.indexColor("sienna1","#ff8247") -color.indexColor("sienna2","#ee7942") -color.indexColor("sienna3","#cd6839") -color.indexColor("sienna4","#8b4726") -color.indexColor("SkyBlue","#87ceeb") -color.indexColor("SkyBlue1","#87ceff") -color.indexColor("SkyBlue2","#7ec0ee") -color.indexColor("SkyBlue3","#6ca6cd") -color.indexColor("SkyBlue4","#4a708b") -color.indexColor("SlateBlue","#6a5acd") -color.indexColor("SlateBlue1","#836fff") -color.indexColor("SlateBlue2","#7a67ee") -color.indexColor("SlateBlue3","#6959cd") -color.indexColor("SlateBlue4","#473c8b") -color.indexColor("SlateGray","#708090") -color.indexColor("SlateGray1","#c6e2ff") -color.indexColor("SlateGray2","#b9d3ee") -color.indexColor("SlateGray3","#9fb6cd") -color.indexColor("SlateGray4","#6c7b8b") -color.indexColor("snow1","#fffafa") -color.indexColor("snow2","#eee9e9") -color.indexColor("snow3","#cdc9c9") -color.indexColor("snow4","#8b8989") -color.indexColor("SpringGreen1","#00ff7f") -color.indexColor("SpringGreen2","#00ee76") -color.indexColor("SpringGreen3","#00cd66") -color.indexColor("SpringGreen4","#008b45") -color.indexColor("SteelBlue","#4682b4") -color.indexColor("SteelBlue1","#63b8ff") -color.indexColor("SteelBlue2","#5cacee") -color.indexColor("SteelBlue3","#4f94cd") -color.indexColor("SteelBlue4","#36648b") -color.indexColor("tan","#d2b48c") -color.indexColor("tan1","#ffa54f") -color.indexColor("tan2","#ee9a49") -color.indexColor("tan3","#cd853f") -color.indexColor("tan4","#8b5a2b") -color.indexColor("thistle","#d8bfd8") -color.indexColor("thistle1","#ffe1ff") -color.indexColor("thistle2","#eed2ee") -color.indexColor("thistle3","#cdb5cd") -color.indexColor("thistle4","#8b7b8b") -color.indexColor("tomato1","#ff6347") -color.indexColor("tomato2","#ee5c42") -color.indexColor("tomato3","#cd4f39") -color.indexColor("tomato4","#8b3626") -color.indexColor("turquoise","#40e0d0") -color.indexColor("turquoise1","#00f5ff") -color.indexColor("turquoise2","#00e5ee") -color.indexColor("turquoise3","#00c5cd") -color.indexColor("turquoise4","#00868b") -color.indexColor("violet","#ee82ee") -color.indexColor("VioletRed","#d02090") -color.indexColor("VioletRed1","#ff3e96") -color.indexColor("VioletRed2","#ee3a8c") -color.indexColor("VioletRed3","#cd3278") -color.indexColor("VioletRed4","#8b2252") -color.indexColor("wheat","#f5deb3") -color.indexColor("wheat1","#ffe7ba") -color.indexColor("wheat2","#eed8ae") -color.indexColor("wheat3","#cdba96") -color.indexColor("wheat4","#8b7e66") -color.indexColor("WhiteSmoke","#f5f5f5") -color.indexColor("yellow1","#ffff00") -color.indexColor("yellow2","#eeee00") -color.indexColor("yellow3","#cdcd00") -color.indexColor("yellow4","#8b8b00") -color.indexColor("YellowGreen","#9acd32") -color.indexColor("purple","#7e1e9c") -color.indexColor("green","#12a217") -color.indexColor("blue","#0343df") -color.indexColor("pink","#ff81c0") -color.indexColor("brown","#653700") -color.indexColor("red","#e50000") -color.indexColor("light_blue","#95d0fc") -color.indexColor("teal","#029386") -color.indexColor("orange","#f97306") -color.indexColor("light_green","#96f97b") -color.indexColor("magenta","#c20078") -color.indexColor("yellow","#ffff14") -color.indexColor("sky_blue","#75bbfd") -color.indexColor("grey","#929591") -color.indexColor("lime_green","#89fe05") -color.indexColor("light_purple","#bf77f6") -color.indexColor("violet","#9a0eea") -color.indexColor("dark_green","#033500") -color.indexColor("turquoise","#06c2ac") -color.indexColor("lavender","#c79fef") -color.indexColor("dark_blue","#00035b") -color.indexColor("tan","#d1b26f") -color.indexColor("cyan","#00ffff") -color.indexColor("aqua","#13eac9") -color.indexColor("forest_green","#06470c") -color.indexColor("mauve","#ae7181") -color.indexColor("dark_purple","#35063e") -color.indexColor("bright_green","#01ff07") -color.indexColor("maroon","#650021") -color.indexColor("olive","#6e750e") -color.indexColor("salmon","#ff796c") -color.indexColor("beige","#e6daa6") -color.indexColor("royal_blue","#0504aa") -color.indexColor("navy_blue","#001146") -color.indexColor("lilac","#cea2fd") -color.indexColor("black","#000000") -color.indexColor("hot_pink","#ff028d") -color.indexColor("light_brown","#ad8150") -color.indexColor("pale_green","#c7fdb5") -color.indexColor("peach","#ffb07c") -color.indexColor("olive_green","#677a04") -color.indexColor("dark_pink","#cb416b") -color.indexColor("periwinkle","#8e82fe") -color.indexColor("sea_green","#53fca1") -color.indexColor("lime","#aaff32") -color.indexColor("indigo","#380282") -color.indexColor("mustard","#ceb301") -color.indexColor("light_pink","#ffd1df") -color.indexColor("rose","#cf6275") -color.indexColor("bright_blue","#0165fc") -color.indexColor("neon_green","#0cff0c") -color.indexColor("burnt_orange","#c04e01") -color.indexColor("aquamarine","#04d8b2") -color.indexColor("navy","#01153e") -color.indexColor("grass_green","#3f9b0b") -color.indexColor("pale_blue","#d0fefe") -color.indexColor("dark_red","#840000") -color.indexColor("bright_purple","#be03fd") -color.indexColor("yellow_green","#c0fb2d") -color.indexColor("baby_blue","#a2cffe") -color.indexColor("gold","#dbb40c") -color.indexColor("mint_green","#8fff9f") -color.indexColor("plum","#580f41") -color.indexColor("royal_purple","#4b006e") -color.indexColor("brick_red","#8f1402") -color.indexColor("dark_teal","#014d4e") -color.indexColor("burgundy","#610023") -color.indexColor("khaki","#aaa662") -color.indexColor("blue_green","#137e6d") -color.indexColor("seafoam_green","#7af9ab") -color.indexColor("kelly_green","#02ab2e") -color.indexColor("puke_green","#9aae07") -color.indexColor("pea_green","#8eab12") -color.indexColor("taupe","#b9a281") -color.indexColor("dark_brown","#341c02") -color.indexColor("deep_purple","#36013f") -color.indexColor("chartreuse","#c1f80a") -color.indexColor("bright_pink","#fe01b1") -color.indexColor("light_orange","#fdaa48") -color.indexColor("mint","#9ffeb0") -color.indexColor("pastel_green","#b0ff9d") -color.indexColor("sand","#e2ca76") -color.indexColor("dark_orange","#c65102") -color.indexColor("spring_green","#a9f971") -color.indexColor("puce","#a57e52") -color.indexColor("seafoam","#80f9ad") -color.indexColor("grey_blue","#6b8ba4") -color.indexColor("army_green","#4b5d16") -color.indexColor("dark_grey","#363737") -color.indexColor("dark_yellow","#d5b60a") -color.indexColor("goldenrod","#fac205") -color.indexColor("slate","#516572") -color.indexColor("light_teal","#90e4c1") -color.indexColor("rust","#a83c09") -color.indexColor("deep_blue","#040273") -color.indexColor("pale_pink","#ffcfdc") -color.indexColor("cerulean","#0485d1") -color.indexColor("light_red","#ff474c") -color.indexColor("mustard_yellow","#d2bd0a") -color.indexColor("ochre","#bf9005") -color.indexColor("pale_yellow","#ffff84") -color.indexColor("crimson","#8c000f") -color.indexColor("fuchsia","#ed0dd9") -color.indexColor("hunter_green","#0b4008") -color.indexColor("blue_grey","#607c8e") -color.indexColor("slate_blue","#5b7c99") -color.indexColor("pale_purple","#b790d4") -color.indexColor("sea_blue","#047495") -color.indexColor("pinkish_purple","#d648d7") -color.indexColor("puke","#a5a502") -color.indexColor("light_grey","#d8dcd6") -color.indexColor("leaf_green","#5ca904") -color.indexColor("light_yellow","#fffe7a") -color.indexColor("eggplant","#380835") -color.indexColor("steel_blue","#5a7d9a") -color.indexColor("moss_green","#658b38") -color.indexColor("robin's_egg_blue","#98eff9") -color.indexColor("white","#ffffff") -color.indexColor("grey_green","#789b73") -color.indexColor("sage","#87ae73") -color.indexColor("brick","#a03623") -color.indexColor("burnt_sienna","#b04e0f") -color.indexColor("reddish_brown","#7f2b0a") -color.indexColor("cream","#ffffc2") -color.indexColor("coral","#fc5a50") -color.indexColor("ocean_blue","#03719c") -color.indexColor("greenish","#40a368") -color.indexColor("dark_magenta","#960056") -color.indexColor("red_orange","#fd3c06") -color.indexColor("bluish_purple","#703be7") -color.indexColor("midnight_blue","#020035") -color.indexColor("light_violet","#d6b4fc") -color.indexColor("dusty_rose","#c0737a") -color.indexColor("medium_blue","#2c6fbb") -color.indexColor("greenish_yellow","#cdfd02") -color.indexColor("yellowish_green","#b0dd16") -color.indexColor("purplish_blue","#601ef9") -color.indexColor("greyish_blue","#5e819d") -color.indexColor("grape","#6c3461") -color.indexColor("light_olive","#acbf69") -color.indexColor("cornflower_blue","#5170d7") -color.indexColor("pinkish_red","#f10c45") -color.indexColor("bright_red","#ff000d") -color.indexColor("azure","#069af3") -color.indexColor("blue_purple","#5729ce") -color.indexColor("dark_turquoise","#045c5a") -color.indexColor("electric_blue","#0652ff") -color.indexColor("off_white","#ffffe4") -color.indexColor("powder_blue","#b1d1fc") -color.indexColor("wine","#80013f") -color.indexColor("dull_green","#74a662") -color.indexColor("apple_green","#76cd26") -color.indexColor("light_turquoise","#7ef4cc") -color.indexColor("neon_purple","#bc13fe") -color.indexColor("cobalt","#1e488f") -color.indexColor("pinkish","#d46a7e") -color.indexColor("olive_drab","#6f7632") -color.indexColor("dark_cyan","#0a888a") -color.indexColor("purple_blue","#632de9") -color.indexColor("dark_violet","#34013f") -color.indexColor("dark_lavender","#856798") -color.indexColor("forrest_green","#154406") -color.indexColor("vomit","#a2a415") -color.indexColor("pale_orange","#ffa756") -color.indexColor("greenish_blue","#0b8b87") -color.indexColor("dark_tan","#af884a") -color.indexColor("green_blue","#06b48b") -color.indexColor("bluish_green","#10a674") -color.indexColor("pastel_blue","#a2bffe") -color.indexColor("moss","#769958") -color.indexColor("grass","#5cac2d") -color.indexColor("deep_pink","#cb0162") -color.indexColor("blood_red","#980002") -color.indexColor("sage_green","#88b378") -color.indexColor("aqua_blue","#02d8e9") -color.indexColor("terracotta","#ca6641") -color.indexColor("pastel_purple","#caa0ff") -color.indexColor("sienna","#a9561e") -color.indexColor("dark_olive","#373e02") -color.indexColor("green_yellow","#c9ff27") -color.indexColor("scarlet","#be0119") -color.indexColor("greyish_green","#82a67d") -color.indexColor("chocolate","#3d1c02") -color.indexColor("blue_violet","#5d06e9") -color.indexColor("cornflower","#6a79f7") -color.indexColor("baby_pink","#ffb7ce") -color.indexColor("charcoal","#343837") -color.indexColor("pine_green","#0a481e") -color.indexColor("pumpkin","#e17701") -color.indexColor("greenish_brown","#696112") -color.indexColor("red_brown","#8b2e16") -color.indexColor("brownish_green","#6a6e09") -color.indexColor("tangerine","#ff9408") -color.indexColor("salmon_pink","#fe7b7c") -color.indexColor("aqua_green","#12e193") -color.indexColor("raspberry","#b00149") -color.indexColor("greyish_purple","#887191") -color.indexColor("rose_pink","#f7879a") -color.indexColor("neon_pink","#fe019a") -color.indexColor("cobalt_blue","#030aa7") -color.indexColor("orange_brown","#be6400") -color.indexColor("deep_red","#9a0200") -color.indexColor("orange_red","#fd411e") -color.indexColor("dirty_yellow","#cdc50a") -color.indexColor("orchid","#c875c4") -color.indexColor("reddish_pink","#fe2c54") -color.indexColor("reddish_purple","#910951") -color.indexColor("yellow_orange","#fcb001") -color.indexColor("light_cyan","#acfffc") -color.indexColor("sky","#82cafc") -color.indexColor("light_magenta","#fa5ff7") -color.indexColor("pale_red","#d9544d") -color.indexColor("emerald","#01a049") -color.indexColor("dark_beige","#ac9362") -color.indexColor("ugly_green","#7a9703") -color.indexColor("jade","#1fa774") -color.indexColor("greenish_grey","#96ae8d") -color.indexColor("dark_salmon","#c85a53") -color.indexColor("purplish_pink","#ce5dae") -color.indexColor("dark_aqua","#05696b") -color.indexColor("brownish_orange","#cb7723") -color.indexColor("light_olive_green","#a4be5c") -color.indexColor("light_aqua","#8cffdb") -color.indexColor("clay","#b66a50") -color.indexColor("medium_green","#39ad48") -color.indexColor("burnt_umber","#a0450e") -color.indexColor("dull_blue","#49759c") -color.indexColor("pale_brown","#b1916e") -color.indexColor("emerald_green","#028f1e") -color.indexColor("brownish","#9c6d57") -color.indexColor("mud","#735c12") -color.indexColor("dark_rose","#b5485d") -color.indexColor("brownish_red","#9e3623") -color.indexColor("pink_purple","#db4bda") -color.indexColor("pinky_purple","#c94cbe") -color.indexColor("camo_green","#526525") -color.indexColor("faded_green","#7bb274") -color.indexColor("dusty_pink","#d58a94") -color.indexColor("purple_pink","#e03fd8") -color.indexColor("vomit_green","#89a203") -color.indexColor("deep_green","#02590f") -color.indexColor("reddish_orange","#f8481c") -color.indexColor("mahogany","#4a0100") -color.indexColor("aubergine","#3d0734") -color.indexColor("dull_pink","#d5869d") -color.indexColor("evergreen","#05472a") -color.indexColor("dark_sky_blue","#448ee4") -color.indexColor("very_light_green","#d1ffbd") -color.indexColor("pastel_pink","#ffbacd") -color.indexColor("grey_purple","#826d8c") -color.indexColor("very_light_blue","#d5ffff") -color.indexColor("dark_mauve","#874c62") -color.indexColor("cadet_blue","#4e7496") -color.indexColor("ice_blue","#d7fffe") -color.indexColor("light_tan","#fbeeac") -color.indexColor("dirty_green","#667e2c") -color.indexColor("neon_blue","#04d9ff") -color.indexColor("wine_red","#7b0323") -color.indexColor("chocolate_brown","#411900") -color.indexColor("dull_purple","#84597e") -color.indexColor("yellow_brown","#b79400") -color.indexColor("denim","#3b638c") -color.indexColor("eggshell","#ffffd4") -color.indexColor("jungle_green","#048243") -color.indexColor("dark_peach","#de7e5d") -color.indexColor("poop","#7f5e00") -color.indexColor("umber","#b26400") -color.indexColor("light_lavender","#dfc5fe") -color.indexColor("bright_yellow","#fffd01") -color.indexColor("golden_yellow","#fec615") -color.indexColor("dusty_blue","#5a86ad") -color.indexColor("electric_green","#21fc0d") -color.indexColor("lighter_green","#75fd63") -color.indexColor("slate_grey","#59656d") -color.indexColor("teal_green","#25a36f") -color.indexColor("marine_blue","#01386a") -color.indexColor("avocado","#90b134") -color.indexColor("terra_cotta","#c9643b") -color.indexColor("dusty_purple","#825f87") -color.indexColor("light_maroon","#a24857") -color.indexColor("reddish","#c44240") -color.indexColor("dark_lilac","#9c6da5") -color.indexColor("dark_periwinkle","#665fd1") -color.indexColor("bluish_grey","#748b97") -color.indexColor("puke_yellow","#c2be0e") -color.indexColor("purplish","#94568c") -color.indexColor("ultramarine","#2000b1") -color.indexColor("barney_purple","#a00498") -color.indexColor("forest","#0b5509") -color.indexColor("pea_soup","#929901") -color.indexColor("brownish_yellow","#c9b003") -color.indexColor("bright_teal","#01f9c6") -color.indexColor("bluegreen","#017a79") -color.indexColor("green_brown","#544e03") -color.indexColor("blurple","#5539cc") -color.indexColor("light_sky_blue","#c6fcff") -color.indexColor("periwinkle_blue","#8f99fb") -color.indexColor("pale_violet","#ceaefa") -color.indexColor("darker_green","#087804") -color.indexColor("true_blue","#010fcc") -color.indexColor("green_grey","#77926f") -color.indexColor("grey_brown","#7f7053") -color.indexColor("dark_olive_green","#3c4d03") -color.indexColor("apricot","#ffb16d") -color.indexColor("faded_purple","#916e99") -color.indexColor("darker_blue","#011288") -color.indexColor("cerise","#de0c62") -color.indexColor("khaki_green","#728639") -color.indexColor("burnt_red","#9f2305") -color.indexColor("light_forest_green","#4f9153") -color.indexColor("violet_blue","#510ac9") -color.indexColor("pale_lavender","#eecffe") -color.indexColor("acid_green","#8ffe09") -color.indexColor("purple_grey","#866f85") -color.indexColor("lemon","#fdff52") -color.indexColor("bright_orange","#ff5b00") -color.indexColor("soft_green","#6fc276") -color.indexColor("blush","#f29e8e") -color.indexColor("yellowish_brown","#9b7a01") -color.indexColor("fluorescent_green","#08ff08") -color.indexColor("electric_purple","#aa23ff") -color.indexColor("steel","#738595") -color.indexColor("dull_orange","#d8863b") -color.indexColor("muddy_green","#657432") -color.indexColor("marigold","#fcc006") -color.indexColor("ocean","#017b92") -color.indexColor("light_mauve","#c292a1") -color.indexColor("bordeaux","#7b002c") -color.indexColor("light_blue_green","#7efbb3") -color.indexColor("yellowish","#faee66") -color.indexColor("snot_green","#9dc100") -color.indexColor("light_lime_green","#b9ff66") -color.indexColor("drab_green","#749551") -color.indexColor("faded_blue","#658cbb") -color.indexColor("dark_forest_green","#002d04") -color.indexColor("hot_purple","#cb00f5") -color.indexColor("dark_maroon","#3c0008") -color.indexColor("brown_green","#706c11") -color.indexColor("swamp_green","#748500") -color.indexColor("light_indigo","#6d5acf") -color.indexColor("purpley_blue","#5f34e7") -color.indexColor("lightish_blue","#3d7afd") -color.indexColor("teal_blue","#01889f") -color.indexColor("denim_blue","#3b5b92") -color.indexColor("dark_lime_green","#7ebd01") -color.indexColor("dull_yellow","#eedc5b") -color.indexColor("pistachio","#c0fa8b") -color.indexColor("lemon_yellow","#fdff38") -color.indexColor("red_violet","#9e0168") -color.indexColor("dusky_pink","#cc7a8b") -color.indexColor("dirt","#8a6e45") -color.indexColor("very_dark_green","#062e03") -color.indexColor("medium_purple","#9e43a2") -color.indexColor("shit","#7f5f00") -color.indexColor("dark_mustard","#a88905") -color.indexColor("pea_soup_green","#94a617") -color.indexColor("bubblegum_pink","#fe83cc") -color.indexColor("barbie_pink","#fe46a5") -color.indexColor("military_green","#667c3e") -color.indexColor("pale_teal","#82cbb2") -color.indexColor("bronze","#a87900") -color.indexColor("pinky_red","#fc2647") -color.indexColor("dull_red","#bb3f3f") -color.indexColor("darkish_blue","#014182") -color.indexColor("bluish","#2976bb") -color.indexColor("dark_gold","#b59410") -color.indexColor("yellowy_green","#bff128") -color.indexColor("pine","#2b5d34") -color.indexColor("dark_blue_green","#005249") -color.indexColor("dirty_pink","#ca7b80") -color.indexColor("slate_green","#658d6d") -color.indexColor("prussian_blue","#004577") -color.indexColor("bright_violet","#ad0afd") -color.indexColor("lighter_purple","#a55af4") -color.indexColor("steel_grey","#6f828a") -color.indexColor("russet","#a13905") -color.indexColor("vermillion","#f4320c") -color.indexColor("greyish_brown","#7a6a4f") -color.indexColor("red_purple","#820747") -color.indexColor("red_pink","#fa2a55") -color.indexColor("bright_turquoise","#0ffef9") -color.indexColor("golden_brown","#b27a01") -color.indexColor("cerulean_blue","#056eee") -color.indexColor("soft_blue","#6488ea") -color.indexColor("easter_green","#8cfd7e") -color.indexColor("amber","#feb308") -color.indexColor("mid_blue","#276ab3") -color.indexColor("shit_brown","#7b5804") -color.indexColor("hospital_green","#9be5aa") -color.indexColor("purpleish_blue","#6140ef") -color.indexColor("purply_blue","#661aee") -color.indexColor("silver","#c5c9c7") -color.indexColor("sickly_green","#94b21c") -color.indexColor("melon","#ff7855") -color.indexColor("dusky_rose","#ba6873") -color.indexColor("brown_orange","#b96902") -color.indexColor("darkish_green","#287c37") -color.indexColor("cranberry","#9e003a") -color.indexColor("purpleish","#98568d") -color.indexColor("ecru","#feffca") -color.indexColor("darker_purple","#5f1b6b") -color.indexColor("mocha","#9d7651") -color.indexColor("bright_magenta","#ff08e8") -color.indexColor("coffee","#a6814c") -color.indexColor("sepia","#985e2b") -color.indexColor("faded_red","#d3494e") -color.indexColor("canary_yellow","#fffe40") -color.indexColor("bluey_purple","#6241c7") -color.indexColor("pastel_yellow","#fffe71") -color.indexColor("pale_turquoise","#a5fbd5") -color.indexColor("greyish_pink","#c88d94") -color.indexColor("marine","#042e60") -color.indexColor("purplish_grey","#7a687f") -color.indexColor("camel","#c69f59") -color.indexColor("brownish_grey","#86775f") -color.indexColor("burnt_yellow","#d5ab09") -color.indexColor("cherry_red","#f7022a") -color.indexColor("orangey_brown","#b16002") -color.indexColor("soft_pink","#fdb0c0") -color.indexColor("dark_sea_green","#11875d") -color.indexColor("aqua_marine","#2ee8bb") -color.indexColor("robin_egg_blue","#8af1fe") -color.indexColor("light_sea_green","#98f6b0") -color.indexColor("mud_brown","#60460f") -color.indexColor("sandstone","#c9ae74") -color.indexColor("british_racing_green","#05480d") -color.indexColor("faded_pink","#de9dac") -color.indexColor("maize","#f4d054") -color.indexColor("ocre","#c69c04") -color.indexColor("orange_yellow","#ffad01") -color.indexColor("dark_khaki","#9b8f55") -color.indexColor("light_lime","#aefd6c") -color.indexColor("bright_light_blue","#26f7fd") -color.indexColor("jade_green","#2baf6a") -color.indexColor("barney","#ac1db8") -color.indexColor("adobe","#bd6c48") -color.indexColor("minty_green","#0bf77d") -color.indexColor("light_navy_blue","#2e5a88") -color.indexColor("dusty_green","#76a973") -color.indexColor("very_dark_blue","#000133") -color.indexColor("ocean_green","#3d9973") -color.indexColor("mustard_green","#a8b504") -color.indexColor("poop_brown","#7a5901") -color.indexColor("olive_brown","#645403") -color.indexColor("pink_red","#f5054f") -color.indexColor("light_navy","#155084") -color.indexColor("very_light_purple","#f6cefc") -color.indexColor("ivory","#ffffcb") -color.indexColor("bright_lavender","#c760ff") -color.indexColor("bright_aqua","#0bf9ea") -color.indexColor("robin's_egg","#6dedfd") -color.indexColor("muted_green","#5fa052") -color.indexColor("medium_brown","#7f5112") -color.indexColor("copper","#b66325") -color.indexColor("dark_lime","#84b701") -color.indexColor("strawberry","#fb2943") -color.indexColor("dirt_brown","#836539") -color.indexColor("celery","#c1fd95") -color.indexColor("bright_sky_blue","#02ccfe") -color.indexColor("poo_brown","#885f01") -color.indexColor("pinkish_brown","#b17261") -color.indexColor("celadon","#befdb7") -color.indexColor("bright_lime_green","#65fe08") -color.indexColor("auburn","#9a3001") -color.indexColor("shocking_pink","#fe02a2") -color.indexColor("mulberry","#920a4e") -color.indexColor("carolina_blue","#8ab8fe") -color.indexColor("lightish_green","#61e160") -color.indexColor("light_lilac","#edc8ff") -color.indexColor("pale_olive","#b9cc81") -color.indexColor("pumpkin_orange","#fb7d07") -color.indexColor("yellow_ochre","#cb9d06") -color.indexColor("fire_engine_red","#fe0002") -color.indexColor("deep_sky_blue","#0d75f8") -color.indexColor("watermelon","#fd4659") -color.indexColor("bottle_green","#044a05") -color.indexColor("very_dark_purple","#2a0134") -color.indexColor("wheat","#fbdd7e") -color.indexColor("murky_green","#6c7a0e") -color.indexColor("brownish_purple","#76424e") -color.indexColor("kermit_green","#5cb200") -color.indexColor("primary_blue","#0804f9") -color.indexColor("orangey_red","#fa4224") -color.indexColor("pale_lilac","#e4cbff") -color.indexColor("rust_red","#aa2704") -color.indexColor("dirty_orange","#c87606") -color.indexColor("pinkish_grey","#c8aca9") -color.indexColor("light_plum","#9d5783") -color.indexColor("greeny_blue","#42b395") -color.indexColor("dark_navy","#000435") -color.indexColor("pink/purple","#ef1de7") -color.indexColor("irish_green","#019529") -color.indexColor("baby_poop","#937c00") -color.indexColor("slime_green","#99cc04") -color.indexColor("purplish_red","#b0054b") -color.indexColor("rouge","#ab1239") -color.indexColor("light_rose","#ffc5cb") -color.indexColor("drab","#828344") -color.indexColor("dark_navy_blue","#00022e") -color.indexColor("light_yellow_green","#ccfd7f") -color.indexColor("easter_purple","#c071fe") -color.indexColor("snot","#acbb0d") -color.indexColor("light_salmon","#fea993") -color.indexColor("purpley_pink","#c83cb9") -color.indexColor("poo","#8f7303") -color.indexColor("berry","#990f4b") -color.indexColor("medium_grey","#7d7f7c") -color.indexColor("brown_red","#922b05") -color.indexColor("blood","#770001") -color.indexColor("soft_purple","#a66fb5") -color.indexColor("grey_pink","#c3909b") -color.indexColor("bluey_green","#2bb179") -color.indexColor("midnight","#03012d") -color.indexColor("dark_indigo","#1f0954") -color.indexColor("warm_grey","#978a84") -color.indexColor("sandy_brown","#c4a661") -color.indexColor("cherry","#cf0234") -color.indexColor("blue/purple","#5a06ef") -color.indexColor("gunmetal","#536267") -color.indexColor("deep_violet","#490648") -color.indexColor("tree_green","#2a7e19") -color.indexColor("orangish_brown","#b25f03") -color.indexColor("shamrock_green","#02c14d") -color.indexColor("orangish_red","#f43605") -color.indexColor("greeny_yellow","#c6f808") -color.indexColor("ugly_yellow","#d0c101") -color.indexColor("french_blue","#436bad") -color.indexColor("dusky_purple","#895b7b") -color.indexColor("butter_yellow","#fffd74") -color.indexColor("light_beige","#fffeb6") -color.indexColor("golden","#f5bf03") -color.indexColor("dusky_blue","#475f94") -color.indexColor("lightblue","#7bc8f6") -color.indexColor("purply_pink","#f075e6") -color.indexColor("off_green","#6ba353") -color.indexColor("ocher","#bf9b0c") -color.indexColor("milk_chocolate","#7f4e1e") -color.indexColor("light_peach","#ffd8b1") -color.indexColor("deep_magenta","#a0025c") -color.indexColor("caramel","#af6f09") -color.indexColor("greenish_teal","#32bf84") -color.indexColor("pale_lime","#befd73") -color.indexColor("purple_red","#990147") -color.indexColor("blueberry","#464196") -color.indexColor("asparagus","#77ab56") -color.indexColor("pale_grey","#fdfdfe") -color.indexColor("light_grey_blue","#9dbcd4") -color.indexColor("pale_lime_green","#b1ff65") -color.indexColor("grassy_green","#419c03") -color.indexColor("mossy_green","#638b27") -color.indexColor("earth","#a2653e") -color.indexColor("deep_orange","#dc4d01") -color.indexColor("pale_aqua","#b8ffeb") -color.indexColor("rose_red","#be013c") -color.indexColor("stone","#ada587") -color.indexColor("rusty_orange","#cd5909") -color.indexColor("pea","#a4bf20") -color.indexColor("sick_green","#9db92c") -color.indexColor("darker_pink","#c4387f") -color.indexColor("chestnut","#742802") -color.indexColor("blue/green","#0f9b8e") -color.indexColor("amethyst","#9b5fc0") -color.indexColor("dark_mint_green","#20c073") -color.indexColor("pale_rose","#fdc1c5") -color.indexColor("muted_blue","#3b719f") -color.indexColor("fawn","#cfaf7b") -color.indexColor("buff","#fef69e") -color.indexColor("turquoise_green","#04f489") -color.indexColor("muddy_brown","#886806") -color.indexColor("sea","#3c9992") -color.indexColor("tomato","#ef4026") -color.indexColor("carnation_pink","#ff7fa7") -color.indexColor("banana","#ffff7e") -color.indexColor("neon_yellow","#cfff04") -color.indexColor("greyish","#a8a495") -color.indexColor("mid_green","#50a747") -color.indexColor("muted_purple","#805b87") -color.indexColor("electric_pink","#ff0490") -color.indexColor("sandy","#f1da7a") -color.indexColor("ugly_pink","#cd7584") -color.indexColor("turquoise_blue","#06b1c4") -color.indexColor("light_burgundy","#a8415b") -color.indexColor("greenish_tan","#bccb7a") -color.indexColor("dark_mint","#48c072") -color.indexColor("light_urple","#b36ff6") -color.indexColor("midnight_purple","#280137") -color.indexColor("pinkish_orange","#ff724c") -color.indexColor("pear","#cbf85f") -color.indexColor("dark_plum","#3f012c") -color.indexColor("tealish","#24bca8") -color.indexColor("perrywinkle","#8f8ce7") -color.indexColor("yellowish_orange","#ffab0f") -color.indexColor("pastel_orange","#ff964f") -color.indexColor("iris","#6258c4") -color.indexColor("ultramarine_blue","#1805db") -color.indexColor("navy_green","#35530a") -color.indexColor("seaweed","#18d17b") -color.indexColor("kiwi","#9cef43") -color.indexColor("fluro_green","#0aff02") -color.indexColor("bright_light_green","#2dfe54") -color.indexColor("vivid_green","#2fef10") -color.indexColor("frog_green","#58bc08") -color.indexColor("dull_brown","#876e4b") -color.indexColor("dusk","#4e5481") -color.indexColor("mustard_brown","#ac7e04") -color.indexColor("leafy_green","#51b73b") -color.indexColor("cool_blue","#4984b8") -color.indexColor("almost_black","#070d0d") -color.indexColor("yellow/green","#c8fd3d") -color.indexColor("heliotrope","#d94ff5") -color.indexColor("green_apple","#5edc1f") -color.indexColor("baby_poop_green","#8f9805") -color.indexColor("apple","#6ecb3c") -color.indexColor("purpleish_pink","#df4ec8") -color.indexColor("night_blue","#040348") -color.indexColor("merlot","#730039") -color.indexColor("lightgreen","#76ff7b") -color.indexColor("tomato_red","#ec2d01") -color.indexColor("key_lime","#aeff6e") -color.indexColor("pale_cyan","#b7fffa") -color.indexColor("vomit_yellow","#c7c10c") -color.indexColor("purplish_brown","#6b4247") -color.indexColor("bubblegum","#ff6cb5") -color.indexColor("shamrock","#01b44c") -color.indexColor("mango","#ffa62b") -color.indexColor("lime_yellow","#d0fe1d") -color.indexColor("hot_green","#25ff29") -color.indexColor("grape_purple","#5d1451") -color.indexColor("faded_orange","#f0944d") -color.indexColor("avocado_green","#87a922") -color.indexColor("peacock_blue","#016795") -color.indexColor("weird_green","#3ae57f") -color.indexColor("bright_lilac","#c95efb") -color.indexColor("fern_green","#548d44") -color.indexColor("dirty_blue","#3f829d") -color.indexColor("rust_orange","#c45508") -color.indexColor("heather","#a484ac") -color.indexColor("deep_teal","#00555a") -color.indexColor("dark_seafoam","#1fb57a") -color.indexColor("baby_poo","#ab9004") -color.indexColor("yellowgreen","#bbf90f") -color.indexColor("light_sage","#bcecac") -color.indexColor("light_aquamarine","#7bfdc7") -color.indexColor("spearmint","#1ef876") -color.indexColor("bright_lime","#87fd05") -color.indexColor("vibrant_green","#0add08") -color.indexColor("very_pale_green","#cffdbc") -color.indexColor("faded_yellow","#feff7f") -color.indexColor("bile","#b5c306") -color.indexColor("viridian","#1e9167") -color.indexColor("very_light_pink","#fff4f2") -color.indexColor("puke_brown","#947706") -color.indexColor("medium_pink","#f36196") -color.indexColor("ugly_purple","#a442a0") -color.indexColor("sunshine_yellow","#fffd37") -color.indexColor("seaweed_green","#35ad6b") -color.indexColor("light_periwinkle","#c1c6fc") -color.indexColor("lemon_green","#adf802") -color.indexColor("greeny_brown","#696006") -color.indexColor("dark_grey_blue","#29465b") -color.indexColor("bright_olive","#9cbb04") -color.indexColor("turtle_green","#75b84f") -color.indexColor("pale_sky_blue","#bdf6fe") -color.indexColor("light_mustard","#f7d560") -color.indexColor("diarrhea","#9f8303") -color.indexColor("dark_aquamarine","#017371") -color.indexColor("brownish_pink","#c27e79") -color.indexColor("baby_shit_green","#889717") -color.indexColor("purpley","#8756e4") -color.indexColor("greyblue","#77a1b5") -color.indexColor("hot_magenta","#f504c9") -color.indexColor("blue/grey","#758da3") -color.indexColor("pale","#fff9d0") -color.indexColor("cool_green","#33b864") -color.indexColor("sandy_yellow","#fdee73") -color.indexColor("eggshell_blue","#c4fff7") -color.indexColor("barf_green","#94ac02") -color.indexColor("baby_green","#8cff9e") -color.indexColor("vibrant_purple","#ad03de") -color.indexColor("brown_grey","#8d8468") -color.indexColor("water_blue","#0e87cc") -color.indexColor("lipstick_red","#c0022f") -color.indexColor("banana_yellow","#fafe4b") -color.indexColor("wisteria","#a87dc2") -color.indexColor("purple_brown","#673a3f") -color.indexColor("brown_yellow","#b29705") -color.indexColor("purple/pink","#d725de") -color.indexColor("lemon_lime","#bffe28") -color.indexColor("grey/blue","#647d8e") -color.indexColor("dusty_red","#b9484e") -color.indexColor("deep_rose","#c74767") -color.indexColor("dark_seafoam_green","#3eaf76") -color.indexColor("muddy_yellow","#bfac05") -color.indexColor("carnation","#fd798f") -color.indexColor("yellowy_brown","#ae8b0c") -color.indexColor("violet_red","#a50055") -color.indexColor("twilight_blue","#0a437a") -color.indexColor("pure_blue","#0203e2") -color.indexColor("lightish_red","#fe2f4a") -color.indexColor("brick_orange","#c14a09") -color.indexColor("velvet","#750851") -color.indexColor("sunflower","#ffc512") -color.indexColor("light_mint_green","#a6fbb2") -color.indexColor("light_grass_green","#9af764") -color.indexColor("lavender_blue","#8b88f8") -color.indexColor("rusty_red","#af2f0d") -color.indexColor("lightish_purple","#a552e6") -color.indexColor("dried_blood","#4b0101") -color.indexColor("light_blue_grey","#b7c9e2") -color.indexColor("leaf","#71aa34") -color.indexColor("orangish","#fc824a") -color.indexColor("pale_olive_green","#b1d27b") -color.indexColor("off_yellow","#f1f33f") -color.indexColor("dusty_orange","#f0833a") -color.indexColor("butter","#ffff81") -color.indexColor("royal","#0c1793") -color.indexColor("petrol","#005f6a") -color.indexColor("greenish_cyan","#2afeb7") -color.indexColor("duck_egg_blue","#c3fbf4") -color.indexColor("bubble_gum_pink","#ff69af") -color.indexColor("bluegrey","#85a3b2") -color.indexColor("warm_brown","#964e02") -color.indexColor("twilight","#4e518b") -color.indexColor("saffron","#feb209") -color.indexColor("purple/blue","#5d21d0") -color.indexColor("dark_sand","#a88f59") -color.indexColor("vibrant_blue","#0339f8") -color.indexColor("putty","#beae8a") -color.indexColor("lawn_green","#4da409") -color.indexColor("camouflage_green","#4b6113") -color.indexColor("blush_pink","#fe828c") -color.indexColor("reddy_brown","#6e1005") -color.indexColor("darkish_red","#a90308") -color.indexColor("algae_green","#21c36f") -color.indexColor("dark_coral","#cf524e") -color.indexColor("bright_cyan","#41fdfe") -color.indexColor("piss_yellow","#ddd618") -color.indexColor("pastel_red","#db5856") -color.indexColor("greenish_turquoise","#00fbb0") -color.indexColor("dark","#1b2431") -color.indexColor("ruby","#ca0147") -color.indexColor("poop_green","#6f7c00") -color.indexColor("orangered","#fe420f") -color.indexColor("dandelion","#fedf08") -color.indexColor("claret","#680018") -color.indexColor("pale_mauve","#fed0fc") -color.indexColor("lipstick","#d5174e") -color.indexColor("rosa","#fe86a4") -color.indexColor("darkblue","#030764") -color.indexColor("tan_brown","#ab7e4c") -color.indexColor("shit_green","#758000") -color.indexColor("red_wine","#8c0034") -color.indexColor("pinky","#fc86aa") -color.indexColor("mud_green","#606602") -color.indexColor("light_greenish_blue","#63f7b4") -color.indexColor("dull_teal","#5f9e8f") -color.indexColor("deep_lavender","#8d5eb7") -color.indexColor("vivid_blue","#152eff") -color.indexColor("raw_umber","#a75e09") -color.indexColor("light_mint","#b6ffbb") -color.indexColor("light_light_blue","#cafffb") -color.indexColor("highlighter_green","#1bfc06") -color.indexColor("greeny_grey","#7ea07a") -color.indexColor("bluey_grey","#89a0b0") -color.indexColor("algae","#54ac68") -color.indexColor("sap_green","#5c8b15") -color.indexColor("pale_salmon","#ffb19a") -color.indexColor("metallic_blue","#4f738e") -color.indexColor("ice","#d6fffa") -color.indexColor("gross_green","#a0bf16") -color.indexColor("dodger_blue","#3e82fc") -color.indexColor("warm_pink","#fb5581") -color.indexColor("light_green_blue","#56fca2") -color.indexColor("flat_green","#699d4c") -color.indexColor("dark_blue_grey","#1f3b4d") -color.indexColor("clay_brown","#b2713d") -color.indexColor("sand_yellow","#fce166") -color.indexColor("grapefruit","#fd5956") -color.indexColor("blood_orange","#fe4b03") -color.indexColor("very_pale_blue","#d6fffe") -color.indexColor("old_pink","#c77986") -color.indexColor("neon_red","#ff073a") -color.indexColor("golden_rod","#f9bc08") -color.indexColor("plum_purple","#4e0550") -color.indexColor("pale_peach","#ffe5ad") -color.indexColor("green_again","#16d43f") -color.indexColor("dark_yellow_green","#728f02") -color.indexColor("carmine","#9d0216") -color.indexColor("deep_sea_blue","#015482") -color.indexColor("dark_hot_pink","#d90166") -color.indexColor("warm_blue","#4b57db") -color.indexColor("light_khaki","#e6f2a2") -color.indexColor("icky_green","#8fae22") -color.indexColor("greenblue","#23c48b") -color.indexColor("dirty_purple","#734a65") -color.indexColor("rich_blue","#021bf9") -color.indexColor("mushroom","#ba9e88") -color.indexColor("flat_blue","#3c73a8") -color.indexColor("dark_slate_blue","#214761") -color.indexColor("dark_sage","#598556") -color.indexColor("coral_pink","#ff6163") -color.indexColor("true_green","#089404") -color.indexColor("darkish_purple","#751973") -color.indexColor("dark_taupe","#7f684e") -color.indexColor("cool_grey","#95a3a6") -color.indexColor("canary","#fdff63") -color.indexColor("booger_green","#96b403") -color.indexColor("muted_pink","#d1768f") -color.indexColor("hazel","#8e7618") -color.indexColor("dark_royal_blue","#02066f") -color.indexColor("vivid_purple","#9900fa") -color.indexColor("racing_green","#014600") -color.indexColor("leather","#ac7434") -color.indexColor("green/blue","#01c08d") -color.indexColor("sunflower_yellow","#ffda03") -color.indexColor("rich_purple","#720058") -color.indexColor("pale_magenta","#d767ad") -color.indexColor("light_yellowish_green","#c2ff89") -color.indexColor("indigo_blue","#3a18b1") -color.indexColor("dark_fuchsia","#9d0759") -color.indexColor("yellow_tan","#ffe36e") -color.indexColor("wintergreen","#20f986") -color.indexColor("violet_pink","#fb5ffc") -color.indexColor("topaz","#13bbaf") -color.indexColor("seafoam_blue","#78d1b6") -color.indexColor("light_gold","#fddc5c") -color.indexColor("grey/green","#86a17d") -color.indexColor("foam_green","#90fda9") -color.indexColor("creme","#ffffb6") -color.indexColor("clear_blue","#247afd") -color.indexColor("ugly_blue","#31668a") -color.indexColor("terracota","#cb6843") -color.indexColor("very_dark_brown","#1d0200") -color.indexColor("straw","#fcf679") -color.indexColor("parchment","#fefcaf") -color.indexColor("orangey_yellow","#fdb915") -color.indexColor("greyish_teal","#719f91") -color.indexColor("sapphire","#2138ab") -color.indexColor("nice_blue","#107ab0") -color.indexColor("browny_orange","#ca6b02") -color.indexColor("washed_out_green","#bcf5a6") -color.indexColor("tiffany_blue","#7bf2da") -color.indexColor("light_seafoam","#a0febf") -color.indexColor("light_neon_green","#4efd54") -color.indexColor("light_bright_green","#53fe5c") -color.indexColor("light_bluish_green","#76fda8") -color.indexColor("rosy_pink","#f6688e") -color.indexColor("peachy_pink","#ff9a8a") -color.indexColor("pale_light_green","#b1fc99") -color.indexColor("old_rose","#c87f89") -color.indexColor("fern","#63a950") -color.indexColor("dusk_blue","#26538d") -color.indexColor("camo","#7f8f4e") -color.indexColor("burnt_siena","#b75203") -color.indexColor("tealish_green","#0cdc73") -color.indexColor("swamp","#698339") -color.indexColor("sand_brown","#cba560") -color.indexColor("rust_brown","#8b3103") -color.indexColor("orangeish","#fd8d49") -color.indexColor("light_royal_blue","#3a2efe") -color.indexColor("cocoa","#875f42") -color.indexColor("baby_purple","#ca9bf7") -color.indexColor("raw_sienna","#9a6200") -color.indexColor("radioactive_green","#2cfa1f") -color.indexColor("light_pea_green","#c4fe82") -color.indexColor("cinnamon","#ac4f06") -color.indexColor("squash","#f2ab15") -color.indexColor("charcoal_grey","#3c4142") -color.indexColor("bright_yellow_green","#9dff00") -color.indexColor("baby_puke_green","#b6c406") -color.indexColor("poison_green","#40fd14") -color.indexColor("light_lavendar","#efc0fe") -color.indexColor("indian_red","#850e04") -color.indexColor("dark_cream","#fff39a") -color.indexColor("toupe","#c7ac7d") -color.indexColor("butterscotch","#fdb147") -color.indexColor("burple","#6832e3") -color.indexColor("tan_green","#a9be70") -color.indexColor("sun_yellow","#ffdf22") -color.indexColor("pale_gold","#fdde6c") -color.indexColor("light_light_green","#c8ffb0") -color.indexColor("lichen","#8fb67b") -color.indexColor("green/yellow","#b5ce08") -color.indexColor("darkgreen","#054907") -color.indexColor("azul","#1d5dec") -color.indexColor("sunny_yellow","#fff917") -color.indexColor("sickly_yellow","#d0e429") -color.indexColor("kelley_green","#009337") -color.indexColor("bruise","#7e4071") -color.indexColor("browny_green","#6f6c0a") -color.indexColor("battleship_grey","#6b7c85") -color.indexColor("off_blue","#5684ae") -color.indexColor("manilla","#fffa86") -color.indexColor("greenish_beige","#c9d179") -color.indexColor("deep_brown","#410200") -color.indexColor("darkish_pink","#da467d") -color.indexColor("custard","#fffd78") -color.indexColor("ugly_brown","#7d7103") -color.indexColor("stormy_blue","#507b9c") -color.indexColor("liliac","#c48efd") -color.indexColor("baby_shit_brown","#ad900d") -color.indexColor("reddish_grey","#997570") -color.indexColor("powder_pink","#ffb2d0") -color.indexColor("eggplant_purple","#430541") -color.indexColor("egg_shell","#fffcc4") -color.indexColor("very_light_brown","#d3b683") -color.indexColor("tea_green","#bdf8a3") -color.indexColor("orange_pink","#ff6f52") -color.indexColor("light_grey_green","#b7e1a1") -color.indexColor("kiwi_green","#8ee53f") -color.indexColor("boring_green","#63b365") -color.indexColor("light_pastel_green","#b2fba5") -color.indexColor("candy_pink","#ff63e9") -color.indexColor("purply","#983fb2") -color.indexColor("purpley_grey","#947e94") -color.indexColor("dusty_lavender","#ac86a8") -color.indexColor("desert","#ccad60") -color.indexColor("deep_lilac","#966ebd") -color.indexColor("pig_pink","#e78ea5") -color.indexColor("olive_yellow","#c2b709") -color.indexColor("light_seafoam_green","#a7ffb5") -color.indexColor("light_moss_green","#a6c875") -color.indexColor("lavender_pink","#dd85d7") -color.indexColor("deep_aqua","#08787f") -color.indexColor("bland","#afa88b") -color.indexColor("strong_pink","#ff0789") -color.indexColor("green_teal","#0cb577") -color.indexColor("deep_turquoise","#017374") -color.indexColor("dark_green_blue","#1f6357") -color.indexColor("bright_sea_green","#05ffa6") -color.indexColor("booger","#9bb53c") -color.indexColor("blue_with_a_hint_of_purple","#533cc6") -color.indexColor("blue_blue","#2242c7") -color.indexColor("windows_blue","#3778bf") -color.indexColor("toxic_green","#61de2a") -color.indexColor("strong_blue","#0c06f7") -color.indexColor("spruce","#0a5f38") -color.indexColor("pinkish_tan","#d99b82") -color.indexColor("macaroni_and_cheese","#efb435") -color.indexColor("grey_teal","#5e9b8a") -color.indexColor("dusty_teal","#4c9085") -color.indexColor("dark_grass_green","#388004") -color.indexColor("cement","#a5a391") -color.indexColor("yellowish_tan","#fcfc81") -color.indexColor("warm_purple","#952e8f") -color.indexColor("tea","#65ab7c") -color.indexColor("really_light_blue","#d4ffff") -color.indexColor("nasty_green","#70b23f") -color.indexColor("light_eggplant","#894585") -color.indexColor("fresh_green","#69d84f") -color.indexColor("electric_lime","#a8ff04") -color.indexColor("dust","#b2996e") -color.indexColor("dark_pastel_green","#56ae57") -color.indexColor("cloudy_blue","#acc2d9") -color.indexColor("highlighter_blue","#30C5FF") -for i=0,255 do - color.indexColor("gray"..i,i,i,i) -end - -return color \ No newline at end of file diff --git a/gui/core/gifloader.lua b/gui/core/gifloader.lua deleted file mode 100644 index eb36f61..0000000 --- a/gui/core/gifloader.lua +++ /dev/null @@ -1,437 +0,0 @@ --- 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 diff --git a/gui/core/probe.lua b/gui/core/probe.lua deleted file mode 100644 index 9a7d56c..0000000 --- a/gui/core/probe.lua +++ /dev/null @@ -1,115 +0,0 @@ ---[[ - scheduler_probe.lua - ------------------- - A drop-in replacement for multi:getLoad() based on scheduler tick-slip - rather than step-count benchmarking. - - THEORY - ------ - Schedule a repeating timer at a fixed interval T. Each time it fires, - measure how much later than T it actually arrived. On an idle scheduler - the slip is near zero. Under load the main loop is busy with other tasks - between iterations, so ticks are delayed. - - We express load as: - - lag = actual_interval - target_interval (seconds) - lag_ratio = lag / target_interval (0 = perfect, 1 = 1 full interval late) - load% = clamp(lag_ratio * 100, 0, 100) - - To smooth out single-frame spikes we keep an exponential moving average - (EMA) of lag_ratio with a configurable smoothing factor. - - WHY THIS IS BETTER THAN THE STEP-COUNT APPROACH - ------------------------------------------------ - - No calibration baseline that drifts with object count or warm-up - - No magic exponents or divisors - - Does not block or create temporary objects on each call - - Measures actual scheduler responsiveness, not raw throughput - - Works correctly regardless of which processor calls it - - A single lightweight TLoop is the only permanent overhead - - USAGE - ----- - local probe = require("scheduler_probe") - probe:install(multi) -- once, at startup - - -- anywhere, non-blocking: - local load, lagMs = multi:getLoad() - -- load : integer 0-100 - -- lagMs : smoothed lag in milliseconds (useful for display) - - OPTIONAL PARAMETERS - ------------------- - probe:install(multi, { - interval = 0.05, -- probe fires every N seconds (default 0.05 = 50ms) - alpha = 0.15, -- EMA smoothing factor 0-1 (default 0.15) - -- lower = smoother but slower to react - -- higher = more reactive but noisier - maxLag = 0.5, -- lag value (seconds) that maps to 100% load (default 0.5) - -- tune this to match your target frame budget - }) -]] - -local probe = {} - --- EMA state — written by the TLoop callback, read by getLoad() --- Both are plain numbers so Lua's assignment is atomic within one thread. -local _emaRatio = 0 -- smoothed lag / maxLag, clamped 0-1 -local _lagMs = 0 -- smoothed lag in milliseconds for display -local _installed = false - -function probe:install(multi_obj, opts) - if _installed then return end - _installed = true - - opts = opts or {} - local INTERVAL = opts.interval or 0.05 -- seconds between probes - local ALPHA = opts.alpha or 0.15 -- EMA weight for new sample - local MAX_LAG = opts.maxLag or 0.5 -- seconds of lag = 100% load - - local clock = os.clock - - -- Track when the tick *should* have fired so we can compute slip - -- relative to the scheduled time, not relative to the previous firing. - -- This avoids error accumulation over long runs. - local expectedTime = clock() + INTERVAL - - local tloop = multi_obj:newTLoop(nil, INTERVAL) - tloop:setName("SchedulerProbe") - tloop:setPriority("core") -- run as early as possible each frame - - tloop.OnLoop(function(self, life, dt) - local now = clock() - local lag = math.max(0, now - expectedTime) -- never negative - local ratio = math.min(lag / MAX_LAG, 1) -- clamp to [0,1] - - -- Exponential moving average: new = alpha*sample + (1-alpha)*old - _emaRatio = ALPHA * ratio + (1 - ALPHA) * _emaRatio - _lagMs = ALPHA * lag*1000 + (1 - ALPHA) * _lagMs - - -- Advance expected time by one interval from where it *should* have been, - -- not from now — prevents the probe from drifting under sustained load. - expectedTime = expectedTime + INTERVAL - -- If we fall more than one interval behind (e.g. after a long GC pause), - -- re-anchor so we don't fire in a catch-up burst. - if now > expectedTime + INTERVAL then - expectedTime = now + INTERVAL - end - end) - - -- Replace multi:getLoad() with a non-blocking version that just reads the EMA - function multi_obj:getLoad() - local pct = math.ceil(_emaRatio * 100) - return pct, _lagMs - end - - -- Also expose raw probe state for diagnostics - function multi_obj:getSchedulerLag() - return _lagMs, _emaRatio - end - - return tloop -end - -return probe diff --git a/gui/core/simulate.lua b/gui/core/simulate.lua deleted file mode 100644 index 15484dd..0000000 --- a/gui/core/simulate.lua +++ /dev/null @@ -1,91 +0,0 @@ -local gui = require("gui") -local multi, thread = require("multi"):init() -local transition = require("gui.core.transitions") - --- Triggers press then release -local function getPosition(obj, x, y) - if not x or y then - local cx, cy, w, h = obj:getAbsolutes() - return cx + w/2, cy + h/2 - else - return x, y - end -end - -proc = gui:getProcessor() - -local simulate = {} - -function simulate:Press(button, x, y, istouch) - if self then - x, y = getPosition(self, x, y) - elseif x == nil or y == nil then - x, y = love.mouse.getPosition() - end - love.mouse.setPosition(x, y) - gui.Events.OnMousePressed:Fire(x, y, button or gui.MOUSE_PRIMARY, istouch or false) -end - -function simulate:Release(button, x, y, istouch) - if self then - x, y = getPosition(self, x, y) - elseif x == nil or y == nil then - x, y = love.mouse.getPosition() - end - love.mouse.setPosition(x, y) - gui.Events.OnMouseReleased:Fire(x, y, button or gui.MOUSE_PRIMARY, istouch or false) -end - -simulate.Click = proc:newFunction(function(self, button, x, y, istouch) - if self then - x, y = getPosition(self, x, y) - elseif x == nil or y == nil then - x, y = love.mouse.getPosition() - end - love.mouse.setPosition(x, y) - gui.Events.OnMousePressed:Fire(x, y, button or gui.MOUSE_PRIMARY, istouch or false) - thread.skip(1) - gui.Events.OnMouseReleased:Fire(x, y, button or gui.MOUSE_PRIMARY, istouch or false) -end, true) - -simulate.Move = proc:newFunction(function(self, dx, dy, x, y, istouch) - local dx, dy = dx or 0, dy or 0 - - if self then - x, y = getPosition(self, x, y) - elseif x == nil or y == nil then - x, y = love.mouse.getPosition() - end - - if dx == 0 and dy == 0 then - _x, _y = love.mouse.getPosition() - if x == _x and y == _y then - return - end - local dx, dy = 0, 0 - dx = x - _x - dy = y - _y - return simulate.Move(nil, dx, dy) - end - gui.Events.OnMouseMoved:Fire(x, y, 0, 0, istouch or false) - thread.skip(1) - local gx = transition.glide(0, dx, .25) - local gy = transition.glide(0, dy, .25) - local xx = gx() - xx.OnStep(function(p) - _x, _y = love.mouse.getPosition() - love.mouse.setPosition(x + p, _y) - end) - local yy = gy() - yy.OnStep(function(p) - _x, _y = love.mouse.getPosition() - love.mouse.setPosition(_x, y + p) - end) - local event = xx.OnStop * yy.OnStop - if not(dx==0 and dy == 0) then - thread.hold(event) - end - gui.Events.OnMouseMoved:Fire(x + dx, y + dy, 0, 0, istouch or false) -end, true) - -return simulate \ No newline at end of file diff --git a/gui/core/theme.lua b/gui/core/theme.lua deleted file mode 100644 index e1ca818..0000000 --- a/gui/core/theme.lua +++ /dev/null @@ -1,135 +0,0 @@ -local color = require("gui.core.color") -local theme = {} -local defaultFont = love.graphics.getFont() -theme.__index = theme - -local function generate_harmonious_colors(num_colors, lightness) - local base_hue = math.random(0, 360) -- random starting hue - local colors = {} - for i = 1, num_colors do - local new_hue = (base_hue + (360 / num_colors) * i) % 360 -- offset hue by 1/n of the color wheel - if lightness == "dark" then - table.insert(colors, color.new(color.hsl(new_hue, math.random(45, 55), math.random(30, 40)))) - elseif lightness == "light" then - table.insert(colors, color.new(color.hsl(new_hue, math.random(45, 55), math.random(60, 80)))) - else - table.insert(colors, color.new(color.hsl(new_hue, math.random(45, 55), math.random(30, 80)))) - end - - end - return colors -end - -function theme:random(seed, lightness, rand) - local seed = seed or math.random(0,9999999999) - math.randomseed(seed) - local harmonious_colors = generate_harmonious_colors(3, lightness) - local t = theme:new(unpack(harmonious_colors)) - - if lightness == "dark" then - t.colorPrimaryText = color.lighten(t.colorPrimaryText, .8) - t.colorButtonText = color.lighten(t.colorButtonText, .7) - elseif lightness == "light" then - t.colorPrimaryText = color.darken(t.colorPrimaryText, .8) - t.colorButtonText = color.darken(t.colorButtonText, .7) - else - if color.getAverageLightness(t.colorPrimary)<.5 then - t.colorPrimaryText = color.lighten(t.colorPrimaryText, .5) - t.colorButtonNormal = color.lighten(t.colorButtonNormal, .2) - else - t.colorPrimaryText = color.darken(t.colorPrimaryText, .3) - end - - if color.getAverageLightness(t.colorPrimary)<.5 then - t.colorButtonText = color.lighten(t.colorButtonText, .5) - else - t.colorButtonText = color.darken(t.colorButtonText, .3) - end - end - - - t.seed = seed - return t -end - -function theme:dump() - return '"' .. table.concat({color.rgbToHex(self.colorPrimary), color.rgbToHex(self.colorPrimaryText), color.rgbToHex(self.colorButtonText)},"\",\"") .. '"' -end - -local function newColor(c,default) - if not c then - return default - end - return color.new(c) -end - -function theme:new(colorPrimary, primaryText, buttonText, buttonNormal, primaryTextFont, buttonTextFont) - local c = {} - setmetatable(c, theme) - - if type(colorPrimary) == "table" then - local opts = colorPrimary - c.colorPrimary = newColor(opts.primary) - c.colorPrimaryDark = newColor(opts.primaryDark, color.darken(c.colorPrimary,.4)) - c.colorPrimaryText = newColor(opts.primaryText) - c.colorButtonNormal = newColor(opts.buttonNormal, color.darken(c.colorPrimary,.2)) - c.colorButtonHighlight = newColor(opts.buttonHighlight, color.lighten(c.colorPrimary,.2)) - c.colorButtonText = newColor(opts.buttonText, c.colorPrimaryText) - c.fontPrimary = opts.textFont or defaultFont - c.fontButton = opts.buttonTextFont or defaultFont - for i,v in pairs(colorPrimary) do - if not c[i] then - c[i] = v -- only overwrite non managed fields - end - end - return c - end - - c.colorPrimary = color.new(colorPrimary) - c.colorPrimaryDark = color.darken(c.colorPrimary,.4) - c.colorPrimaryText = color.new(primaryText) - c.colorButtonNormal = newColor(buttonNormal) or color.darken(c.colorPrimary,.2) - c.colorButtonHighlight = color.lighten(c.colorButtonNormal,.2) - c.colorButtonText = color.new(buttonText) - c.fontPrimary = primaryTextFont or defaultFont - c.fontButton = buttonTextFont or defaultFont - return c -end - -function theme:setColorPrimary(c) - self.colorPrimary = color.new(c) -end - -function theme:setColorPrimaryDark(c) - self.colorPrimaryDark = color.new(c) -end - -function theme:setColorPrimaryText(c) - self.colorPrimaryText = color.new(c) -end - -function theme:setColorButtonNormal(c) - self.colorButtonNormal = color.new(c) -end - -function theme:setColorButtonHighlight(c) - self.colorButtonHighlight = color.new(c) -end - -function theme:setColorButtonText(c) - self.colorButtonText = color.new(c) -end - -function theme:setFontPrimary(c) - self.fontPrimary = c -end - -function theme:setFontButton(c) - self.fontButton = c -end - -function theme:getSeed() - return self.seed -end - -return theme \ No newline at end of file diff --git a/gui/core/transitions.lua b/gui/core/transitions.lua deleted file mode 100644 index 9e176aa..0000000 --- a/gui/core/transitions.lua +++ /dev/null @@ -1,93 +0,0 @@ -local gui = require("gui") -local multi, thread = require("multi"):init() -local processor = gui:newProcessor("Transition 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) - local s = st or start - local e = sp or stop - local dur = ti or time or 1 - if not s or not e then return multi.error("start and stop must be supplied") end - if s == e then - local temp = { - OnStep = function() end, - OnStop = multi:newConnection() - } - processor:newTask(function() - temp.OnStop:Fire() - end) - return temp - end - local handle = t.func(t, s, e, dur, unpack(args)) - return { - OnStep = handle.OnStatus, - OnStop = handle.OnReturn + handle.OnError, - Kill = function() t:Kill() end - } - 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(f) - self.fps = f - end - - function c:GetFPS() - 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 split = stop - start - local startTime = love.timer.getTime() - local endTime = startTime + time - local stepTime = 1 / t.fps - - t.running = true - - while not t.kill do - thread.sleep(stepTime) - - local now = love.timer.getTime() - local elapsed = now - startTime - local clamped = math.min(elapsed, time) - local value = start + (clamped / time) * split - - thread.pushStatus(value, clamped) - - if now >= endTime then - break - end - end - - t.running = false - t.kill = false -end) - -return transition \ No newline at end of file diff --git a/gui/docs/gui-library-docs.md b/gui/docs/gui-library-docs.md deleted file mode 100644 index 5da4675..0000000 --- a/gui/docs/gui-library-docs.md +++ /dev/null @@ -1,1122 +0,0 @@ -# GUI Library Documentation - -A LÖVE2D-based GUI framework with a dual-dimension layout system, event-driven architecture, theming, transitions, and a rich set of built-in widgets. - ---- - -## Table of Contents - -1. [Setup & Integration](#1-setup--integration) -2. [The Dual-Dimension Layout System](#2-the-dual-dimension-layout-system) -3. [Core Widget Reference](#3-core-widget-reference) - - [Frame](#31-frame) - - [TextLabel](#32-textlabel) - - [TextButton](#33-textbutton) - - [TextBox (Input)](#34-textbox-input) - - [ImageLabel](#35-imagelabel) - - [ImageButton](#36-imagebutton) - - [Video](#37-video) -4. [Base Object API](#4-base-object-api) - - [Positioning & Sizing](#41-positioning--sizing) - - [Appearance](#42-appearance) - - [Visibility & Lifecycle](#43-visibility--lifecycle) - - [Interaction](#44-interaction) - - [Shape & Form Factor](#45-shape--form-factor) -5. [Events & Connections](#5-events--connections) - - [Per-Object Events](#51-per-object-events) - - [Global Events](#52-global-events) - - [Hotkeys](#53-hotkeys) -6. [Theming](#6-theming) -7. [Color Module](#7-color-module) -8. [Transitions & Animation](#8-transitions--animation) -9. [Add-on Widgets](#9-add-on-widgets) - - [Window](#91-window) - - [ScrollFrame](#92-scrollframe) - - [Slide-in Menu](#93-slide-in-menu) - - [Video Player](#94-video-player) -10. [Canvas](#10-canvas) -11. [Simulation (Testing)](#11-simulation-testing) -12. [Scheduler Probe (Load Monitoring)](#12-scheduler-probe-load-monitoring) -13. [Task Manager](#13-task-manager) -14. [Tips & Patterns](#14-tips--patterns) - ---- - -## 1. Setup & Integration - -Require the library at the top of your project. The library hooks into LÖVE's callbacks automatically. - -```lua -local gui = require("gui") - -function love.update(dt) - gui.update(dt) -end - -function love.draw() - gui.draw() -end -``` - -That is all that is required. The library installs its own hooks for `love.keypressed`, `love.mousepressed`, `love.resize`, etc., so you do not need to forward those manually. - -### Creating Additional Processors - -The library runs on the `multi` scheduler. If you need background work to integrate with the GUI update cycle, use `gui:newProcessor`: - -```lua -local proc = gui:newProcessor("MyProcessor") -proc:newThread(function() ... end) -``` - ---- - -## 2. The Dual-Dimension Layout System - -Every object's size and position is described by **eight numbers** that combine pixel offsets with fractional (0–1) scale values relative to the parent. This is the library's central concept. - -``` -setDualDim(x, y, w, h, sx, sy, sw, sh) -``` - -| Parameter | Meaning | -|-----------|---------| -| `x` | Pixel offset from parent's left edge | -| `y` | Pixel offset from parent's top edge | -| `w` | Pixel width | -| `h` | Pixel height | -| `sx` | Fractional X position (0 = left, 1 = right of parent) | -| `sy` | Fractional Y position (0 = top, 1 = bottom of parent) | -| `sw` | Fractional width (0 = 0px, 1 = full parent width) | -| `sh` | Fractional height (0 = 0px, 1 = full parent height) | - -The resolved absolute values are calculated as: - -``` -absolute_x = parent.w * sx + x + parent.x -absolute_y = parent.h * sy + y + parent.y -absolute_w = parent.w * sw + w -absolute_h = parent.h * sh + h -``` - -### Examples - -```lua --- Full-screen frame (fills parent completely) -frame:setDualDim(0, 0, 0, 0, 0, 0, 1, 1) - --- Fixed 200×50 button in the top-left corner -btn:setDualDim(10, 10, 200, 50) - --- Centered horizontally, 300px wide, 5% from the top -panel:setDualDim(-150, 0, 300, 0, 0.5, 0.05, 0, 0.4) --- sx=0.5 puts the left edge at the parent's midpoint; --- x=-150 shifts it left by half the panel's own width. - --- Right-aligned sidebar, 20% of parent width, full height -sidebar:setDualDim(0, 0, 0, 0, 0.8, 0, 0.2, 1) - --- Anchored to bottom-right corner, fixed 100×30 -btn:setDualDim(-110, -40, 100, 30, 1, 1) -``` - -### Helper: `fullFrame()` - -Sets the object to fill its parent completely (equivalent to `setDualDim(0,0,0,0,0,0,1,1)`). - -```lua -frame:fullFrame() -``` - -### Reading Position - -```lua -local x, y, w, h = obj:getAbsolutes() -- resolved pixel values - -local x, y, w, h, sx, sy, sw, sh = obj:getDualDim() -- raw dual-dim values -``` - -### Squaring - -Setting `obj.square = "w"` forces `h = w` (width-driven square). Setting `obj.square = "h"` forces `w = h` (height-driven square). Useful for icon buttons and circle elements. - ---- - -## 3. Core Widget Reference - -All constructors follow the same signature pattern: - -```lua -parent:newXxx(x, y, w, h, sx, sy, sw, sh) -``` - -where `parent` is either `gui` (the root) or another object. Children are drawn on top of and clipped by (if `clipDescendants` is set) their parent. - ---- - -### 3.1 Frame - -A plain rectangular container. The base building block. - -```lua -local panel = gui:newFrame(x, y, w, h, sx, sy, sw, sh) -``` - -```lua --- Example: a full-screen dark overlay -local overlay = gui:newFrame(0, 0, 0, 0, 0, 0, 1, 1) -overlay.color = {0, 0, 0} -overlay.visibility = 0.6 -``` - -**Virtual Frame** — exists in memory and updates but is never drawn. Used to move objects off-screen without destroying them. - -```lua -local vframe = gui:newVirtualFrame(...) -``` - -**Visual Frame** — a frame that does not participate in mouse hit-testing. Children of a visual frame are also non-interactive. - -```lua -local display = gui:newVisualFrame(...) -``` - ---- - -### 3.2 TextLabel - -A non-interactive frame that renders text. - -```lua -local label = parent:newTextLabel("Hello, World!", x, y, w, h, sx, sy, sw, sh) -``` - -**Key properties:** - -| Property | Type | Description | -|----------|------|-------------| -| `text` | string | The displayed text | -| `textColor` | color | Text color (default: black) | -| `font` | Font | LÖVE font object | -| `align` | constant | `gui.ALIGN_LEFT`, `gui.ALIGN_CENTER`, or `gui.ALIGN_RIGHT` | -| `textOffsetX/Y` | number | Pixel nudge for text rendering | -| `textScaleX/Y` | number | Scale multiplier for text | -| `textVisibility` | number | 0–1 alpha for text only | - -**Font methods:** - -```lua -label:setFont(16) -- set by size (default font) -label:setFont("fonts/myfont.ttf", 18) -- set by file + size -label:setFont(myLoveFont) -- set by existing font object - -label:fitFont(minSize, maxSize) -- auto-fit text to the element's bounds -label:centerFont() -- vertically center text within element -``` - ---- - -### 3.3 TextButton - -An interactive button with text. Automatically shows a hand cursor on hover. - -```lua -local btn = parent:newTextButton("Click Me", x, y, w, h, sx, sy, sw, sh) -``` - -```lua -btn.OnReleased(function(self, x, y, button, istouch) - print("Button clicked!") -end) -``` - -Buttons automatically integrate with the theming system inside a `newWindow` — they receive button colors, hover highlights, and the button font. - ---- - -### 3.4 TextBox (Input) - -An editable single-line text input field. Gains focus on click and shows a blinking cursor. - -```lua -local input = parent:newTextBox("placeholder", x, y, w, h, sx, sy, sw, sh) -``` - -**Key properties:** - -| Property | Type | Description | -|----------|------|-------------| -| `text` | string | Current text value | -| `cur_pos` | number | Cursor position (character index) | -| `blink` | boolean | Whether cursor blinks (default: true) | - -**Selection API:** - -```lua -input:HasSelection() -- boolean -input:GetSelection() -- start, stop (always start <= stop) -input:GetSelectedText() -- string -input:ClearSelection() -``` - -**Events:** - -```lua -input.OnReturn(function(self, text) - print("Submitted:", text) -end) -``` - -**Built-in hotkeys** (active when the textbox has focus): -- `Ctrl+A` — select all -- `Ctrl+C` — copy selection -- `Ctrl+V` — paste -- `Ctrl+X` — cut selection -- `Left/Right` — move cursor -- `Backspace/Delete` — delete character - ---- - -### 3.5 ImageLabel - -Displays an image. Supports PNG, JPG, and GIF. - -```lua -local img = parent:newImageLabel("path/to/image.png", x, y, w, h, sx, sy, sw, sh) -``` - -The image is stretched to fill the element's bounds. GIFs animate automatically. - -**Changing the image:** - -```lua -img:setImage("path/to/other.png") -img:setImage(loveImageObject) --- Tile from a spritesheet: -img:setImage("spritesheet.png", srcX, srcY, srcW, srcH) -``` - -**Flipping:** - -```lua -img:flip() -- flip horizontally -img:flip(true) -- flip vertically -``` - -**Gradient fill** (applies a gradient image to any frame or image element): - -```lua -panel:applyGradient("vertical", color1, color2, color3) -panel:applyGradient("horizontal", {1,0,0,1}, {0,0,1,1}) -``` - -**Pre-loading images into cache:** - -```lua -gui.cacheImage(gui, "assets/hero.png") -gui.cacheImage(gui, {"assets/a.png", "assets/b.png"}) -``` - ---- - -### 3.6 ImageButton - -An image that responds to click events. Shows a hand cursor on hover. - -```lua -local btn = parent:newImageButton("icon.png", x, y, w, h, sx, sy, sw, sh) - -btn.OnReleased(function(self) - self:setImage("icon_pressed.png") -end) -``` - ---- - -### 3.7 Video - -Plays a LÖVE-supported video file inside an element. - -```lua -local vid = parent:newVideo("movie.ogv", x, y, w, h, sx, sy, sw, sh) -``` - -**Playback control:** - -```lua -vid:play() -vid:pause() -vid:stop() -- pause + rewind -vid:rewind() -vid:seek(t) -- seek to time in seconds -vid:tell() -- returns current time in seconds -vid:getDuration() -vid:setVolume(0.8) -``` - -**Events:** - -```lua -vid.OnVideoFinished(function(self) - print("Video ended") -end) -``` - ---- - -## 4. Base Object API - -Every object in the hierarchy inherits the following API. - ---- - -### 4.1 Positioning & Sizing - -```lua -obj:setDualDim(x, y, w, h, sx, sy, sw, sh) -- update position/size; nil preserves current value -obj:getAbsolutes() -- returns resolved x, y, w, h in pixels -obj:getDualDim() -- returns all 8 raw values -obj:move(dx, dy) -- increment pixel offset (fires OnPositionChanged) -obj:size(dx, dy) -- increment pixel size (fires OnSizeChanged) -obj:moveInBounds(dx, dy) -- move but keep within parent bounds -obj:fullFrame() -- shorthand for 100%×100% fill -obj:centerX(true) -- auto-center horizontally within parent -obj:centerY(true) -- auto-center vertically within parent -``` - ---- - -### 4.2 Appearance - -```lua -obj.color = {r, g, b} -- background color (0–1 range) -obj.borderColor = {r, g, b} -- border color -obj.drawBorder = true/false -- show/hide border -obj.visibility = 0.8 -- overall opacity 0–1 -obj.rotation = 45 -- rotation in degrees - -obj:setRoundness(rx, ry, segments) -- round all corners -obj:setRoundness(rx, ry, seg, "top") -- round top corners only -obj:setRoundness(rx, ry, seg, "bottom") -- round bottom corners only - -obj.shader = myShader -- apply a LÖVE shader (image elements only) -obj.clipDescendants = true -- clip child rendering to this element's bounds -``` - ---- - -### 4.3 Visibility & Lifecycle - -```lua -obj.visible = false -- hide (and stop receiving events) -obj.active = false -- deactivate without hiding - -obj:isActive() -- true if active and not in the virtual tree -obj:isOffScreen() -- true if fully outside the window - -obj:topStack() -- move to top of parent's draw order (drawn last = on top) -obj:bottomStack() -- move to bottom of draw order - -obj:destroy() -- remove from tree, disconnect all events, free resources -obj:removeChildren() -- destroy all children but keep the object itself -``` - ---- - -### 4.4 Interaction - -```lua --- Drag support -obj:enableDragging(gui.MOUSE_PRIMARY) -- enable drag with left mouse button -obj:enableDragging(gui.MOUSE_SECONDARY) -- enable drag with right button -obj:enableDragging(false) -- disable dragging - --- Hierarchy hit-testing: only fires events when not occluded by a sibling -obj:respectHierarchy(true) - --- Tag system (used for identifying objects and filtering events) -obj:tag("myTag") -- set the primary tag (accessible via :getTag()) -obj:setTag("category") -- set an arbitrary tag key -obj:hasTag("category") -- boolean -obj:parentHasTag("visual") -- checks the ancestor chain - --- Tree queries -obj:getChildren() -- immediate children table -obj:getAllChildren() -- flat list of all visible descendants -obj:isDescendantOf(other) -- boolean -obj:canPress(mx, my) -- boolean: would a click at mx,my hit this object? - --- Cloning -local copy = obj:clone() -local copy = obj:clone({copyTo = parent, connections = true}) -``` - ---- - -### 4.5 Shape & Form Factor - -By default elements are rectangles. You can change an element's form factor: - -```lua --- Circle -obj:makeCircle(x, y, radius, sx, sy, sr, segments) - --- Arc -obj:makeArc(arcType, x, y, radius, sx, sy, sr, angle1, angle2, segments) --- arcType is a LÖVE arc type string: "open", "closed", or "pie" -``` - -The `formFactor` property can also be set directly: - -```lua -obj.formFactor = gui.FORM_RECTANGLE -- default -obj.formFactor = gui.FORM_CIRCLE -obj.formFactor = gui.FORM_ARC -``` - ---- - -## 5. Events & Connections - -The library uses the `multi` connection system. Connections are called with the syntax: - -```lua -obj.OnSomeEvent(function(self, ...) - -- handler -end) -``` - -Multiple handlers can be attached to a single event. Connections can be combined: - -```lua --- OR: fires the handler if either event fires -(obj.OnReleased + obj.OnReleasedOuter)(function() ... end) - --- AND: fires the handler only when both have fired -(conn1 * conn2)(function() ... end) -``` - ---- - -### 5.1 Per-Object Events - -| Event | Arguments | Description | -|-------|-----------|-------------| -| `OnPressed` | `self, x, y, dx, dy, istouch` | Mouse pressed inside the element | -| `OnReleased` | `self, x, y, button, istouch, presses` | Mouse released inside the element | -| `OnReleasedOuter` | same | Released after a press, but outside the element | -| `OnReleasedOther` | same | Released with no previous press on this element | -| `OnPressedOuter` | `self, x, y, button, istouch, presses` | Pressed outside this element | -| `OnEnter` | `self, x, y` | Mouse moved onto the element | -| `OnExit` | `self, x, y` | Mouse moved off the element | -| `OnMoved` | `self, x, y, dx, dy, istouch` | Mouse moved while over the element | -| `OnDragStart` | `self, dx, dy, x, y, istouch` | Drag began | -| `OnDragging` | `self, dx, dy, x, y, istouch` | Drag in progress | -| `OnDragEnd` | `self, dx, dy, x, y, istouch, presses` | Drag ended | -| `OnWheelMoved` | `x, y` | Scroll wheel moved while cursor is over element | -| `OnSizeChanged` | `self, ...` | Element size changed | -| `OnPositionChanged` | `self, ...` | Element position changed | -| `OnDestroy` | `self` | Element is being destroyed | -| `OnCreated` | `element` | Fires for any descendant created under this element | -| `OnLoad` | — | Fires once when the element is first set up | -| `OnUpdate` | `self, dt` | Fires every update frame | - -**Gamepad / joystick events** are also available on every object: - -```lua -obj.OnLeftStickUp / Down / Left / Right -obj.OnRightStickUp / Down / Left / Right -``` - -**TextLabel / TextButton / TextBox extras:** - -```lua -obj.OnFontUpdated(function(self) end) -- font changed -input.OnReturn(function(self, text) end) -- Enter key pressed in textbox -``` - -**Video extras:** - -```lua -vid.OnVideoFinished(function(self) end) -``` - ---- - -### 5.2 Global Events - -All global events live under `gui.Events`: - -```lua -gui.Events.OnKeyPressed(function(key, scancode, isrepeat) end) -gui.Events.OnKeyReleased(function(key, scancode) end) -gui.Events.OnTextInputed(function(text) end) -gui.Events.OnMouseMoved(function(x, y, dx, dy, istouch) end) -gui.Events.OnMousePressed(function(x, y, button, istouch, presses) end) -gui.Events.OnMouseReleased(function(x, y, button, istouch, presses) end) -gui.Events.OnWheelMoved(function(x, y) end) -gui.Events.OnResized(function(w, h) end) -gui.Events.OnQuit(function() end) -gui.Events.OnFilesDropped(function(x, y, files) end) -gui.Events.OnFocus(function(focus) end) -gui.Events.OnObjectFocusChanged(function(previous, current) end) - --- Gamepad / joystick -gui.Events.OnGamepadPressed(function(joystick, button) end) -gui.Events.OnGamepadAxis(function(joystick, axis, value) end) -``` - ---- - -### 5.3 Hotkeys - -Register a hotkey and get back a connection object: - -```lua -local conn = obj:setHotKey({"lctrl", "s"}) -conn(function(ref) - print("Save triggered from", ref) -end) -``` - -Multiple key combinations for the same action: - -```lua -local onSave = gui:setHotKey({"lctrl", "s"}) + gui:setHotKey({"rctrl", "s"}) -onSave(function() save() end) -``` - -**Built-in hotkeys:** - -| Hotkey | Connection | -|--------|-----------| -| Ctrl+A | `gui.HotKeys.OnSelectAll` | -| Ctrl+C | `gui.HotKeys.OnCopy` | -| Ctrl+V | `gui.HotKeys.OnPaste` | -| Ctrl+X | `gui.HotKeys.OnCut` | -| Ctrl+Z | `gui.HotKeys.OnUndo` | -| Ctrl+Y / Ctrl+Shift+Z | `gui.HotKeys.OnRedo` | -| Ctrl+T | Toggle Task Manager | - ---- - -## 6. Theming - -The `theme` module generates consistent color palettes for use with `newWindow` and other themed widgets. - -```lua -local theme = require("gui.core.theme") -``` - -### Creating a Theme - -**From explicit colors:** - -```lua -local t = theme:new(primaryColor, primaryText, buttonText, buttonNormal, primaryFont, buttonFont) - --- Using hex strings (most convenient): -local t = theme:new("#2d6a9f", "#f0f0f0", "#ffffff") -``` - -**From a table (preferred for full control):** - -```lua -local t = theme:new({ - primary = "#124559", - primaryDark = "#01161E", - primaryText = "#AEC3B0", - buttonNormal = "#1e6f8a", - buttonHighlight = "#2a9bbf", - buttonText = "#ffffff", - textFont = myFont, - buttonTextFont = myBoldFont, - -- Any extra keys are kept and accessible on the theme object -}) -``` - -**Random harmonious theme:** - -```lua -local t = theme:random() -- any brightness -local t = theme:random(nil, "dark") -- dark palette -local t = theme:random(nil, "light") -- light palette -local t = theme:random(12345) -- reproducible from a seed -print(t:getSeed()) -- retrieve the seed -print(t:dump()) -- export as hex string -``` - -### Theme Properties - -| Property | Description | -|----------|-------------| -| `colorPrimary` | Main background color | -| `colorPrimaryDark` | Darker variant (headers, accents) | -| `colorPrimaryText` | Text on primary backgrounds | -| `colorButtonNormal` | Button resting color | -| `colorButtonHighlight` | Button hover color | -| `colorButtonText` | Text on buttons | -| `fontPrimary` | Font for labels | -| `fontButton` | Font for buttons | - -### Applying a Theme to a Window - -Pass the theme as the last argument to `newWindow` — the window will automatically style all child buttons and labels that are created inside it: - -```lua -local win = gui:newWindow(x, y, w, h, "Title", draggable, myTheme) -``` - ---- - -## 7. Color Module - -The `color` module handles color creation, conversion, and manipulation. All colors in the library are `{r, g, b}` or `{r, g, b, a}` tables with values in the 0–1 range. - -```lua -local color = require("gui.core.color") -``` - -### Creating Colors - -```lua --- Hex string -color.new("#ff5500") -color.new("#ff550088") -- with alpha - --- CSS-style strings -color.new("rgb(255,85,0)") -color.new("rgba(255,85,0,0.5)") -color.new("hsl(20,100,50)") -color.new("hsla(20,100,50,0.8)") - --- Raw 0–1 floats -color.new(1, 0.33, 0) - --- HSL (hue 0–360, sat 0–100, light 0–100) -color.new(color.hsl(200, 60, 40)) - --- HSV (hue 0–360, sat 0–1, val 0–1) -color.new(color.hsv(200, 0.6, 0.8)) -``` - -### Manipulation - -```lua -color.lighten(c, amount) -- amount is 0–1 factor -color.darken(c, amount) -color.saturate(c, amount) -color.desaturate(c, amount) -color.invert(c) -color.lerp(c1, c2, t) -- blend; t is 0–1 -color.mix(c1, c2, t) -- alias for lerp -``` - -### Queries - -```lua -color.isLight(c) -- boolean -color.getAverageLightness(c) -- 0–1 float -color.rgbToHex(c) -- returns hex string without "#" -``` - -### Arithmetic - -Color objects support `+`, `-`, `*`, `/`, `%`, `^`, and unary `-` operators, applied component-wise. - -### Named Colors - -```lua -color.white -color.black -color.red -color.green -color.blue -color.highlighter_blue --- (and more — check core_color.lua for the full list) -``` - ---- - -## 8. Transitions & Animation - -The `transitions` module provides smooth interpolated animations for numeric values. - -```lua -local transition = require("gui.elements.transitions") -``` - -### Built-in Transitions - -Currently `transition.glide` is provided — a linear glide from a start value to a stop value. - -### Using a Transition - -A transition factory is created by calling `transition.glide(start, stop, duration)`. This returns a **factory function** that, when called, starts the animation and returns a handle. - -```lua --- Animate a panel sliding in from the left -local slideIn = transition.glide(-200, 0, 0.3) -- from -200 to 0 in 0.3 seconds - -local t = slideIn() -- start the animation -t.OnStep(function(position) - panel:setDualDim(position) -end) -t.OnStop(function() - print("Animation complete") -end) -``` - -### Overriding Values at Runtime - -The factory function accepts optional overrides: - -```lua -local t = slideIn(newStart, newStop, newDuration) -``` - -### Stopping Early - -```lua -t.Kill() -``` - -### Custom Transitions - -```lua -local myTransition = transition:newTransition(function(t, start, stop, time) - -- t.fps is the target FPS for this transition - local steps = t.fps * time - local piece = time / steps - t.running = true - for i = 0, steps do - if not t.kill then - thread.sleep(piece) - -- push the current interpolated value as a status update - thread.pushStatus(start + i * ((stop - start) / steps)) - end - end - t.running = false - t.kill = false -end) -``` - -### Changing FPS - -```lua -transition.glide:SetFPS(30) -- lower for performance-sensitive animations -``` - ---- - -## 9. Add-on Widgets - -These widgets live in `gui/addons` and extend the core library. - ---- - -### 9.1 Window - -A resizable, draggable floating window with a title bar and a close button. - -```lua -require("gui.addons") -- loads addons - -local win = gui:newWindow(x, y, width, height, "Window Title", draggable, theme) -``` - -The returned object is the **inner content frame** (inside the title bar). Add children directly to `win`. - -**API:** - -```lua -win:setTitle("New Title") -win:close() -- moves the window to the virtual tree (hides it) -win:open() -- brings the window back to the main tree -win:setTheme(theme) -- re-apply a different theme -win:getTheme() -- returns current theme - -win.OnClose(function(self) - -- fires when the X button is pressed -end) -``` - -**Minimum dimensions:** 200px wide, 100px tall (enforced by resize handles). - -**Children auto-styled:** Any `TYPE_BUTTON` or `TYPE_TEXT` created inside the window automatically receives the theme's colors and fonts via the `OnCreated` event. - ---- - -### 9.2 ScrollFrame - -A viewport with automatic vertical and horizontal scrollbars. Returns the **content frame** — add children to that. - -```lua -require("gui.addons") - -local content = gui:newScrollFrame(x, y, w, h, sx, sy, sw, sh) --- Add children to `content`: -local row = content:newFrame(0, rowY, 0, 30, 0, 0, 1) -``` - -**Scrolling API (on the content frame):** - -```lua -content:scrollTo(scrollY, scrollX) -- jump to absolute scroll position -content:scrollBy(deltaY, deltaX) -- scroll by a relative amount -content:scrollToTop() -content:scrollToBottom() -content:setScrollSpeed(speed) -- default is 40 pixels per wheel tick -content:getScrollPos() -- returns scrollX, scrollY -content:getMaxScroll() -- returns maxScrollX, maxScrollY -content:setContentSize(width, height) -- explicitly set the content dimensions -``` - -The scrollbars appear automatically when the content overflows the viewport and hide when it does not. - ---- - -### 9.3 Slide-in Menu - -A panel that slides in from the left, right, or top with an animated transition. - -```lua -local transition = require("gui.elements.transitions") - -local menu = gui:newMenu(title, size, position, trans) --- title : string label (required) --- size : fractional width/height (e.g. 0.25 for 25% of screen) --- position : gui.ALIGN_LEFT (default), gui.ALIGN_RIGHT, or gui.ALIGN_CENTER --- trans : transition factory (default: transition.glide) -``` - -**API:** - -```lua -menu:Open(true) -- slide open -menu:Open(false) -- slide closed -menu:isOpen() -- boolean -``` - -**Example:** - -```lua -local sidebar = gui:newMenu("Navigation", 0.2, gui.ALIGN_LEFT) - --- Add content to the menu frame -local navBtn = sidebar:newTextButton("Home", 10, 60, 180, 40) - --- Wire toggle -toggleBtn.OnReleased(function() - sidebar:Open(not sidebar:isOpen()) -end) -``` - ---- - -### 9.4 Video Player - -A pre-built media player widget with play/pause toggle and a seek bar. - -```lua -require("gui.addons") - -gui:newVideoPlayer(source, x, y, w, h, sx, sy, sw, sh) -``` - -The player creates its own themed window containing the video, a play/pause button, and a progress bar. - ---- - -## 10. Canvas - -The `canvas` module creates full-screen root frames. - -```lua -local newCanvas = require("gui.core.canvas") - -local visual = newCanvas("visual") -- visual frame: non-interactive, for backgrounds/effects -local regular = newCanvas() -- regular interactive frame -``` - -**Swapping child trees between canvases:** - -```lua -visual:swap(frameA, frameB) --- Exchanges the children of frameA and frameB, re-parenting correctly. --- Useful for scene transitions. -``` - ---- - -## 11. Simulation (Testing) - -The `simulate` module lets you programmatically fire mouse events, useful for automated testing or scripted UI demos. - -```lua -local simulate = require("gui.core.simulate") -``` - -### Methods - -```lua --- Immediate (synchronous) mouse press and release -simulate:Press(button, x, y, istouch) -simulate:Release(button, x, y, istouch) - --- Async click (press then release after one scheduler tick) -simulate.Click(obj, button, x, y, istouch) - --- Animated mouse movement -simulate.Move(obj, dx, dy, x, y, istouch) -``` - -When called on an object (`obj:Press()`), the position defaults to the object's center. When called on `simulate` directly, `x` and `y` default to the current mouse position. - -**Example:** - -```lua --- Programmatically click a button -simulate.Click(myButton) - --- Simulate a drag from one point to another -simulate.Move(nil, 100, 0, startX, startY) -- move 100px to the right -``` - ---- - -## 12. Scheduler Probe (Load Monitoring) - -Measures scheduler responsiveness using tick-slip detection. Gives a 0–100% load estimate without blocking. - -```lua -local probe = require("gui.core.probe") -probe:install(multi) -``` - -**Options:** - -```lua -probe:install(multi, { - interval = 0.05, -- probe fires every N seconds (default: 0.05) - alpha = 0.15, -- EMA smoothing factor 0–1 (default: 0.15, lower = smoother) - maxLag = 0.5, -- seconds of lag that equals 100% load (default: 0.5) -}) -``` - -**Reading load:** - -```lua --- Both are non-blocking — safe to call every frame -local load, lagMs = multi:getLoad() --- load : integer 0–100 --- lagMs : smoothed scheduler lag in milliseconds - -local lagMs, lagRatio = multi:getSchedulerLag() -``` - -The probe is automatically installed when `gui:showTaskManager()` is called. - ---- - -## 13. Task Manager - -A built-in debug overlay showing all active scheduler tasks, their state, uptime, and priority. - -```lua -require("gui.addons") -gui:showTaskManager() -``` - -**Default hotkey:** `Ctrl+T` toggles it open/closed. - -The task manager window provides: -- A list of all processors, threads, and tasks with live state -- Pause/Resume buttons for individual tasks -- Kill buttons to terminate tasks -- Clickable priority column to cycle a task's scheduler priority -- An Error Log tab that captures all thread errors in real-time -- A load bar showing current scheduler utilization - ---- - -## 14. Tips & Patterns - -### Aspect Ratio Locking - -```lua --- Lock the root to a 16:9 canvas; letterbox on resize -gui:setAspectSize(1920, 1080) -gui.aspect_ratio = true -``` - -### Clipping Children - -```lua -local container = gui:newFrame(x, y, w, h) -container.clipDescendants = true -- children are scissored to container bounds -``` - -### Tag-Based Queries - -```lua --- Mark a group of elements and query by tag -for _, child in ipairs(parent:getAllChildren()) do - if child:hasTag("card") then - child.color = selectedColor - end -end -``` - -### Using `gui.apply` for Bulk Property Setting - -`gui.apply` sets properties on multiple objects at once. It understands connection names (`C_` prefix), invoke-style functions (`I_` prefix), and plain properties. - -```lua -gui.apply({ - color = {0.2, 0.2, 0.2}, - drawBorder = false, - I_enableDragging = {gui.MOUSE_PRIMARY}, - OnReleased = function(self) print("clicked", self:getTag()) end, -}, btn1, btn2, btn3) -``` - -### Stacking Order - -Objects are drawn in the order they appear in their parent's `children` table. The last child is drawn on top. - -```lua -obj:topStack() -- draw on top of siblings -obj:bottomStack() -- draw below all siblings -``` - -### Responsive Layouts - -Combine fractional scale with negative pixel offsets for padding: - -```lua --- A frame inset 10px on all sides within its parent -inner:setDualDim(10, 10, -20, -20, 0, 0, 1, 1) -``` - -### Intersection Testing - -```lua -local ix, iy, iw, ih = obj:intersecpt(x, y, w, h) --- Returns the overlapping rectangle, or 0,0,0,0 if no overlap -``` - -### Programmatic Focus - -```lua -local focused = gui:getObjectFocus() -- the currently focused object -``` - -### OnUpdate (per-frame callback) - -```lua -obj:OnUpdate(function(self, dt) - -- runs every frame while the object exists - self.rotation = self.rotation + 90 * dt -end) -``` diff --git a/gui/docs/yaml-based-elements.yaml b/gui/docs/yaml-based-elements.yaml deleted file mode 100644 index a648f5a..0000000 --- a/gui/docs/yaml-based-elements.yaml +++ /dev/null @@ -1,186 +0,0 @@ -# ── Full GUI YAML reference ───────────────────────────────────────── - -# Every element shares these fields: -type: frame # frame | virtual-frame | visual-frame | - # label | button | textbox | - # image | image-button | video - -# ── Position & size (dual-dimension system) ───────────────────────── -# Offset (pixels): -x: 10 -y: 10 -w: 200 -h: 50 -# Or as lists: -pos: [10, 10] -size: [200, 50] - -# Scale (0.0–1.0 of parent): -sx: 0.0 # scale pos x -sy: 0.0 # scale pos y -sw: 0.5 # scale size x — 50% of parent width -sh: 1.0 # scale size y — 100% of parent height -# Or as lists: -scale-pos: [0.0, 0.0] -scale-size: [0.5, 1.0] - -# Shorthand for "fill parent completely": -full-frame: true - -# ── Appearance ─────────────────────────────────────────────────────── -color: "#3a7bd5" # hex string -color: [58, 123, 213] # RGB 0-255 -color: [0.23, 0.48, 0.84] # RGB 0-1 -border-color: "#000000" -draw-border: true -visible: true -active: true -visibility: 1.0 # 0.0–1.0 alpha for the background rect -rotation: 45 # degrees - -# ── Form factor ────────────────────────────────────────────────────── -form: rectangle # default — no extra fields needed -form: circle - radius: 40 # optional; derived from w if omitted - segments: 32 - -form: arc - radius: 60 - arc-type: open # open | closed | pie - angle-start: 0 - angle-end: 3.14159 - segments: 32 - -# ── Roundness ──────────────────────────────────────────────────────── -roundness: 8 # uniform rx/ry -roundness: [8, 8, 30] # rx, ry, segments -roundness: top # "top" or "bottom" special mode -roundness: - side: top # full table form - -# ── Tags ───────────────────────────────────────────────────────────── -tag: "my-element" # single fast-lookup tag (gui:tag) -tags: # multi-tag (gui:setTag) - - draggable-panel - - visual - -# ── Stack order ────────────────────────────────────────────────────── -stack: top # bring to front -stack: bottom # send to back - -# ── Centering ──────────────────────────────────────────────────────── -center-x: true # horizontally center within parent -center-y: true # vertically center within parent - -# ── Dragging ───────────────────────────────────────────────────────── -draggable: 1 # mouse button (1=primary, 2=secondary, 3=middle) - # false/omit to disable - -# ── Hierarchy & clipping ───────────────────────────────────────────── -respect-hierarchy: true # blocks presses when covered by sibling -clip-descendants: true # scissor-clips all children to this rect - -# ── Square locking ─────────────────────────────────────────────────── -square: w # force height = width -square: h # force width = height - -# ── Text element fields (label, button, textbox) ───────────────────── -text: "Hello, world!" -align: left # left | center | right -text-color: "#ffffff" -text-visibility: 1.0 -text-scale: [1.0, 1.0] # [scaleX, scaleY] -text-offset: [0, 0] # [offsetX, offsetY] -text-shear: [0, 0] # [shearX, shearY] - -font: 16 # size, uses default font -font: "fonts/Roboto-Regular.ttf" # path, uses font-size below -font-size: 18 -font: - file: "fonts/Roboto-Regular.ttf" - size: 18 - -fit-font: true # auto-fit font to element bounds -fit-font: - min: 8 - max: 200 - scale: 0.95 # shrink slightly from computed best - -center-font: true # vertically center glyphs in box -center-font: 10 # with y_offset - -# ── Image element fields (image, image-button) ─────────────────────── -source: "assets/logo.png" -tile: [0, 0, 64, 64] # sub-quad [x, y, w, h] -scale-x: 1.0 -scale-y: 1.0 -flip: horizontal # horizontal | vertical | both -image-color: "#ffffff" -image-visibility: 1.0 - -# Gradient (replaces solid image with a generated gradient image): -gradient: - direction: vertical # vertical | horizontal - colors: - - [255, 80, 80, 255] - - [80, 80, 255, 255] - -# ── Video element fields ────────────────────────────────────────────── -source: "assets/intro.ogv" -volume: 0.8 -autoplay: true -video-color: "#ffffff" -video-visibility: 1.0 - -# ── Events ─────────────────────────────────────────────────────────── -# Value can be a global function name (string) or inline Lua source. - -on-pressed: "myPressHandler" -on-released: "myReleaseHandler" -on-released-outer: "myOuterRelease" -on-pressed-outer: "myOuterPress" -on-enter: "onHoverEnter" -on-exit: "onHoverExit" -on-moved: "onMouseMoved" -on-drag-start: "onDragStart" -on-dragging: "onDragging" -on-drag-end: "onDragEnd" -on-wheel: "onWheel" -on-size-changed: "onResized" -on-position-changed: "onMoved" -on-destroy: "onDestroy" -on-load: "onLoaded" -on-return: "onSubmit" # textbox only -on-update: "onUpdate" # called every frame - -# Inline Lua (multi-line string): -on-pressed: | - print("pressed!", self.text) - -# Per-element hotkeys: -hotkeys: - - keys: [lctrl, s] - action: "saveDocument" - - keys: [escape] - action: "closeDialog" - -# ── Children ───────────────────────────────────────────────────────── -children: - - type: label - text: "I am a child" - x: 10 - y: 10 - w: 180 - h: 30 - color: "#2a2a2a" - text-color: "#ffffff" - align: center - - - type: button - text: "Click me" - x: 10 - y: 50 - w: 100 - h: 36 - on-pressed: "handleClick" - children: [] # buttons can also have children \ No newline at end of file diff --git a/gui/init.lua b/gui/init.lua deleted file mode 100644 index bb91d87..0000000 --- a/gui/init.lua +++ /dev/null @@ -1,2447 +0,0 @@ -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.core.gifloader") -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 - --- 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 - --- Form Factor -gui.FORM_RECTANGLE = 1 -gui.FORM_CIRCLE = 2 -gui.FORM_ARC = 3 - --- Variables - -gui.__index = gui -gui.MOUSE_PRIMARY = 1 -gui.MOUSE_SECONDARY = 2 -gui.MOUSE_MIDDLE = 3 - -gui.ALIGN_CENTER = 0 -gui.ALIGN_LEFT = 1 -gui.ALIGN_RIGHT = 2 - --- Connections -gui.Events = {} -- We are using fastmode for all connection objects. -gui.Events.OnQuit = multi:newConnection() -gui.Events.OnDirectoryDropped = multi:newConnection() -gui.Events.OnDisplayRotated = multi:newConnection() -gui.Events.OnFilesDropped = multi:newConnection() -gui.Events.OnFocus = multi:newConnection() -gui.Events.OnMouseFocus = multi:newConnection() -gui.Events.OnResized = multi:newConnection() -gui.Events.OnVisible = multi:newConnection() -gui.Events.OnKeyPressed = multi:newConnection() -gui.Events.OnKeyReleased = multi:newConnection() -gui.Events.OnTextEdited = multi:newConnection() -gui.Events.OnTextInputed = multi:newConnection() -gui.Events.OnMouseMoved = multi:newConnection() -gui.Events.OnMousePressed = multi:newConnection() -gui.Events.OnMouseReleased = multi:newConnection() -gui.Events.OnWheelMoved = multi:newConnection() -gui.Events.OnTouchMoved = multi:newConnection() -gui.Events.OnTouchPressed = multi:newConnection() -gui.Events.OnTouchReleased = multi:newConnection() - --- Joysticks and gamepads -gui.Events.OnGamepadPressed = multi:newConnection() -gui.Events.OnGamepadReleased = multi:newConnection() -gui.Events.OnGamepadAxis = multi:newConnection() -gui.Events.OnJoystickAdded = multi:newConnection() -gui.Events.OnJoystickHat = multi:newConnection() -gui.Events.OnJoystickPressed = multi:newConnection() -gui.Events.OnJoystickReleased = multi:newConnection() -gui.Events.OnJoystickRemoved = multi:newConnection() - --- Internal Connections -gui.Events.OnCreated = multi:newConnection() -gui.Events.OnObjectFocusChanged = multi:newConnection() - --- Virtual gui init -gui.virtual = {} - --- 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 - --- Incase you define one of these methods, we need to process this after that -updater:newTask(function() - -- System - 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("resize", gui.Events.OnResized.Fire) - Hook("visible", gui.Events.OnVisible.Fire) - - -- Mouse - Hook("mousefocus", gui.Events.OnMouseFocus.Fire) - Hook("keypressed", gui.Events.OnKeyPressed.Fire) - Hook("keyreleased", gui.Events.OnKeyReleased.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) - - -- Keyboard - Hook("textedited", gui.Events.OnTextEdited.Fire) - Hook("textinput", gui.Events.OnTextInputed.Fire) - - -- Touchscreen - Hook("touchmoved", gui.Events.OnTouchMoved.Fire) - Hook("touchpressed", gui.Events.OnTouchPressed.Fire) - Hook("touchreleased", gui.Events.OnTouchReleased.Fire) - - -- Joystick/Gamepad - Hook("gamepadpressed", gui.Events.OnGamepadPressed.Fire) - Hook("gamepadaxis", gui.Events.OnGamepadAxis.Fire) - Hook("gamepadreleased", gui.Events.OnGamepadReleased.Fire) - Hook("joystickpressed", gui.Events.OnJoystickPressed.Fire) - Hook("joystickreleased", gui.Events.OnJoystickReleased.Fire) - Hook("joystickhat", gui.Events.OnJoystickHat.Fire) - Hook("joystickremoved", gui.Events.OnJoystickRemoved.Fire) - Hook("joystickadded", gui.Events.OnJoystickAdded.Fire) -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 = {} - --- Wait for keys to release to reset -local unPress = updater:newFunction(function(keys) - local check = function() - for key = 1, #keys["Keys"] do - if not love.keyboard.isDown(keys["Keys"][key]) then - keys.isBusy = false - return true - end - end - end - thread.hold(check) -end) - -updater:newThread("GUI Hotkey Manager", function() - local check = function() return has_hotkey end - while true do - thread.hold(check) - 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:tag(tag) - self.__tag = tag - return self -end - -function gui:getTag() - return self.__tag -end - ---[[ -C_ prefix = connect function to a connection -I_ prefix = invoke function args should be wrapped in a table -]] -local function handleConnection(object,field,value) - if field == "OnUpdate" then - object[field](object,value) - else - object[field](value) - end -end - -local function handleFunction(object,field,value) - if type(value) ~= "table" then return end - object[field](object,unpack(value)) -end - -function gui.apply(apply, ...) - for field, value in pairs(apply) do - for _, object in pairs({...}) do - local cmd = field:sub(1,2) - local handle = field:sub(3,-1) - local tp = type(object[field]) - if cmd == "C_" then - handleConnection(object,handle,value) - elseif cmd == "I_" then - handleFunction(object,handle,value) - elseif tp == "table" and object[field].Type == multi.registerType("connector", "connections") then - handleConnection(object,field,value) - elseif tp == "function" then - handleFunction(object,field,value) - else - object[field] = value - end - end - end -end - -gui.newFunction = updater.newFunction - -function gui:getProcessor() return updater end - -function gui:getObjectFocus() return object_focus end - -function gui:hasType(t) return band(self.type, t) == t end - -function gui:move(x, y) - self.dualDim.offset.pos.x = self.dualDim.offset.pos.x + x - self.dualDim.offset.pos.y = self.dualDim.offset.pos.y + y - self.OnPositionChanged:Fire(self, x, y) -end - -function gui:size(x,y) - self.dualDim.offset.size.x = self.dualDim.offset.size.x + x - self.dualDim.offset.size.y = self.dualDim.offset.size.y + y - self.OnSizeChanged:Fire(self, x, y) -end - -function gui:moveInBounds(dx, dy) - local x, y, w, h = self:getAbsolutes() - local x1, y1, w1, h1 = self.parent:getAbsolutes() - if (x + dx >= x1 or dx > 0) and (x + w + dx <= x1 + w1 or dx < 0) and - (y + dy >= y1 or dy > 0) and (y + h + dy <= y1 + h1 or dy < 0) then - self:move(dx, dy) - end -end - -local function intersecpt(x1, y1, x2, y2, x3, y3, x4, y4) - - local x5 = max(x1, x3) - local y5 = max(y1, y3) - local x6 = min(x2, x4) - local y6 = min(y2, y4) - - -- no intersection - if x5 > x6 or y5 > y6 then - return 0, 0, 0, 0 -- Return a no - end - - local x7 = x5 - local y7 = y6 - local x8 = x6 - local y8 = y5 - - return x7, y7, abs(x7 - x8), abs(y7 - y8) -end - -local function toCoordPoints(x, y, w, h) return x, y, x + w, y + h end - -function gui:intersecpt(x, y, w, h) - local x1, y1, x2, y2 = toCoordPoints(self:getAbsolutes()) - local x3, y3, x4, y4 = toCoordPoints(x, y, w, h) - - return intersecpt(x1, y1, x2, y2, x3, y3, x4, y4) -end - -function gui:isDescendantOf(obj) - local parent = self.parent - while parent ~= gui and parent ~= nil do - if parent == obj then return true end - parent = parent.parent - end - return false -end - -function gui:getChildren() return self.children end - -function gui:offsetToScale() - local children = self:getAllChildren() - for i = 1, #children do - local child = children[i] - local x, y = child:getAbsolutes() - local _, __, w, h = child.parent:getAbsolutes() - local _, __, w, h = child:getAbsolutes() - local _, __, pw, ph = child.parent:getAbsolutes() - end -end - -function gui:getAbsolutes(transform) -- returns x, y, w, h - local x,y,w,h - if transform then - x, y, w, h = transform((self.parent.w * self.dualDim.scale.pos.x) + - self.dualDim.offset.pos.x + self.parent.x), - transform((self.parent.h * self.dualDim.scale.pos.y) + - self.dualDim.offset.pos.y + self.parent.y), transform((self.parent.w * - self.dualDim.scale.size.x) + self.dualDim.offset.size.x), - transform((self.parent.h * self.dualDim.scale.size.y) + - self.dualDim.offset.size.y) - else - x, y, w, h = (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 - if self.square == "w" then - h = w - elseif self.square == "h" then - w = h - end - return x, y, w, h -end - -function gui:getAllChildren(vis) - local children = self:getChildren() - local allChildren = {} - for i, child in ipairs(children) do - if not (vis) and child.visible == true then - allChildren[#allChildren + 1] = child - local grandChildren = child:getAllChildren() - for j, grandChild in ipairs(grandChildren) do - allChildren[#allChildren + 1] = grandChild - end - end - end - return allChildren -end - -function gui:newThread(func) - return updater:newThread("ThreadHandler<" .. self.type .. ">", func, self, thread) -end - -function gui:setDualDim(x, y, w, h, sx, sy, sw, sh) - --[[ - dd.offset.pos = {x = x or 0, y = y or 0} - self.dualDim.offset.size = {x = w or 0, y = h or 0} - self.dualDim.scale.pos = {x = sx or 0, y = sy or 0} - self.dualDim.scale.size = {x = sw or 0, y = sh or 0} - ]] - self.dualDim = self:newDualDim( - x or self.dualDim.offset.pos.x, - y or self.dualDim.offset.pos.y, - w or self.dualDim.offset.size.x, - h or self.dualDim.offset.size.y, - sx or self.dualDim.scale.pos.x, - sy or self.dualDim.scale.pos.y, - sw or self.dualDim.scale.size.x, - sh or self.dualDim.scale.size.y) - self.OnSizeChanged:Fire(self, x, y, w, h, sx, sy, sw, sh) -end - -function gui:rawSetDualDim(x, y, w, h, sx, sy, sw, sh) - self.dualDim = self:newDualDim( - x or self.dualDim.offset.pos.x, - y or self.dualDim.offset.pos.y, - w or self.dualDim.offset.size.x, - h or self.dualDim.offset.size.y, - sx or self.dualDim.scale.pos.x, - sy or self.dualDim.scale.pos.y, - sw or self.dualDim.scale.size.x, - sh or self.dualDim.scale.size.y) -end - -local image_cache = {} -function gui:getTile(i, x, y, w, h) -- returns imagedata - local tw, wh - if i == nil then return end - if type(i) == "string" then i = image_cache[i] or i end - if type(i) == "string" then - i = love.image.newImageData(i) - image_cache[i] = i - elseif type(i) == "userdata" then - -- do nothing - elseif self:hasType(image) then - i, x, y, w, h = self.image, i, x, y, w - else - error("getTile invalid args!!! Usage: ImageElement:getTile(x,y,w,h) or gui:getTile(imagedata,x,y,w,h)") - end - return i, love.graphics.newQuad(x, y, w, h, i:getWidth(), i:getHeight()) -end - -function gui:topStack() - local siblings = self.parent.children - for i = 1, #siblings do - if siblings[i] == self then - table.remove(siblings, i) - break - end - end - siblings[#siblings + 1] = self -end - -function gui:bottomStack() - local siblings = self.parent.children - for i = 1, #siblings do - if siblings[i] == self then - table.remove(siblings, i) - break - end - end - table.insert(siblings, 1, self) -end - -local mainupdater = updater:newLoop() -mainupdater:setName("GUI Update Handler") - -function gui:OnUpdate(func) -- Not crazy about this approach, will probably rework this - if type(self) == "function" then - func = self - end - - mainupdater.OnLoop(function(_,_,dt) - func(self, dt) - end) -end - -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, respect) - -- if not respect then return false end - local children = gui:getAllChildren() - for i = #children, 1, -1 do - if children[i] == self or not respect then - return false - elseif children[i]:canPress(mx, my) and not (children[i] == self) and not (children[i].ignore) then - return true - end - end - return false -end - -function gui:getLocalCords(mx, my) - x, y, w, h = self:getAbsolutes() - return mx - x, my - y -end - -function gui:setParent(parent) - local temp = self.parent:getChildren() - for i = 1, #temp do - if temp[i] == self then - table.remove(self.parent.children, i) - break - end - end - if parent then - table.insert(parent.children, self) - self.parent = parent - end -end - -local function processDo(ref) ref.Do[1]() end - -function gui:clone(opt) - --[[ - { - copyTo: Who to set the parent to - connections: Do we copy connections? (true/false) - } - ]] - - local temp - local u = self:getUniques() - if self.type == frame then - temp = gui:newFrame(self:getDualDim()) - elseif self.type == text + box then - temp = gui:newTextBox(self.text, self:getDualDim()) - elseif self.type == text + button then - temp = gui:newTextButton(self.text, self:getDualDim()) - elseif self.type == text then - temp = gui:newTextLabel(self.text, self:getDualDim()) - elseif self.type == image + button then - temp = gui:newImageButton(u.Do[2], self:getDualDim()) - elseif self.type == image then - temp = gui:newImageLabel(u.Do[2], self:getDualDim()) - else -- We are dealing with a complex object - temp = processDo(u) - end - - for i, v in pairs(u) do temp[i] = v end - - local conn - if opt then - temp:setParent(opt.copyTo or gui.virtual) - if opt.connections then - conn = true - for i, v in pairs(self) do - if type(v) == "table" and v.Type == "connector" then - -- We want to copy the connection functions from the original object and bind them to the new one - if not temp[i] then - -- Incase we are dealing with a custom object, create a connection if the custom objects unique declearation didn't - temp[i] = multi:newConnection() - end - temp[i]:Bind(v:getConnections()) - end - end - end - end - - -- This recursively clones and sets the parent to the temp - for i, v in pairs(self:getChildren()) do - v:clone({copyTo = temp, connections = conn}) - end - - return temp -end - -function gui:isActive() - return self.active and not self:isDescendantOf(gui.virtual) -end - -function gui:isOnScreen() - return not self:isOffScreen() -end - --- Base get uniques -function gui:getUniques(tab) - local base = { - active = self.active, - visible = self.visible, - visibility = self.visibility, - color = self.color, - borderColor = self.borderColor, - drawBorder = self.drawborder, - rotation = self.rotation, - shader = self.shader - } - - if tab then for i, v in pairs(tab) do base[i] = tab[i] end end - return base -end - -function gui:setTag(tag) - self.tags[tag] = true -end - -function gui:hasTag(tag) - return self.tags[tag] -end - -function gui:parentHasTag(tag) - local parent = self.parent - while parent do - if parent.tags and parent.tags[tag] then return true end - parent = parent.parent - if parent == gui.virtual or parent == gui then return false end - end - return false -end - -local function testVisual(c, x, y, button, istouch, presses) - return not(c:hasTag("visual") or c:parentHasTag("visual")) -end - -local extensions = {} - -function gui.registerExtension(template) - table.insert(extensions, template) -end - -function gui:extend(c) - for i,v in pairs(extensions) do - for key, value in pairs(v) do - c[key] = value - end - end -end - --- Base Library -function gui:newBase(typ, x, y, w, h, sx, sy, sw, sh, virtual) - local c = {} - c.tags = {} - 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, true)) - 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 - - local function creationCheck(self) - return self:isDescendantOf(c) - end - - setmetatable(c, gui) - c.__variables = {clip = {false, 0, 0, 0, 0}} - c.focus = false - c.active = true - c.type = typ - c.dualDim = self:newDualDim(x, y, w, h, sx, sy, sw, sh) - c.children = {} - c.visible = true - c.visibility = 1 - c.color = {.6, .6, .6} - c.borderColor = color.black - c.drawBorder = true - c.rotation = 0 - c.formFactor = gui.FORM_RECTANGLE - - c.OnLoad = multi:newConnection() - - c.OnPressed = testVisual .. (testHierarchy .. multi:newConnection()) - c.OnPressedOuter = testVisual .. multi:newConnection() - c.OnReleased = testVisual .. (testHierarchy .. multi:newConnection()) - c.OnReleasedOuter = testVisual .. multi:newConnection() - c.OnReleasedOther = testVisual .. multi:newConnection() - - c.OnDragStart = testVisual .. multi:newConnection() - c.OnDragging = testVisual .. multi:newConnection() - c.OnDragEnd = testVisual .. multi:newConnection() - - c.OnEnter = (testHierarchy .. multi:newConnection()) - c.OnExit = testVisual .. multi:newConnection() - - c.OnMoved = testVisual .. (testHierarchy .. multi:newConnection()) - c.OnWheelMoved = testVisual .. (defaultCheck / gui.Events.OnWheelMoved) - - c.OnSizeChanged = testVisual .. multi:newConnection() - c.OnPositionChanged = testVisual .. multi:newConnection() - - c.OnLeftStickUp = testVisual .. multi:newConnection() - c.OnLeftStickDown = testVisual .. multi:newConnection() - c.OnLeftStickLeft = testVisual .. multi:newConnection() - c.OnLeftStickRight = testVisual .. multi:newConnection() - c.OnRightStickUp = testVisual .. multi:newConnection() - c.OnRightStickDown = testVisual .. multi:newConnection() - c.OnRightStickLeft = testVisual .. multi:newConnection() - c.OnRightStickRight = testVisual .. multi:newConnection() - - c.OnDestroy = multi:newConnection() - - c.OnCreated = creationCheck .. multi:newConnection() - local _forwardedRef = multi.forwardConnection(gui.Events.OnCreated,c.OnCreated) - local dragging = false - local entered = false - local moved = false - local pressed = false - - local _mouseMoveRef = gui.Events.OnMouseMoved(function(x, y, dx, dy, istouch) - if not c:isActive() then return end - if c:canPress(x, y) or dragging 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) - - local _mouseRelRef = gui.Events.OnMouseReleased(function(x, y, button, istouch, presses) - pressed = false -- we need to handle dragging stopped even if an element is not active - if dragging and button == dragbutton then - dragging = false - global_drag = false - c.OnDragEnd:Fire(c, dx, dy, x, y, istouch, presses) - end - if not c:isActive() then return end - if c:canPress(x, y) then - c.OnReleased:Fire(c, x, y, button, 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 - end) - - local _mousePressRef = gui.Events.OnMousePressed(function(x, y, button, istouch, presses) - if not c:isActive() then return end - if c:canPress(x, y) or dragging 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, hierarchy) 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:setColor(key,col) - if col[4] then - self.visibility = col[4] - end - self[key] = col - end - - function c:isOffScreen() - local x, y, w, h = self:getAbsolutes() - return y + h < 0 or y > gui.h or x + w < 0 or x > gui.w - 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:makeCircle(x, y, r, sx, sy, sr, segments) - self.formFactor = gui.FORM_CIRCLE - self.segments = segments - self.__radius = r - self:setDualDim(x, y, 2*r, 2*r, sx, sy, sr) - return self - end - - function c:makeArc(tp, x, y, r, sx, sy, sr, angle1, angle2, segments) - self.arcType = tp - self:setDualDim(x, y, 2*r, 2*r, sx, sy, sr) - self.__angleS = angle1 - self.__angleE = angle2 - self.__radius = r - self.segments = segments - self.formFactor = gui.FORM_ARC - return self - end - - function c:respectHierarchy(bool) - hierarchy = bool - end - - local function centerthread() - if centerX or centerY then - local x, y, w, h = c:getAbsolutes() - if centerX then - c:rawSetDualDim(-w / 2, nil, nil, nil, .5) - end - if centerY then - c:rawSetDualDim(nil, -h / 2, nil, nil, nil, .5) - end - end - end - - function c:enableDragging(but) - if not but then - draggable = false - return - end - dragbutton = but or dragbutton - draggable = true - end - - function c:centerX(bool) - centerX = bool - if centering then return end - centering = true - self.OnSizeChanged(centerthread) - self.OnPositionChanged(centerthread) - updater:newLoop(centerthread) - end - - function c:centerY(bool) - centerY = bool - if centering then return end - centering = true - self.OnSizeChanged(centerthread) - self.OnPositionChanged(centerthread) - updater:newLoop(centerthread) - end - - function c:fullFrame() - self:setDualDim(0,0,0,0,0,0,1,1) - end - - function c:destroy() - -- Find and remove self from parent's children list - local children = self.parent and self.parent.children - if not children then return end - - local foundIdx - for i, v in ipairs(children) do - if v == self then - foundIdx = i - break - end - end - if not foundIdx then return end - - -- Fire OnDestroy before teardown so listeners still work during the callback - self.OnDestroy:Fire(self) - - -- Recursively destroy all children first - for _, child in pairs(self.children) do - if type(child.destroy) == "function" then - child:destroy() - end - end - self.children = {} - - -- Disconnect the global connections - gui.Events.OnMouseMoved:Unconnect(_mouseMoveRef) - gui.Events.OnMouseReleased:Unconnect(_mouseRelRef) - gui.Events.OnMousePressed:Unconnect(_mousePressRef) - gui.Events.OnCreated:Unconnect(_forwardedRef) - self.OnWheelMoved:Destroy() - - -- Destroy all connection objects on self (OnPressed, OnReleased, etc.) - for key, value in pairs(self) do - if type(value) == "table" and - value.Type == multi.registerType("connector", "connections") then - value:Destroy() - end - end - - -- Remove from parent - table.remove(children, foundIdx) - self.parent = nil - end - - function c:removeChildren() - for _, child in pairs(self.children) do - if type(child.destroy) == "function" then - child:destroy() -- recursive, disconnects gui.Events listeners - end - end - self.children = {} - end - - -- Add to the parents children table - if virtual then - c.parent = gui.virtual - table.insert(gui.virtual.children, c) - else - c.parent = self - table.insert(self.children, c) - end - local a = 0 - if typ == frame then - gui.Events.OnCreated:Fire(c) -- Trigger frame types instantly - end - -- shader stuff - - function c:setShader(shader, env) - if type(shader) == "string" then - self.shader = love.graphics.newShader(shader) - elseif type(shader) == "table" then - self.shader = shader.source - for i,v in pairs(shader or {}) do - if i ~= "source" and i ~= "usage" then - if self[i] then - if type(v) == "function" then - local data = v(self) - self.shader:send(i, data) - else - self.shader:send(i, self[i]) - end - elseif env[i] then - if type(v) == "function" then - local data = v(env) - self.shader:send(i, data) - else - self.shader:send(i, env[i]) - end - else - error(i .. " is a required argument!\n\n".. shader.usage()) - end - end - end - else - self.shader = shader -- already a compiled love Shader object - end - return self - end - - function c:clearShader() - self.shader = nil - end - - function c:setShaderUniform(name, ...) - if not self.shader then return end - if self.shader:hasUniform(name) then - self.shader:send(name, ...) - end - end - local st - function c:shaderTime(b) - if not b then - st:Unconnect() - end - if st then return end - self.__shaderTime = 0 - st = mainupdater.OnLoop(function(_, _, dt) - if not self.shader then return end - self.__shaderTime = self.__shaderTime + dt - if self.shader:hasUniform("time") then - self.shader:send("time", self.__shaderTime) - end - end) - end - - gui:extend(c, typ) - - 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 - -function gui:newVisualFrame(x, y, w, h, sx, sy, sw, sh) - local visual = self:newBase(frame, x, y, w, h, sx, sy, sw, sh) - visual:setTag("visual") - return visual -end - -local function anyToString(value) - local t = type(value) - if t == "table" then - local parts = {} - for k, v in pairs(value) do - parts[#parts + 1] = tostring(k) .. "=" .. tostring(v) - end - return "{" .. table.concat(parts, ", ") .. "}" - end - return tostring(value) -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 = testVisual .. multi:newConnection() - - function c:calculateFontOffset(font, adjust) - local adjust = adjust or 20 - local x, y, width, height = self:getAbsolutes() - local top = height + adjust - local bottom = 0 - local canvas = love.graphics.newCanvas(width, height + adjust) - love.graphics.setCanvas(canvas) - love.graphics.clear(0, 0, 0, .5, false, false) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.setFont(font) - love.graphics.printf(self.text, 0, adjust / 2, width, "left", - self.rotation, self.textScaleX, self.textScaleY, 0, - 0, self.textShearingFactorX, - self.textShearingFactorY) - love.graphics.setCanvas() - local data = canvas:newImageData() - local f_top, f_bot = false, false - for yy = 0, height - 1 do - for xx = 0, width - 1 do - local r, g, b, a = data:getPixel(xx, yy) - if r ~= 0 or g ~= 0 or b ~= 0 then - if yy < top and not f_top then - top = yy - f_top = true - break - end - end - end - end - for yy = height - 1, 0, -1 do - for xx = 0, width - 1 do - local r, g, b, a = data:getPixel(xx, yy) - if r ~= 0 or g ~= 0 or b ~= 0 then - if yy > bottom and not f_bot then - bottom = yy - f_bot = false - break - end - end - end - end - return top - adjust, bottom - adjust - end - - function c:setFont(font, size) - if type(font) == "number" then - self.font = love.graphics.newFont(font) - elseif type(font) == "string" then - self.fontFile = font - self.font = love.graphics.newFont(font, size) - else - self.font = font - end - self.OnFontUpdated:Fire(self) - end - - local cache = {} - function c:fitFont(minSize, maxSize, opt) - local _,_,w,h = self:getAbsolutes() - local sw, sh = love.graphics.getDimensions() - local index = self.text .. tostring(w) .. tostring(h) .. tostring(sw) .. tostring(sh) - if cache[index] then - self:setFont(cache[index][1]) - return unpack(cache[index]) - end - if opt == nil then - opt = {scale=1} - end - local font - local x, y, boxWidth, boxHeight = self:getAbsolutes() - - if self.fontFile then - if self.fontFile:match("ttf") then - font = function(n) - return love.graphics.newFont(self.fontFile, n, "normal") - end - else - font = function(n) - return love.graphics.newFont(self.fontFile, n) - end - end - else - font = function(n) return love.graphics.setNewFont(n) end - end - - minSize = minSize or 8 - maxSize = maxSize or 200 - - local bestSize = minSize - local bestFont - - local low = minSize - local high = maxSize - local text = self.text - local mid - while low <= high do - mid = math.floor((low + high) / 2) - local testFont = font(mid) - local width = testFont:getWidth(text) - local height = testFont:getHeight() - - if width <= boxWidth and height <= boxHeight then - -- Font fits, try larger - bestSize = mid - bestFont = testFont - low = mid + 1 - else - -- Font too big, try smaller - high = mid - 1 - end - end - if type(opt) == "table" and opt.scale ~= 0 then - bestFont = font(mid*opt.scale) - else - bestFont = font(mid - 1) - end - self:setFont(bestFont) - cache[index] = {bestFont, bestSize} - return bestFont, bestSize - end - - function c:centerFont(y_offset) - local x, y, width, height = self:getAbsolutes() - local top, bottom = self:calculateFontOffset(self.font, y_offset or 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: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) - - 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) - gui.Events.OnCreated:Fire(c) - return c -end - -function gui:newTextLabel(txt, x, y, w, h, sx, sy, sw, sh) - local c = self:newTextBase(frame, txt, x, y, w, h, sx, sy, sw, sh) - gui.Events.OnCreated:Fire(c) - return c -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 = testVisual .. multi:newConnection() - - c.cur_pos = 0 - c.selection = {0, 0} - c.blink = true - - 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) - gui.Events.OnCreated:Fire(c) - return c -end - -local function textBoxThread() - updater:newThread("Textbox Handler", function() - local check = function() return object_focus:hasType(box) and object_focus.blink end - while true do - -- Do nothing if we aren't dealing with a textbox - thread.hold(check) - 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.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 - _G.THREAD.pushStatus(i, images, love.image.newImageData(paths[i])) - end -end) - --- Loads a resource and adds it to the cache -gui.cacheImage = updater:newFunction(function(self, path_or_paths) - if type(path_or_paths) == "string" then - -- runs thread to load image then cache it for faster loading - load_image(path_or_paths).OnReturn(function(img) - image_cache[path_or_paths] = img - end) - -- table of paths - elseif type(path_or_paths) == "table" then - local handler = load_images(path_or_paths) - handler.OnStatus(function(part, whole, img) - image_cache[path_or_paths[part]] = img - thread.pushStatus(part, whole, image_cache[path_or_paths[part]]) - end) - end -end) - -function gui:applyGradient(direction, ...) - local colors = {...} - if direction == "horizontal" then - direction = true - elseif direction == "vertical" then - direction = false - else - error("Invalid direction '" .. tostring(direction) .. "' for gradient. Horizontal or vertical expected.") - end - local result = love.image.newImageData(direction and 1 or #colors, direction and #colors or 1) - for i, color in ipairs(colors) do - local x, y - if direction then - x, y = 0, i - 1 - else - x, y = i - 1, 0 - end - result:setPixel(x, y, color[1], color[2], color[3], color[4] or 255) - end - - local img = love.graphics.newImage(result) - img:setFilter('linear', 'linear') - local x, y, w, h = self:getAbsolutes() - self.imageColor = color.white - self.imageVisibility = 1 - self.image = img - self.image:setWrap("repeat", "repeat") - self.imageHeight = img:getHeight() - self.imageWidth = img:getWidth() - self.quad = love.graphics.newQuad(0, 0, self.imageWidth, self.imageHeight, self.imageWidth, self.imageHeight) - - if not (band(self.type, image) == image) then - self.type = self.type + image - end -end - -function gui:newImageBase(typ, x, y, w, h, sx, sy, sw, sh) - local c = self:newBase(image + typ, x, y, w, h, sx, sy, sw, sh) - c.color = color.white - c.visibility = 0 - c.scaleX = 1 - c.scaleY = 1 - - local IMAGE - - function c:getUniques() - return gui.getUniques(c, { - -- Recreating the image object using set image is the way to go - DO = {[[setImage]], c.image or IMAGE} - }) - end - - function c:flip(vert) - if vert then - c.scaleY = c.scaleY * -1 - else - c.scaleX = c.scaleX * -1 - end - end - - function c:getSource() - return IMAGE - end - - local img - - c.setImage = function(self, i, x, y, w, h) - if i == nil then return end - - if type(i) == "string" and i:match(".gif") then - img = gif.load(i) - - gif.Updater(img, drawer) - c.OnDestroy(function() - img.kill = true -- trigger the gif thread to terminate - end) - - IMAGE = i - c.__isGif = true - elseif type(i) == "string" then - img = love.image.newImageData(i) - img = love.graphics.newImage(img) - IMAGE = i - end - - if type(i) == "string" then i = image_cache[i] or i end - - if i and x then - c.imageHeight = h - c.imageWidth = w - - if type(i) == "string" and not c.__isGif then - image_cache[i] = img - i = image_cache[i] - end - - c.image = i - if not c.__isGif then - c.image:setWrap("repeat", "repeat") - end - c.imageColor = color.white - c.quad = love.graphics.newQuad(x, y, w, h, c.image:getWidth(), c.image:getHeight()) - c.imageVisibility = 1 - - return - end - - if type(i) == "userdata" and i:type() == "Image" then - img = i - end - - local x, y, w, h = c:getAbsolutes() - c.imageColor = color.white - c.imageVisibility = 1 - c.image = img - if not self.__isGif then - c.image:setWrap("repeat", "repeat") - end - c.imageHeight = img:getHeight() - c.imageWidth = img:getWidth() - c.quad = love.graphics.newQuad(0, 0, c.imageWidth, c.imageHeight, c.imageWidth, c.imageHeight) - end - return c -end - -function gui:newImageLabel(source, x, y, w, h, sx, sy, sw, sh) - local c = self:newImageBase(frame, x, y, w, h, sx, sy, sw, sh) - c:setImage(source) - gui.Events.OnCreated:Fire(c) - 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) - gui.Events.OnCreated:Fire(c) - 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:getDuration() - return c.audioLength - 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 - - function c:isPlaying() return c.video:isPlaying() end - - updater:newThread("Video Handler",function() - - local testCompletion = function() -- More intensive test - if c.video:tell() == 0 then - c.OnVideoFinished:Fire(c) - return true - end - end - - local isplaying = function() -- Less intensive test - return c.video:isPlaying() - end - - while true do thread.chain(isplaying, testCompletion) end - - end) - - c.videoVisibility = 1 - c.videoColor = color.white - gui.Events.OnCreated:Fire(c) - 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) - if child.__isGif then - if child.scaleX < 0 or child.scaleY < 0 then - local sx, sy = child.scaleX, child.scaleY - local adjustX, adjustY = child.scaleX * w, child.scaleY * h - if sx < 0 and sy < 0 then - love.graphics.draw(child.image.frames[child.image.currentFrame], child.quad, x - adjustX, y - adjustY, rad(child.rotation), (w / child.imageWidth) * child.scaleX, (h / child.imageHeight) * child.scaleY) - elseif sx < 0 then - love.graphics.draw(child.image.frames[child.image.currentFrame], child.quad, x - adjustX, y, rad(child.rotation), (w / child.imageWidth) * child.scaleX, h / child.imageHeight) - else - love.graphics.draw(child.image.frames[child.image.currentFrame], child.quad, x, y - adjustY, rad(child.rotation), w / child.imageWidth, (h / child.imageHeight) * child.scaleY) - end - else - if child.image.frames[child.image.currentFrame] then - love.graphics.draw(child.image.frames[child.image.currentFrame], child.quad, x, y, rad(child.rotation), w / child.imageWidth, h / child.imageHeight) - end - end - else - if child.scaleX < 0 or child.scaleY < 0 then - local sx, sy = child.scaleX, child.scaleY - local adjustX, adjustY = child.scaleX * w, child.scaleY * h - if sx < 0 and sy < 0 then - love.graphics.draw(child.image, child.quad, x - adjustX, y - adjustY, rad(child.rotation), (w / child.imageWidth) * child.scaleX, (h / child.imageHeight) * child.scaleY) - elseif sx < 0 then - love.graphics.draw(child.image, child.quad, x - adjustX, y, rad(child.rotation), (w / child.imageWidth) * child.scaleX, h / child.imageHeight) - else - love.graphics.draw(child.image, child.quad, x, y - adjustY, rad(child.rotation), w / child.imageWidth, (h / child.imageHeight) * child.scaleY) - end - else - love.graphics.draw(child.image, child.quad, x, y, rad(child.rotation), w / child.imageWidth, h / child.imageHeight) - end - end - end - end, - [2] = function(child, x, y, w, h) - love.graphics.setColor(child.textColor[1], child.textColor[2], - child.textColor[3], child.textVisibility) - love.graphics.setFont(child.font) - -- if child.align == gui.ALIGN_LEFT then - -- child.adjust = 0 - -- elseif child.align == gui.ALIGN_CENTER then - -- local fw = child.font:getWidth(child.text) - -- child.adjust = (w - fw) / 2 - -- elseif child.align == gui.ALIGN_RIGHT then - -- local fw = child.font:getWidth(child.text) - -- child.adjust = w - fw - 4 - -- end - local mul = 1 - if (child.formFactor == gui.FORM_ARC) or (child.formFactor == gui.FORM_CIRCLE) then - mul = 2 - end - love.graphics.printf(child.text, child.adjust + x + child.textOffsetX, - y + child.textOffsetY, w*mul, ({[0]="center","left", "right", "justify"})[child.align], 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) - draw_factor(child,"fill", x + x1 + child.adjust, y + y1, x2 - x1, y2 - y1) - --love.graphics.rectangle("fill", x + x1 + child.adjust, y + y1,x2 - x1, y2 - y1) - end - end, - [8] = function(child, x, y, w, h) - if child.video and child.playing then - love.graphics.setColor(child.videoColor[1], child.videoColor[2], - child.videoColor[3], child.videoVisibility) - if w ~= child.imageWidth and h ~= child.imageHeight then - love.graphics.draw(child.video, x, y, rad(child.rotation), - w / child.videoWidth, h / child.videoHeigth) - else - love.graphics.draw(child.video, child.quad, x, y, - rad(child.rotation), w / child.videoWidth, - h / child.videoHeigth) - end - end - end, - [16] = function(child, x, y, w, h) - -- - end -} - -local draw_factor = function(child, mode, x, y, w, h, rx, ry, as, ae, seg) - if child.formFactor == gui.FORM_RECTANGLE then - love.graphics.rectangle(mode, x, y, w, h, rx, ry, seg) - elseif child.formFactor == gui.FORM_CIRCLE then - love.graphics.circle(mode, x+child.__radius, y+child.__radius, child.__radius, seg) - elseif child.formFactor == gui.FORM_ARC then - love.graphics.arc(mode, child.arcType, x+child.__radius, y+child.__radius, child.__radius, child.__angleS, child.__angleE, seg) - else - error("Invalid form factor selected: ".. tostring(child.formFactor)) - end -end - -local draw_handler = function(child, no_draw, dt) - 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 child.segments or 0 - child.x = x - child.y = y - child.w = w - child.h = h - - if no_draw then return end - - if child.clipDescendants then - local children = child:getAllChildren() - for c = 1, #children do -- Tell the children to clip themselves - local clip = children[c].__variables.clip - clip[1] = true - clip[2] = x - clip[3] = y - clip[4] = w - clip[5] = h - end - end - - if child.shader then - if child.shader:hasUniform("size") then - child.shader:send("size", {w, h}) - end - if child.shader:hasUniform("position") then - child.shader:send("position", {x, y}) - end - love.graphics.setShader(child.shader) - end - - if child.__variables.clip[1] then - local clip = child.__variables.clip - love.graphics.setScissor(clip[2], clip[3], clip[4], clip[5]) - elseif type(roundness) == "string" then - love.graphics.setScissor(x - 1, y - 2, w + 2, h + 3) - end - - local drawB = child.drawBorder - - love.graphics.setColor(bg[1], bg[2], bg[3], vis) - draw_factor(child,"fill", x, y, w, h, rx, ry, nil, nil, segments) - - love.graphics.setLineStyle("smooth") - love.graphics.setLineWidth(1) - - if drawB then - love.graphics.setColor(bbg[1], bbg[2], bbg[3], vis) - draw_factor(child,"line", x, y, w, h, rx, ry, nil, nil, segments) - if roundness == "top" then - love.graphics.setColor(bg[1], bg[2], bg[3], vis) - draw_factor(child,"fill", x, y + ry / 2, w, h - ry / 2 + 1) - --love.graphics.rectangle("fill", x, y + ry / 2, w, h - ry / 2 + 1) - love.graphics.setLineStyle("smooth") - 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.setColor(bg[1], bg[2], bg[3], vis) - draw_factor(child,"fill", x, y, w, h - ry + 2) - --love.graphics.rectangle("fill", x, y, w, h - ry + 2) - love.graphics.setLineStyle("smooth") - love.graphics.setColor(bbg[1], bbg[2], bbg[3], 1) - love.graphics.setLineWidth(2) - love.graphics.line(x - 1, y + ry + 1, x - 1, y - 1, x + w + 1, y - 1, - x + w + 1, y + ry + 1) - love.graphics.setScissor() - love.graphics.line(x - 1, y - 1, x + w + 1, y - 1) - - love.graphics.setColor(bbg[1], bbg[2], bbg[3], .6) - love.graphics.setLineWidth(2) - love.graphics.line(x - 1, y + 2, x - 1, y + h - 4 - ry / 2) - love.graphics.line(x + w + 1, y + 2, x + w + 1, y + h - 4 - ry / 2) - end - end - - -- Start object specific stuff - drawtypes[band(ctype, video)](child, x, y, w, h) - drawtypes[band(ctype, image)](child, x, y, w, h) - drawtypes[band(ctype, text)](child, x, y, w, h) - drawtypes[band(ctype, box)](child, x, y, w, h) - - if child.post then child:post() end - - if child.__variables.clip[1] then - love.graphics.setScissor() -- Remove the scissor - end - - if child.shader then - love.graphics.setShader() - end -end - -gui.draw_handler = draw_handler - -local function has_blur_ancestor(child) - local parent = child.parent - while parent and parent ~= gui and parent ~= gui.virtual do - if parent.__blur then return parent end - parent = parent.parent - end - return nil -end - -local function blur_draw(child, dt) - local x, y, w, h = child:getAbsolutes() - child.x = x - child.y = y - child.w = w - child.h = h - - local b = child.__blur - local pw, ph = math.max(1, math.ceil(w)), math.max(1, math.ceil(h)) - - if not b.canvas1 - or b.canvas1:getWidth() ~= pw - or b.canvas1:getHeight() ~= ph then - b.canvas1 = love.graphics.newCanvas(pw, ph) - b.canvas2 = love.graphics.newCanvas(pw, ph) - end - - local prevCanvas = love.graphics.getCanvas() - local prevShader = love.graphics.getShader() - local pr, pg, pb, pa = love.graphics.getColor() - - -- ------------------------------------------------------- - -- Pass 1: draw the object AND all its descendants onto canvas1 - -- ------------------------------------------------------- - love.graphics.setCanvas(b.canvas1) - love.graphics.setShader() - love.graphics.clear(0, 0, 0, 0) - love.graphics.setScissor() - - -- Shift everything into canvas space by offsetting by -x, -y - love.graphics.push() - love.graphics.translate(-x, -y) - - -- Draw the parent object itself - draw_handler(child, nil, dt) - - -- Draw all descendants in order - local descendants = child:getAllChildren() - for i = 1, #descendants do - local desc = descendants[i] - -- Recalculate absolutes so positions are correct - local dx, dy, dw, dh = desc:getAbsolutes() - desc.x = dx - desc.y = dy - desc.w = dw - desc.h = dh - draw_handler(desc, nil, dt) - end - - love.graphics.pop() - - -- ------------------------------------------------------- - -- Pass 2: horizontal blur canvas1 → canvas2 - -- ------------------------------------------------------- - b.shader_h:send("size", {pw, ph}) - b.shader_h:send("radius", b.radius) - - love.graphics.setCanvas(b.canvas2) - love.graphics.clear(0, 0, 0, 0) - love.graphics.setShader(b.shader_h) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.draw(b.canvas1, 0, 0) - - -- ------------------------------------------------------- - -- Pass 3: vertical blur canvas2 → screen - -- ------------------------------------------------------- - b.shader_v:send("size", {pw, ph}) - b.shader_v:send("radius", b.radius) - - love.graphics.setCanvas(prevCanvas) - love.graphics.setShader(b.shader_v) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.draw(b.canvas2, x, y) - - -- Restore state - love.graphics.setShader(prevShader) - love.graphics.setColor(pr, pg, pb, pa) - love.graphics.setScissor() -end - -local draw_loop = drawer:newLoop(function(self, dt) - local children = gui:getAllChildren() - for i = 1, #children do - local child = children[i] - -- Skip if this child belongs to a blur parent - -- (blur_draw handles drawing it during the canvas capture pass) - if has_blur_ancestor(child) then - -- update x/y/w/h so layout still works, but don't draw - local x, y, w, h = child:getAbsolutes() - child.x = x - child.y = y - child.w = w - child.h = h - elseif child.__blur then - blur_draw(child, dt) - elseif child.effect then - child.effect(function() draw_handler(child, nil, dt) end) - else - draw_handler(child, nil, dt) - end - end - love.graphics.setColor(1, 1, 1, 1) -end) -draw_loop:setName("GUI Draw Handler") - -drawer:newThread("Draw Handler",function() - while true do - thread.sleep(.1) - local children = gui.virtual:getAllChildren() - for i = 1, #children do - local child = children[i] - draw_handler(child, true, 0) - end - end -end) - -local processors = { - updater.run -} - --- Drawing and Updating -gui.draw = drawer.run -gui.update = function(dt) - for i = 1, #processors do - processors[i](dt) - end -end - -function gui:newProcessor(name) - local proc = multi:newProcessor(name or "UnNamedProcess_"..multi.randomString(4), true) - table.insert(processors, proc.run) - return proc -end - --- Virtual gui -gui.virtual.type = frame -gui.virtual.children = {} -gui.virtual.dualDim = gui:newDualDim() -gui.virtual.x = 0 -gui.virtual.y = 0 -setmetatable(gui.virtual, gui) - -local w, h = love.graphics.getDimensions() - -gui.virtual.dualDim.offset.size.x = w -gui.virtual.dualDim.offset.size.y = h -gui.virtual.w = w -gui.virtual.h = h -gui.virtual.parent = gui.virtual - --- Root gui -gui.parent = gui -gui.type = frame -gui.children = {} -gui.dualDim = gui:newDualDim() -gui.x = 0 -gui.y = 0 - -local w, h = love.graphics.getDimensions() -gui.dualDim.offset.size.x = w -gui.dualDim.offset.size.y = h -gui.w = w -gui.h = h - -function gui:GetSizeAdjustedToAspectRatio(dWidth, dHeight) - - local newHeight = 0 - local newWidth = 0 - - if self.g_width / self.g_height > dWidth / dHeight then - newHeight = dWidth * self.g_height / self.g_width - newWidth = dWidth - else - newWidth = dHeight * self.g_width / self.g_height - newHeight = dHeight - end - return newWidth, newHeight, (dWidth-newWidth)/2, (dHeight-newHeight)/2 -end - ---gui.GetSizeAdjustedToAspectRatio = GetSizeAdjustedToAspectRatio - -function gui:setAspectSize(w, h) - if w and h then - self.g_width, self.g_height = w, h - else - self.g_width, self.g_height = 0, 0 - end -end - -gui.Events.OnResized(function(w, h) - if gui.aspect_ratio then - local nw, nh, xt, yt = gui:GetSizeAdjustedToAspectRatio(w, h) - gui.x = xt - gui.y = yt - gui.dualDim.offset.size.x = nw - gui.dualDim.offset.size.y = nh - gui.w = nw - gui.h = nh - - gui.virtual.x = xt - gui.virtual.y = yt - gui.virtual.dualDim.offset.size.x = nw - gui.virtual.dualDim.offset.size.y = nh - gui.virtual.w = nw - gui.virtual.h = nh - else - gui.dualDim.offset.size.x = w - gui.dualDim.offset.size.y = h - gui.w = w - gui.h = h - - gui.virtual.dualDim.offset.size.x = w - gui.virtual.dualDim.offset.size.y = h - gui.virtual.w = w - gui.virtual.h = h - end -end) - --- load shaders -files = love.filesystem.getDirectoryItems("gui/shaders") -for i,v in pairs(files) do - require("gui.shaders."..v:sub(1,-5)).init(gui) -end - -return gui diff --git a/gui/shaders/blur.lua b/gui/shaders/blur.lua deleted file mode 100644 index 98471e7..0000000 --- a/gui/shaders/blur.lua +++ /dev/null @@ -1,70 +0,0 @@ -local blur_h = love.graphics.newShader([[ - extern vec2 size; - extern float radius; - - vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) { - vec2 step = vec2(1.0 / size.x, 0.0); - vec4 result = vec4(0.0); - float total = 0.0; - float sigma = radius * 0.5; - float r = floor(radius); - float i = -r; - - while (i <= r) { - float weight = exp(-0.5 * (i * i) / (sigma * sigma)); - result += Texel(tex, tc + step * i) * weight; - total += weight; - i = i + 1.0; - } - - return (result / total) * color; - } -]]) - -local blur_v = love.graphics.newShader([[ - extern vec2 size; - extern float radius; - - vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) { - vec2 step = vec2(0.0, 1.0 / size.y); - vec4 result = vec4(0.0); - float total = 0.0; - float sigma = radius * 0.5; - float r = floor(radius); - float i = -r; - - while (i <= r) { - float weight = exp(-0.5 * (i * i) / (sigma * sigma)); - result += Texel(tex, tc + step * i) * weight; - total += weight; - i = i + 1.0; - } - - return (result / total) * color; - } -]]) - -local c = {} - -function c:setBlur(radius) - radius = radius or 8 - c.__blur = { - radius = radius, - canvas1 = nil, -- populated on first draw - canvas2 = nil, - shader_h = blur_h, - shader_v = blur_v, - } -end - -function c:clearBlur() - c.__blur = nil -end - -function c:setBlurRadius(radius) - if c.__blur then - c.__blur.radius = radius - end -end - -return {init = function(gui) gui.registerExtension(c) end} \ No newline at end of file diff --git a/gui/shaders/shaders.lua b/gui/shaders/shaders.lua deleted file mode 100644 index f9e8048..0000000 --- a/gui/shaders/shaders.lua +++ /dev/null @@ -1,274 +0,0 @@ -local shaders = {} -function NewShader(name, shader, opt_args) - local uniforms = {} - local opt_args = opt_args or {} - for typ, uname in shader:gmatch("extern%s+(%w+)%s+(%w+)%s*;") do - if uname ~= "time" and uname ~= "size" then - table.insert(uniforms, "Argument \"" .. uname .. "\" is expected to be: \"" .. typ .. "\"") - opt_args[uname] = true - end - end - if #opt_args > 0 or #uniforms > 0 then - opt_args.source = love.graphics.newShader(shader) - shaders[name] = opt_args - if opt_args.usage == nil then - opt_args.usage = function() return table.concat(uniforms,"\n").."\n" end - end - else - shaders[name] = love.graphics.newShader(shader) - end -end - -function GetShaderUniforms(name) - local source = shaders[name] -- we need the source not the compiled shader - local uniforms = {} - for type, uname in source:gmatch("extern%s+(%w+)%s+(%w+)%s*;") do - uniforms[uname] = type - end - return uniforms -end - --- ───────────────────────────────────────────── --- GLOW (original – kept for reference) --- Uniforms: vec2 size, float time --- ───────────────────────────────────────────── -NewShader("glow", [[ - extern float time; - vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) { - vec4 pixel = Texel(tex, tc); - float pulse = 0.15 * sin(time * 3.0) + 0.85; - return vec4(pixel.rgb * pulse, pixel.a) * color; - } -]]) - --- ───────────────────────────────────────────── --- GRAYSCALE --- Converts the sprite to grayscale (good for disabled/inactive state). --- Uniforms: float grayScale (0.0 = full color, 1.0 = full gray) --- ───────────────────────────────────────────── -NewShader("grayscale", [[ - extern float amount; - vec4 effect(vec4 col, Image tex, vec2 tc, vec2 sc) { - vec4 pixel = Texel(tex, tc) * col; - float gray = dot(pixel.rgb, vec3(0.299, 0.587, 0.114)); - float a = clamp(amount, 0.0, 1.0); - vec3 mixed = pixel.rgb * (1.0 - a) + vec3(gray) * a; - return vec4(mixed, pixel.a); - } -]]) - - --- ───────────────────────────────────────────── --- CHROMATIC ABERRATION --- Uniforms: float amount (try 0.003 – 0.012) --- ───────────────────────────────────────────── -NewShader("chromatic_aberration", [[ - extern float amount; - vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) { - vec4 r = Texel(tex, tc + vec2(amount, 0.0)); - vec4 g = Texel(tex, tc); - vec4 b = Texel(tex, tc + vec2(-amount, 0.0)); - vec4 pixel = vec4(r.r, g.g, b.b, g.a) * color; - return pixel; - } -]]) - --- ───────────────────────────────────────────── --- GAUSSIAN BLUR (single-pass, 9-tap) --- Run twice with {1,0} then {0,1} for full 2D blur. --- Uniforms: vec2 direction ({1,0} or {0,1}), vec2 size --- ───────────────────────────────────────────── -NewShader("blur", [[ - extern vec2 direction; - extern vec2 size; - vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) { - vec2 px = direction / size; - vec4 result = vec4(0.0); - result += Texel(tex, tc - px * 4.0) * 0.0162; - result += Texel(tex, tc - px * 3.0) * 0.0540; - result += Texel(tex, tc - px * 2.0) * 0.1216; - result += Texel(tex, tc - px * 1.0) * 0.1945; - result += Texel(tex, tc ) * 0.2270; - result += Texel(tex, tc + px * 1.0) * 0.1945; - result += Texel(tex, tc + px * 2.0) * 0.1216; - result += Texel(tex, tc + px * 3.0) * 0.0540; - result += Texel(tex, tc + px * 4.0) * 0.0162; - return result * color; - } -]]) - --- ───────────────────────────────────────────── --- SCANLINES --- Uniforms: float strength (0.0-1.0), float count (e.g. 200) --- ───────────────────────────────────────────── -NewShader("scanlines", [[ - extern float strength; - extern float count; - vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) { - vec4 pixel = Texel(tex, tc) * color; - float line = sin(tc.y * count * 3.14159) * 0.5 + 0.5; - float dim = 1.0 - strength * (1.0 - line); - return vec4(pixel.rgb * dim, pixel.a); - } -]]) - --- ───────────────────────────────────────────── --- PIXELATE --- Uniforms: float pixels (grid cell size, e.g. 8.0), vec2 size --- ───────────────────────────────────────────── -NewShader("pixelate", [[ - extern float pixels; - extern vec2 size; - vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) { - vec2 grid = floor(tc * size / pixels) * pixels / size; - return Texel(tex, grid) * color; - } -]]) - --- ───────────────────────────────────────────── --- VIGNETTE --- Uniforms: float intensity (0.0-1.0), float smoothness (0.0-1.0) --- ───────────────────────────────────────────── -NewShader("vignette", [[ - extern float intensity; - extern float smoothness; - vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) { - vec4 pixel = Texel(tex, tc) * color; - vec2 uv = tc - 0.5; - float dist = length(uv); - float vig = smoothstep(0.8, 0.8 - smoothness, dist * intensity); - return vec4(pixel.rgb * vig, pixel.a); - } -]]) - --- ───────────────────────────────────────────── --- HUE SHIFT --- Uniforms: float hue (radians, 0 = no change) --- ───────────────────────────────────────────── -NewShader("hue_shift", [[ - extern float hue; - vec3 rgb2hsv(vec3 c) { - vec4 K = vec4(0.0, -1.0/3.0, 2.0/3.0, -1.0); - vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g)); - vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r)); - float d = q.x - min(q.w, q.y); - float e = 1.0e-10; - return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x); - } - vec3 hsv2rgb(vec3 c) { - vec4 K = vec4(1.0, 2.0/3.0, 1.0/3.0, 3.0); - vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); - return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); - } - vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) { - vec4 pixel = Texel(tex, tc) * color; - vec3 hsv = rgb2hsv(pixel.rgb); - hsv.x = fract(hsv.x + hue / 6.28318); - return vec4(hsv2rgb(hsv), pixel.a); - } -]]) - ---[[ - NEEDS TESTING :P -]] - --- ───────────────────────────────────────────── --- DISSOLVE --- Uniforms: float threshold (0.0=visible, 1.0=gone) --- float edge_width (e.g. 0.05) --- vec4 edge_color --- ───────────────────────────────────────────── -NewShader("dissolve", [[ - extern float threshold; - extern float edge_width; - extern vec4 edge_color; - float hash(vec2 p) { - return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); - } - float noise(vec2 p) { - vec2 i = floor(p); - vec2 f = fract(p); - vec2 u = f * f * (3.0 - 2.0 * f); - return mix(mix(hash(i), hash(i + vec2(1,0)), u.x), - mix(hash(i + vec2(0,1)), hash(i + vec2(1,1)), u.x), u.y); - } - vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) { - vec4 pixel = Texel(tex, tc) * color; - float n = noise(tc * 8.0); - if (n < threshold) discard; - if (n < threshold + edge_width) return edge_color; - return pixel; - } -]]) - --- ───────────────────────────────────────────── --- WAVE --- Uniforms: float time, float amplitude (e.g. 0.01), float frequency (e.g. 10.0) --- ───────────────────────────────────────────── -NewShader("wave", [[ - extern float time; - extern float amplitude; - extern float frequency; - vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) { - vec2 uv = tc; - uv.x += sin(uv.y * frequency + time * 3.0) * amplitude; - uv.y += sin(uv.x * frequency + time * 2.5) * amplitude * 0.6; - return Texel(tex, uv) * color; - } -]]) - - --- ───────────────────────────────────────────── --- RAINBOW / IRIDESCENCE --- Uniforms: float time, float speed (e.g. 1.0), float spread (e.g. 2.0) --- ───────────────────────────────────────────── -NewShader("rainbow", [[ - extern float time; - extern float speed; - extern float spread; - vec3 hsv2rgb(vec3 c) { - vec4 K = vec4(1.0, 2.0/3.0, 1.0/3.0, 3.0); - vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); - return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); - } - vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) { - vec4 pixel = Texel(tex, tc) * color; - float h = fract(tc.x * spread + time * speed * 0.1); - vec3 rainbow = hsv2rgb(vec3(h, 0.8, 1.0)); - return vec4(pixel.rgb * rainbow, pixel.a); - } -]]) - - --- ───────────────────────────────────────────── --- FLASH / HIT-FLASH --- Uniforms: float flash (0.0=normal, 1.0=full flash) --- vec4 flash_color (e.g. {1,1,1,1}) --- ───────────────────────────────────────────── -NewShader("flash", [[ - extern float flash; - extern vec4 flash_color; - vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) { - vec4 pixel = Texel(tex, tc) * color; - vec3 mixed = pixel.rgb * (1.0 - flash) + flash_color.rgb * flash; - return vec4(mixed, pixel.a); - } -]]) - - --- ───────────────────────────────────────────── --- INVERT --- Uniforms: float amount (0.0 = normal, 1.0 = fully inverted) --- ───────────────────────────────────────────── -NewShader("invert", [[ - extern float amount; - vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) { - vec4 pixel = Texel(tex, tc) * color; - vec3 inverted = 1.0 - pixel.rgb; - vec3 mixed = pixel.rgb * (1.0 - amount) + inverted * amount; - return vec4(mixed, pixel.a); - } -]]) - --- Auto hooks new shaders to SHADERS namespace, also adds the NewShader function -return {init = function(gui) gui.SHADERS=shaders gui.NewShader = NewShader end} \ No newline at end of file diff --git a/gui/yaml/builder.lua b/gui/yaml/builder.lua deleted file mode 100644 index 84bb80f..0000000 --- a/gui/yaml/builder.lua +++ /dev/null @@ -1,430 +0,0 @@ --- gui_yaml.lua --- Parses a YAML-like table (pre-parsed by a YAML lib) into GUI elements. --- Usage: local yaml = require("tinyyaml") (or lyaml, etc.) --- local def = yaml.parse(yaml_string) --- local root = gui_yaml.build(gui, def) - -local gui_yaml = {} - --- ───────────────────────────────────────────── --- Helpers --- ───────────────────────────────────────────── - -local function parseColor(v) - if type(v) == "string" then - return require("gui.core.color").new(v) - elseif type(v) == "table" then - -- {r, g, b} or {r, g, b, a} — values 0-255 or 0-1 - local r, g, b, a = v[1], v[2], v[3], v[4] or 255 - -- normalise if in 0-255 range - if r > 1 or g > 1 or b > 1 then - r, g, b = r/255, g/255, b/255 - if a > 1 then a = a/255 end - end - return {r, g, b, a} - end - return nil -end - -local function parseDualDim(def) - --[[ - YAML formats accepted: - pos: [x, y] offset - size: [w, h] offset - scale-pos: [sx, sy] scale (0-1) - scale-size: [sw, sh] scale (0-1) - - Or shorthand flat: - x, y, w, h, sx, sy, sw, sh - ]] - local px = def.x or (def.pos and def.pos[1]) or 0 - local py = def.y or (def.pos and def.pos[2]) or 0 - local pw = def.w or def.width or (def.size and def.size[1]) or 0 - local ph = def.h or def.height or (def.size and def.size[2]) or 0 - local sx = def.sx or (def["scale-pos"] and def["scale-pos"][1]) or 0 - local sy = def.sy or (def["scale-pos"] and def["scale-pos"][2]) or 0 - local sw = def.sw or (def["scale-size"] and def["scale-size"][1]) or 0 - local sh = def.sh or (def["scale-size"] and def["scale-size"][2]) or 0 - return px, py, pw, ph, sx, sy, sw, sh -end - -local function applyShared(parent, obj, def) - -- Color / border - if def.color then obj.color = parseColor(def.color) end - if def["border-color"] then obj.borderColor = parseColor(def["border-color"]) end - if def["draw-border"] ~= nil then obj.drawBorder = def["draw-border"] end - - -- Visibility - if def.visible ~= nil then obj.visible = def.visible end - if def.active ~= nil then obj.active = def.active end - if def.visibility ~= nil then obj.visibility = def.visibility end - - -- Rotation - if def.rotation then obj.rotation = def.rotation end - - -- Tag - if def.tag then obj:tag(def.tag) end - - -- Tags (multi) - if def.tags then - for _, t in ipairs(def.tags) do obj:setTag(t) end - end - - -- Form factor - if def.form then - local f = def.form - if f == "circle" then - local x, y, w, h, sx, sy, sw = parseDualDim(def) - local r = def.radius or (w / 2) - obj:makeCircle(x, y, r, sx, sy, sw, def.segments) - elseif f == "arc" then - local x, y, w, h, sx, sy, sw = parseDualDim(def) - local r = def.radius or (w / 2) - obj:makeArc( - def["arc-type"] or "open", - x, y, r, sx, sy, sw, - def["angle-start"] or 0, - def["angle-end"] or math.pi * 2, - def.segments - ) - end - end - - -- Roundness - if def.roundness then - local r = def.roundness - if type(r) == "table" then - obj:setRoundness(r[1], r[2], r[3], r.side) - elseif type(r) == "string" then - -- "top" | "bottom" shorthand - obj:setRoundness(5, 5, 30, r) - else - obj:setRoundness(r, r, 30) - end - end - - -- Centering - if def["center-x"] then obj:centerX(def["center-x"]) end - if def["center-y"] then obj:centerY(def["center-y"]) end - - -- Full frame shorthand - if def["full-frame"] then obj:fullFrame() end - - -- Square lock - if def.square then obj.square = def.square end - - -- Dragging - if def.draggable then - obj:enableDragging( - type(def.draggable) == "number" and def.draggable or 1 - ) - end - - -- Hierarchy - if def["respect-hierarchy"] ~= nil then - obj:respectHierarchy(def["respect-hierarchy"]) - end - - -- Clip descendants - if def["clip-descendants"] ~= nil then - obj.clipDescendants = def["clip-descendants"] - end - - -- Effects (function reference by name — looked up via _G or a registry) - if def.effect then - local fn = type(def.effect) == "function" - and def.effect - or _G[def.effect] - if fn then obj.effect = fn end - end - - -- Shader (name → looked up in _G) - if def.shader then - obj.shader = type(def.shader) == "userdata" - and def.shader - or _G[def.shader] - end - - -- Position on stack - if def.stack then - if def.stack == "top" then obj:topStack() end - if def.stack == "bottom" then obj:bottomStack() end - end -end - -local function applyEvents(obj, def, env) - --[[ - Events in YAML can be: - on-pressed: "myFunction" -- looks up _G or env - on-pressed: | - print("hello") -- raw Lua string, loaded as chunk - ]] - local function resolve(v) - if type(v) == "function" then return v end - if type(v) == "string" then - -- Try global lookup first - if _G[v] and type(_G[v]) == "function" then return _G[v] end - -- Otherwise treat as Lua source - if env then - return env[v] - end - end - end - - local map = { - ["on-pressed"] = "OnPressed", - ["on-released"] = "OnReleased", - ["on-released-outer"] = "OnReleasedOuter", - ["on-pressed-outer"] = "OnPressedOuter", - ["on-enter"] = "OnEnter", - ["on-exit"] = "OnExit", - ["on-moved"] = "OnMoved", - ["on-drag-start"] = "OnDragStart", - ["on-dragging"] = "OnDragging", - ["on-drag-end"] = "OnDragEnd", - ["on-wheel"] = "OnWheelMoved", - ["on-size-changed"] = "OnSizeChanged", - ["on-position-changed"] = "OnPositionChanged", - ["on-destroy"] = "OnDestroy", - ["on-load"] = "OnLoad", - ["on-return"] = "OnReturn", -- textbox only - } - - for yaml_key, conn_key in pairs(map) do - if def[yaml_key] and obj[conn_key] then - local fn = resolve(def[yaml_key]) - if fn then obj[conn_key](fn) end - end - end - - -- on-update is special (not a connection) - if def["on-update"] then - local fn = resolve(def["on-update"]) - if fn then obj:OnUpdate(fn) end - end - - -- Hotkeys - if def.hotkeys then - for _, hk in ipairs(def.hotkeys) do - -- {keys: [lctrl, s], action: "mySaveFunction"} - local fn = resolve(hk.action) - if fn then obj:setHotKey(hk.keys)(fn) end - end - end -end - -local function applyTextProps(obj, def) - if def.text then obj.text = tostring(def.text) end - if def["text-color"] then obj.textColor = parseColor(def["text-color"]) end - if def["text-visibility"] then obj.textVisibility = def["text-visibility"] end - if def["text-scale"] then - obj.textScaleX = def["text-scale"][1] or 1 - obj.textScaleY = def["text-scale"][2] or 1 - end - if def["text-offset"] then - obj.textOffsetX = def["text-offset"][1] or 0 - obj.textOffsetY = def["text-offset"][2] or 0 - end - if def["text-shear"] then - obj.textShearingFactorX = def["text-shear"][1] or 0 - obj.textShearingFactorY = def["text-shear"][2] or 0 - end - - -- Alignment - local alignMap = {left = 1, center = 0, right = 2} - if def.align then - obj.align = alignMap[def.align] or 1 - end - - -- Font - if def.font then - local f = def.font - if type(f) == "number" then - obj:setFont(f) - elseif type(f) == "string" then - obj:setFont(f, def["font-size"]) - elseif type(f) == "table" then - -- {file: "fonts/roboto.ttf", size: 18} - obj:setFont(f.file or f[1], f.size or f[2]) - end - end - - -- fit-font: true | {min: 8, max: 200, scale: 1} - if def["fit-font"] then - local ff = def["fit-font"] - if ff == true then - obj:fitFont() - elseif type(ff) == "table" then - obj:fitFont(ff.min, ff.max, ff.scale and {scale=ff.scale} or nil) - end - end - - -- center-font: true | offset - if def["center-font"] then - local cf = def["center-font"] - obj:centerFont(type(cf) == "number" and cf or nil) - end -end - -local function applyImageProps(obj, def) - -- source: "path/to/image.png" - -- tile: [x, y, w, h] (optional sub-quad) - if def.source then - if def.tile then - local t = def.tile - obj:setImage(def.source, t[1], t[2], t[3], t[4]) - else - obj:setImage(def.source) - end - end - - if def["scale-x"] then obj.scaleX = def["scale-x"] end - if def["scale-y"] then obj.scaleY = def["scale-y"] end - if def["image-color"] then obj.imageColor = parseColor(def["image-color"]) end - if def["image-visibility"] then obj.imageVisibility = def["image-visibility"] end - - -- flip: "horizontal" | "vertical" | "both" - if def.flip then - local fl = def.flip - if fl == "horizontal" or fl == "both" then obj:flip(false) end - if fl == "vertical" or fl == "both" then obj:flip(true) end - end - - -- gradient shorthand - if def.gradient then - local g = def.gradient - -- {direction: "vertical", colors: [[r,g,b,a], ...]} - local colors = {} - for _, c in ipairs(g.colors) do - colors[#colors+1] = parseColor(c) - end - obj:applyGradient(g.direction or "vertical", table.unpack(colors)) - end -end - -local function applyVideoProps(obj, def) - if def.source then obj:setVideo(def.source) end - if def.volume then obj:setVolume(def.volume) end - if def.autoplay and def.autoplay then obj:play() end - if def["video-color"] then obj.videoColor = parseColor(def["video-color"]) end - if def["video-visibility"] then obj.videoVisibility = def["video-visibility"] end -end - --- ───────────────────────────────────────────── --- Core builder --- ───────────────────────────────────────────── - -local builders -- forward ref for recursion - -builders = { - ["frame"] = function(parent, def) - local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) - return parent:newFrame(x,y,w,h,sx,sy,sw,sh) - end, - ["virtual-frame"] = function(parent, def) - local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) - return parent:newVirtualFrame(x,y,w,h,sx,sy,sw,sh) - end, - ["visual-frame"] = function(parent, def) - local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) - return parent:newVisualFrame(x,y,w,h,sx,sy,sw,sh) - end, - ["label"] = function(parent, def) - local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) - return parent:newTextLabel(def.text or "", x,y,w,h,sx,sy,sw,sh) - end, - ["button"] = function(parent, def) - local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) - return parent:newTextButton(def.text or "", x,y,w,h,sx,sy,sw,sh) - end, - ["textbox"] = function(parent, def) - local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) - return parent:newTextBox(def.text or "", x,y,w,h,sx,sy,sw,sh) - end, - ["image"] = function(parent, def) - local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) - return parent:newImageLabel(def.source, x,y,w,h,sx,sy,sw,sh) - end, - ["image-button"] = function(parent, def) - local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) - return parent:newImageButton(def.source, x,y,w,h,sx,sy,sw,sh) - end, - ["video"] = function(parent, def) - local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) - return parent:newVideo(def.source, x,y,w,h,sx,sy,sw,sh) - end, -} - -local TEXT_TYPES = {label=true, button=true, textbox=true} -local IMAGE_TYPES = {image=true, ["image-button"]=true} -local VIDEO_TYPES = {video=true} - -function gui_yaml.build(parent, def, env) - --[[ - parent : gui element or gui root - def : parsed YAML table (one element) - env : optional Lua env table for event string resolution - - Returns the created object (or nil on unknown type). - ]] - local typ = def.type - if not typ then - error("gui_yaml: element missing 'type' field") - end - - local builder = builders[typ] - if not builder then - error("gui_yaml: unknown element type '" .. tostring(typ) .. "'") - end - - local obj = builder(parent, def) - - -- Apply shared properties - applyShared(parent, obj, def) - - -- Apply type-specific properties - if TEXT_TYPES[typ] then applyTextProps(obj, def) end - if IMAGE_TYPES[typ] then applyImageProps(obj, def) end - if VIDEO_TYPES[typ] then applyVideoProps(obj, def) end - - -- Events - applyEvents(obj, def, env) - - -- Recurse into children - if def.children then - for _, child_def in ipairs(def.children) do - gui_yaml.build(obj, child_def, env) - end - end - - return obj -end - -function gui_yaml.buildMany(parent, defs, env) - local results = {} - for _, def in ipairs(defs) do - results[#results+1] = gui_yaml.build(parent, def, env) - end - return results -end - --- Convenience: parse a YAML string and build in one call. --- Requires a YAML library. Tries tinyyaml, then lyaml. -function gui_yaml.fromString(parent, yaml_str, env) - local ok, yaml = pcall(require, "gui.yaml.tinyyaml") - if not ok then - ok, yaml = pcall(require, "lyaml") - if not ok then - error("gui_yaml.fromString: no YAML library found (tried tinyyaml, lyaml)") - end - end - local def = yaml.parse(yaml_str) - -- Support both single-element and list-of-elements at root - if def.type then - return gui_yaml.build(parent, def, env) - else - return gui_yaml.buildMany(parent, def, env) - end -end - -return gui_yaml \ No newline at end of file diff --git a/gui/yaml/tinyyaml.lua b/gui/yaml/tinyyaml.lua deleted file mode 100644 index 6d4bcba..0000000 --- a/gui/yaml/tinyyaml.lua +++ /dev/null @@ -1,776 +0,0 @@ -------------------------------------------------------------------------------- --- tinyyaml - YAML subset parser -------------------------------------------------------------------------------- - -local table = table -local string = string -local schar = string.char -local ssub, gsub = string.sub, string.gsub -local sfind, smatch = string.find, string.match -local tinsert, tremove = table.insert, table.remove -local setmetatable = setmetatable -local pairs = pairs -local type = type -local tonumber = tonumber -local math = math -local getmetatable = getmetatable -local error = error - -local UNESCAPES = { - ['0'] = "\x00", z = "\x00", N = "\x85", - a = "\x07", b = "\x08", t = "\x09", - n = "\x0a", v = "\x0b", f = "\x0c", - r = "\x0d", e = "\x1b", ['\\'] = '\\', -}; - -------------------------------------------------------------------------------- --- utils -local function select(list, pred) - local selected = {} - for i = 0, #list do - local v = list[i] - if v and pred(v, i) then - tinsert(selected, v) - end - end - return selected -end - -local function startswith(haystack, needle) - return ssub(haystack, 1, #needle) == needle -end - -local function ltrim(str) - return smatch(str, "^%s*(.-)$") -end - -local function rtrim(str) - return smatch(str, "^(.-)%s*$") -end - -------------------------------------------------------------------------------- --- Implementation. --- -local class = {__meta={}} -function class.__meta.__call(cls, ...) - local self = setmetatable({}, cls) - if cls.__init then - cls.__init(self, ...) - end - return self -end - -function class.def(base, typ, cls) - base = base or class - local mt = {__metatable=base, __index=base} - for k, v in pairs(base.__meta) do mt[k] = v end - cls = setmetatable(cls or {}, mt) - cls.__index = cls - cls.__metatable = cls - cls.__type = typ - cls.__meta = mt - return cls -end - - -local types = { - null = class:def('null'), - map = class:def('map'), - omap = class:def('omap'), - pairs = class:def('pairs'), - set = class:def('set'), - seq = class:def('seq'), - timestamp = class:def('timestamp'), -} - -local Null = types.null -function Null.__tostring() return 'yaml.null' end -function Null.isnull(v) - if v == nil then return true end - if type(v) == 'table' and getmetatable(v) == Null then return true end - return false -end -local null = Null() - -function types.timestamp:__init(y, m, d, h, i, s, f, z) - self.year = tonumber(y) - self.month = tonumber(m) - self.day = tonumber(d) - self.hour = tonumber(h or 0) - self.minute = tonumber(i or 0) - self.second = tonumber(s or 0) - if type(f) == 'string' and sfind(f, '^%d+$') then - self.fraction = tonumber(f) * math.pow(10, 3 - #f) - elseif f then - self.fraction = f - else - self.fraction = 0 - end - self.timezone = z -end - -function types.timestamp:__tostring() - return string.format( - '%04d-%02d-%02dT%02d:%02d:%02d.%03d%s', - self.year, self.month, self.day, - self.hour, self.minute, self.second, self.fraction, - self:gettz()) -end - -function types.timestamp:gettz() - if not self.timezone then - return '' - end - if self.timezone == 0 then - return 'Z' - end - local sign = self.timezone > 0 - local z = sign and self.timezone or -self.timezone - local zh = math.floor(z) - local zi = (z - zh) * 60 - return string.format( - '%s%02d:%02d', sign and '+' or '-', zh, zi) -end - - -local function countindent(line) - local _, j = sfind(line, '^%s+') - if not j then - return 0, line - end - return j, ssub(line, j+1) -end - -local function parsestring(line, stopper) - stopper = stopper or '' - local q = ssub(line, 1, 1) - if q == ' ' or q == '\t' then - return parsestring(ssub(line, 2)) - end - if q == "'" then - local i = sfind(line, "'", 2, true) - if not i then - return nil, line - end - return ssub(line, 2, i-1), ssub(line, i+1) - end - if q == '"' then - local i, buf = 2, '' - while i < #line do - local c = ssub(line, i, i) - if c == '\\' then - local n = ssub(line, i+1, i+1) - if UNESCAPES[n] ~= nil then - buf = buf..UNESCAPES[n] - elseif n == 'x' then - local h = ssub(i+2,i+3) - if sfind(h, '^[0-9a-fA-F]$') then - buf = buf..schar(tonumber(h, 16)) - i = i + 2 - else - buf = buf..'x' - end - else - buf = buf..n - end - i = i + 1 - elseif c == q then - break - else - buf = buf..c - end - i = i + 1 - end - return buf, ssub(line, i+1) - end - if q == '{' or q == '[' then -- flow style - return nil, line - end - if q == '|' or q == '>' then -- block - return nil, line - end - if q == '-' or q == ':' then - if ssub(line, 2, 2) == ' ' or #line == 1 then - return nil, line - end - end - local buf = '' - while #line > 0 do - local c = ssub(line, 1, 1) - if sfind(stopper, c, 1, true) then - break - elseif c == ':' and (ssub(line, 2, 2) == ' ' or #line == 1) then - break - elseif c == '#' and (ssub(buf, #buf, #buf) == ' ') then - break - else - buf = buf..c - end - line = ssub(line, 2) - end - return rtrim(buf), line -end - -local function isemptyline(line) - return line == '' or sfind(line, '^%s*$') or sfind(line, '^%s*#') -end - -local function equalsline(line, needle) - return startswith(line, needle) and isemptyline(ssub(line, #needle+1)) -end - -local function checkdupekey(map, key) - if map[key] ~= nil then - -- print("found a duplicate key '"..key.."' in line: "..line) - local suffix = 1 - while map[key..'_'..suffix] do - suffix = suffix + 1 - end - key = key ..'_'..suffix - end - return key -end - -local function parseflowstyle(line, lines) - local stack = {} - while true do - if #line == 0 then - if #lines == 0 then - break - else - line = tremove(lines, 1) - end - end - local c = ssub(line, 1, 1) - if c == '#' then - line = '' - elseif c == ' ' or c == '\t' or c == '\r' or c == '\n' then - line = ssub(line, 2) - elseif c == '{' or c == '[' then - tinsert(stack, {v={},t=c}) - line = ssub(line, 2) - elseif c == ':' then - local s = tremove(stack) - tinsert(stack, {v=s.v, t=':'}) - line = ssub(line, 2) - elseif c == ',' then - local value = tremove(stack) - if value.t == ':' or value.t == '{' or value.t == '[' then error() end - if stack[#stack].t == ':' then - -- map - local key = tremove(stack) - key.v = checkdupekey(stack[#stack].v, key.v) - stack[#stack].v[key.v] = value.v - elseif stack[#stack].t == '{' then - -- set - stack[#stack].v[value.v] = true - elseif stack[#stack].t == '[' then - -- seq - tinsert(stack[#stack].v, value.v) - end - line = ssub(line, 2) - elseif c == '}' then - if stack[#stack].t == '{' then - if #stack == 1 then break end - stack[#stack].t = '}' - line = ssub(line, 2) - else - line = ','..line - end - elseif c == ']' then - if stack[#stack].t == '[' then - if #stack == 1 then break end - stack[#stack].t = ']' - line = ssub(line, 2) - else - line = ','..line - end - else - local s, rest = parsestring(line, ',{}[]') - if not s then - error('invalid flowstyle line: '..line) - end - tinsert(stack, {v=s, t='s'}) - line = rest - end - end - return stack[1].v, line -end - -local function parseblockstylestring(line, lines, indent) - if #lines == 0 then - error("failed to find multi-line scalar content") - end - local s = {} - local firstindent = -1 - local endline = -1 - for i = 1, #lines do - local ln = lines[i] - local idt = countindent(ln) - if idt <= indent then - break - end - if ln == '' then - tinsert(s, '') - else - if firstindent == -1 then - firstindent = idt - elseif idt < firstindent then - break - end - tinsert(s, ssub(ln, firstindent + 1)) - end - endline = i - end - - local striptrailing = true - local sep = '\n' - local newlineatend = true - if line == '|' then - striptrailing = true - sep = '\n' - newlineatend = true - elseif line == '|+' then - striptrailing = false - sep = '\n' - newlineatend = true - elseif line == '|-' then - striptrailing = true - sep = '\n' - newlineatend = false - elseif line == '>' then - striptrailing = true - sep = ' ' - newlineatend = true - elseif line == '>+' then - striptrailing = false - sep = ' ' - newlineatend = true - elseif line == '>-' then - striptrailing = true - sep = ' ' - newlineatend = false - else - error('invalid blockstyle string:'..line) - end - local eonl = 0 - for i = #s, 1, -1 do - if s[i] == '' then - tremove(s, i) - eonl = eonl + 1 - end - end - if striptrailing then - eonl = 0 - end - if newlineatend then - eonl = eonl + 1 - end - for i = endline, 1, -1 do - tremove(lines, i) - end - return table.concat(s, sep)..string.rep('\n', eonl) -end - -local function parsetimestamp(line) - local _, p1, y, m, d = sfind(line, '^(%d%d%d%d)%-(%d%d)%-(%d%d)') - if not p1 then - return nil, line - end - if p1 == #line then - return types.timestamp(y, m, d), '' - end - local _, p2, h, i, s = sfind(line, '^[Tt ](%d+):(%d+):(%d+)', p1+1) - if not p2 then - return types.timestamp(y, m, d), ssub(line, p1+1) - end - if p2 == #line then - return types.timestamp(y, m, d, h, i, s), '' - end - local _, p3, f = sfind(line, '^%.(%d+)', p2+1) - if not p3 then - p3 = p2 - f = 0 - end - local zc = ssub(line, p3+1, p3+1) - local _, p4, zs, z = sfind(line, '^ ?([%+%-])(%d+)', p3+1) - if p4 then - z = tonumber(z) - local _, p5, zi = sfind(line, '^:(%d+)', p4+1) - if p5 then - z = z + tonumber(zi) / 60 - end - z = zs == '-' and -tonumber(z) or tonumber(z) - elseif zc == 'Z' then - p4 = p3 + 1 - z = 0 - else - p4 = p3 - z = false - end - return types.timestamp(y, m, d, h, i, s, f, z), ssub(line, p4+1) -end - -local function parsescalar(line, lines, indent) - line = ltrim(line) - line = gsub(line, '^%s*#.*$', '') -- comment only -> '' - line = gsub(line, '^%s*', '') -- trim head spaces - - if line == '' or line == '~' then - return null - end - - local ts, _ = parsetimestamp(line) - if ts then - return ts - end - - local s, _ = parsestring(line) - -- startswith quote ... string - -- not startswith quote ... maybe string - if s and (startswith(line, '"') or startswith(line, "'")) then - return s - end - - if startswith('!', line) then -- unexpected tagchar - error('unsupported line: '..line) - end - - if equalsline(line, '{}') then - return {} - end - if equalsline(line, '[]') then - return {} - end - - if startswith(line, '{') or startswith(line, '[') then - return parseflowstyle(line, lines) - end - - if startswith(line, '|') or startswith(line, '>') then - return parseblockstylestring(line, lines, indent) - end - - -- Regular unquoted string - line = gsub(line, '%s*#.*$', '') -- trim tail comment - local v = line - if v == 'null' or v == 'Null' or v == 'NULL'then - return null - elseif v == 'true' or v == 'True' or v == 'TRUE' then - return true - elseif v == 'false' or v == 'False' or v == 'FALSE' then - return false - elseif v == '.inf' or v == '.Inf' or v == '.INF' then - return math.huge - elseif v == '+.inf' or v == '+.Inf' or v == '+.INF' then - return math.huge - elseif v == '-.inf' or v == '-.Inf' or v == '-.INF' then - return -math.huge - elseif v == '.nan' or v == '.NaN' or v == '.NAN' then - return 0 / 0 - elseif sfind(v, '^[%+%-]?[0-9]+$') or sfind(v, '^[%+%-]?[0-9]+%.$')then - return tonumber(v) -- : int - elseif sfind(v, '^[%+%-]?[0-9]+%.[0-9]+$') then - return tonumber(v) - end - return s or v -end - -local parsemap; -- : func - -local function parseseq(line, lines, indent) - local seq = setmetatable({}, types.seq) - if line ~= '' then - error() - end - while #lines > 0 do - -- Check for a new document - line = lines[1] - if startswith(line, '---') then - while #lines > 0 and not startswith(lines, '---') do - tremove(lines, 1) - end - return seq - end - - -- Check the indent level - local level = countindent(line) - if level < indent then - return seq - elseif level > indent then - error("found bad indenting in line: ".. line) - end - - local i, j = sfind(line, '%-%s+') - if not i then - i, j = sfind(line, '%-$') - if not i then - return seq - end - end - local rest = ssub(line, j+1) - - if sfind(rest, '^[^\'\"%s]*:') then - -- Inline nested hash - local indent2 = j - lines[1] = string.rep(' ', indent2)..rest - tinsert(seq, parsemap('', lines, indent2)) - elseif sfind(rest, '^%-%s+') then - -- Inline nested seq - local indent2 = j - lines[1] = string.rep(' ', indent2)..rest - tinsert(seq, parseseq('', lines, indent2)) - elseif isemptyline(rest) then - tremove(lines, 1) - if #lines == 0 then - tinsert(seq, null) - return seq - end - if sfind(lines[1], '^%s*%-') then - local nextline = lines[1] - local indent2 = countindent(nextline) - if indent2 == indent then - -- Null seqay entry - tinsert(seq, null) - else - tinsert(seq, parseseq('', lines, indent2)) - end - else - -- - # comment - -- key: value - local nextline = lines[1] - local indent2 = countindent(nextline) - tinsert(seq, parsemap('', lines, indent2)) - end - elseif rest then - -- Array entry with a value - tremove(lines, 1) - tinsert(seq, parsescalar(rest, lines)) - end - end - return seq -end - -local function parseset(line, lines, indent) - if not isemptyline(line) then - error('not seq line: '..line) - end - local set = setmetatable({}, types.set) - while #lines > 0 do - -- Check for a new document - line = lines[1] - if startswith(line, '---') then - while #lines > 0 and not startswith(lines, '---') do - tremove(lines, 1) - end - return set - end - - -- Check the indent level - local level = countindent(line) - if level < indent then - return set - elseif level > indent then - error("found bad indenting in line: ".. line) - end - - local i, j = sfind(line, '%?%s+') - if not i then - i, j = sfind(line, '%?$') - if not i then - return set - end - end - local rest = ssub(line, j+1) - - if sfind(rest, '^[^\'\"%s]*:') then - -- Inline nested hash - local indent2 = j - lines[1] = string.rep(' ', indent2)..rest - set[parsemap('', lines, indent2)] = true - elseif sfind(rest, '^%s+$') then - tremove(lines, 1) - if #lines == 0 then - tinsert(set, null) - return set - end - if sfind(lines[1], '^%s*%?') then - local indent2 = countindent(lines[1]) - if indent2 == indent then - -- Null array entry - set[null] = true - else - set[parseseq('', lines, indent2)] = true - end - end - - elseif rest then - tremove(lines, 1) - set[parsescalar(rest, lines)] = true - else - error("failed to classify line: "..line) - end - end - return set -end - -function parsemap(line, lines, indent) - if not isemptyline(line) then - error('not map line: '..line) - end - local map = setmetatable({}, types.map) - while #lines > 0 do - -- Check for a new document - line = lines[1] - if startswith(line, '---') then - while #lines > 0 and not startswith(lines, '---') do - tremove(lines, 1) - end - return map - end - - -- Check the indent level - local level, _ = countindent(line) - if level < indent then - return map - elseif level > indent then - error("found bad indenting in line: ".. line) - end - - -- Find the key - local key - local s, rest = parsestring(line) - - -- Quoted keys - if s and startswith(rest, ':') then - local sc = parsescalar(s, {}, 0) - if sc and type(sc) ~= 'string' then - key = sc - else - key = s - end - line = ssub(rest, 2) - else - error("failed to classify line: "..line) - end - - key = checkdupekey(map, key) - line = ltrim(line) - - if ssub(line, 1, 1) == '!' then - -- ignore type - local rh = ltrim(ssub(line, 3)) - local typename = smatch(rh, '^!?[^%s]+') - line = ltrim(ssub(rh, #typename+1)) - end - - if not isemptyline(line) then - tremove(lines, 1) - line = ltrim(line) - map[key] = parsescalar(line, lines, indent) - else - -- An indent - tremove(lines, 1) - if #lines == 0 then - map[key] = null - return map; - end - if sfind(lines[1], '^%s*%-') then - local indent2 = countindent(lines[1]) - map[key] = parseseq('', lines, indent2) - elseif sfind(lines[1], '^%s*%?') then - local indent2 = countindent(lines[1]) - map[key] = parseset('', lines, indent2) - else - local indent2 = countindent(lines[1]) - if indent >= indent2 then - -- Null hash entry - map[key] = null - else - map[key] = parsemap('', lines, indent2) - end - end - end - end - return map -end - - --- : (list)->dict -local function parsedocuments(lines) - lines = select(lines, function(s) return not isemptyline(s) end) - - if sfind(lines[1], '^%%YAML') then tremove(lines, 1) end - - local root = {} - local in_document = false - while #lines > 0 do - local line = lines[1] - -- Do we have a document header? - local docright; - if sfind(line, '^%-%-%-') then - -- Handle scalar documents - docright = ssub(line, 4) - tremove(lines, 1) - in_document = true - end - if docright then - if (not sfind(docright, '^%s+$') and - not sfind(docright, '^%s+#')) then - tinsert(root, parsescalar(docright, lines)) - end - elseif #lines == 0 or startswith(line, '---') then - -- A naked document - tinsert(root, null) - while #lines > 0 and not sfind(lines[1], '---') do - tremove(lines, 1) - end - in_document = false - -- XXX The final '-+$' is to look for -- which ends up being an - -- error later. - elseif not in_document and #root > 0 then - -- only the first document can be explicit - error('parse error: '..line) - elseif sfind(line, '^%s*%-') then - -- An array at the root - tinsert(root, parseseq('', lines, 0)) - elseif sfind(line, '^%s*[^%s]') then - -- A hash at the root - local level = countindent(line) - tinsert(root, parsemap('', lines, level)) - else - -- Shouldn't get here. @lines have whitespace-only lines - -- stripped, and previous match is a line with any - -- non-whitespace. So this clause should only be reachable via - -- a perlbug where \s is not symmetric with \S - - -- uncoverable statement - error('parse error: '..line) - end - end - if #root > 1 and Null.isnull(root[1]) then - tremove(root, 1) - return root - end - return root -end - ---- Parse yaml string into table. -local function parse(source) - local lines = {} - for line in string.gmatch(source .. '\n', '(.-)\r?\n') do - tinsert(lines, line) - end - - local docs = parsedocuments(lines) - if #docs == 1 then - return docs[1] - end - - return docs -end - -return { - version = 0.1, - parse = parse, -} \ No newline at end of file diff --git a/gui/yaml/toyaml.lua b/gui/yaml/toyaml.lua deleted file mode 100644 index d157fd5..0000000 --- a/gui/yaml/toyaml.lua +++ /dev/null @@ -1,362 +0,0 @@ -local function colorToYaml(c) - if not c then return nil end - -- Check if it's a color object with hex method, otherwise use raw values - if type(c) == "table" then - if c.toHex then return c:toHex() end - -- Normalise to 0-255 for readability - local r = c[1] or 0 - local g = c[2] or 0 - local b = c[3] or 0 - local a = c[4] - if r <= 1 and g <= 1 and b <= 1 then - r, g, b = math.floor(r*255), math.floor(g*255), math.floor(b*255) - if a then a = math.floor(a*255) end - end - if a and a < 255 then - return string.format("[%d, %d, %d, %d]", r, g, b, a) - end - return string.format("[%d, %d, %d]", r, g, b) - end - return nil -end - -local function dualDimToYaml(obj) - local dd = obj.dualDim - local fields = {} - local op = dd.offset.pos - local os = dd.offset.size - local sp = dd.scale.pos - local ss = dd.scale.size - - if op.x ~= 0 then fields[#fields+1] = {"x", op.x} end - if op.y ~= 0 then fields[#fields+1] = {"y", op.y} end - if os.x ~= 0 then fields[#fields+1] = {"w", os.x} end - if os.y ~= 0 then fields[#fields+1] = {"h", os.y} end - if sp.x ~= 0 then fields[#fields+1] = {"sx", sp.x} end - if sp.y ~= 0 then fields[#fields+1] = {"sy", sp.y} end - if ss.x ~= 0 then fields[#fields+1] = {"sw", ss.x} end - if ss.y ~= 0 then fields[#fields+1] = {"sh", ss.y} end - return fields -end - -local bit = require("bit") -local band = bit.band -local frame, image, text, box, video, button, anim = 0, 1, 2, 4, 8, 16, 32 - -local function resolveTypeName(typ) - -- Match in specificity order (combined types first) - if typ == text + box then return "textbox" end - if typ == text + button then return "button" end - if typ == text + frame then return "label" end - if typ == image + frame then return "image" end - -- image+button uses frame internally in this lib - -- (newImageButton sets type = image+frame but adds cursor behaviour) - -- We detect image buttons by checking for cursor handler presence; - -- as a fallback we use "image" and the loader will still reconstruct it. - if band(typ, video) == video then return "video" end - if band(typ, image) == image then return "image" end - if typ == frame then return "frame" end - return "frame" -- safe fallback -end - -local alignNames = {[0]="center", [1]="left", [2]="right"} -local formNames = { - [1] = "rectangle", - [2] = "circle", - [3] = "arc", -} - --- YAML emitter — produces clean, human-readable YAML without a library dep. -local function emit(val, indent, visited) - indent = indent or 0 - visited = visited or {} - local pad = string.rep(" ", indent) - local t = type(val) - - if t == "boolean" then return tostring(val) end - if t == "number" then - -- Avoid scientific notation for small floats - if val == math.floor(val) then return string.format("%d", val) end - return string.format("%.6g", val) - end - if t == "string" then - -- Quote if contains special YAML chars or is empty - if val == "" or val:match("^[%s#&*!|>'\"%[%]{},?:-]") or val:match("[\n\r]") then - -- Escape inner quotes, wrap in double quotes - return '"' .. val:gsub('"', '\\"'):gsub("\n", "\\n") .. '"' - end - return val - end - if t ~= "table" then return tostring(val) end - - -- Cycle guard - if visited[val] then return '""' end - visited[val] = true - - -- Detect plain array (sequential integer keys starting at 1) - local isArray = true - local maxN = 0 - for k, _ in pairs(val) do - if type(k) ~= "number" or k ~= math.floor(k) or k < 1 then - isArray = false - break - end - if k > maxN then maxN = k end - end - if isArray and maxN ~= #val then isArray = false end - - local lines = {} - - if isArray then - -- Inline short numeric/string arrays on one line - local allScalar = true - for _, v in ipairs(val) do - if type(v) == "table" then allScalar = false; break end - end - if allScalar and #val <= 6 then - local parts = {} - for _, v in ipairs(val) do parts[#parts+1] = emit(v, 0, visited) end - visited[val] = nil - return "[" .. table.concat(parts, ", ") .. "]" - end - for _, v in ipairs(val) do - local rendered = emit(v, indent + 1, visited) - if type(v) == "table" then - lines[#lines+1] = pad .. "-\n" .. rendered - else - lines[#lines+1] = pad .. "- " .. rendered - end - end - else - for _, pair in ipairs(val) do - local k, v = pair[1], pair[2] - local rendered = emit(v, indent + 1, visited) - if type(v) == "table" and #v > 0 and type(v[1]) == "table" then - -- Nested block (list of pairs = mapping, or list of items) - lines[#lines+1] = pad .. k .. ":\n" .. rendered - elseif type(v) == "table" and type(v[1]) ~= "table" then - -- Inline array - lines[#lines+1] = pad .. k .. ": " .. rendered - else - lines[#lines+1] = pad .. k .. ": " .. rendered - end - end - end - - visited[val] = nil - return table.concat(lines, "\n") -end - --- ───────────────────────────────────────────────────────────────────────────── --- Main export function --- ───────────────────────────────────────────────────────────────────────────── - -local function guiToYaml(obj, opts) - --[[ - opts = { - indent = 0, -- starting indent level - skipDefaults = true, -- omit fields that equal their default values - includeChildren = true, -- recurse into children - eventNames = {}, -- map of connection object -> string name - -- e.g. {[obj.OnPressed] = "handlePress"} - } - --]] - opts = opts or {} - local skipDef = opts.skipDefaults ~= false -- default true - local inclChild = opts.includeChildren ~= false -- default true - local indent = opts.indent or 0 - local evNames = opts.eventNames or {} - - -- Ordered list of {key, value} pairs — order controls YAML output order - local fields = {} - local function add(k, v) - if v == nil then return end - if skipDef then - -- Skip booleans that match common defaults - if k == "visible" and v == true then return end - if k == "active" and v == true then return end - if k == "visibility" and v == 1 then return end - if k == "draw-border" and v == true then return end - if k == "rotation" and v == 0 then return end - if k == "align" and v == "left" then return end - if k == "text-visibility" and v == 1 then return end - if k == "image-visibility" and v == 1 then return end - if k == "video-visibility" and v == 1 then return end - if k == "scale-x" and v == 1 then return end - if k == "scale-y" and v == 1 then return end - end - fields[#fields+1] = {k, v} - end - - -- ── Type ────────────────────────────────────────────────────────── - add("type", resolveTypeName(obj.type)) - - -- ── Dual dimensions ─────────────────────────────────────────────── - for _, pair in ipairs(dualDimToYaml(obj)) do - add(pair[1], pair[2]) - end - - -- ── Shared appearance ───────────────────────────────────────────── - local col = colorToYaml(obj.color) - if col and col ~= "[142, 141, 141]" then -- skip library default grey - add("color", col) - end - local bcol = colorToYaml(obj.borderColor) - if bcol and bcol ~= "[0, 0, 0]" then - add("border-color", bcol) - end - add("draw-border", obj.drawBorder) - add("visible", obj.visible) - add("active", obj.active) - if obj.visibility ~= 1 then add("visibility", obj.visibility) end - if obj.rotation ~= 0 then add("rotation", obj.rotation) end - - -- ── Tag / tags ──────────────────────────────────────────────────── - if obj.__tag then add("tag", obj.__tag) end - if obj.tags then - local tagList = {} - for t, _ in pairs(obj.tags) do tagList[#tagList+1] = t end - if #tagList > 0 then add("tags", tagList) end - end - - -- ── Form factor ─────────────────────────────────────────────────── - local ff = obj.formFactor or 1 - if ff ~= 1 then -- skip default "rectangle" - add("form", formNames[ff] or "rectangle") - if obj.__radius then add("radius", obj.__radius) end - if obj.segments then add("segments", obj.segments) end - if ff == 3 then - add("arc-type", obj.arcType or "open") - add("angle-start", obj.__angleS) - add("angle-end", obj.__angleE) - end - end - - -- ── Roundness ───────────────────────────────────────────────────── - if obj.roundness then - local r = obj.roundness - if r == true then - -- generic — emit the rx/ry/segments triple - add("roundness", {obj.__rx or 5, obj.__ry or 5, obj.__segments or 30}) - elseif type(r) == "string" then - add("roundness", r) -- "top" or "bottom" - end - end - - -- ── Behaviour flags ─────────────────────────────────────────────── - if obj.clipDescendants then add("clip-descendants", true) end - if obj.square then add("square", obj.square) end - - -- ── Text-type fields ────────────────────────────────────────────── - if band(obj.type, text) == text then - if obj.text and obj.text ~= "" then add("text", obj.text) end - - local al = alignNames[obj.align] - add("align", al) - - local tc = colorToYaml(obj.textColor) - if tc and tc ~= "[0, 0, 0]" then add("text-color", tc) end - if obj.textVisibility ~= 1 then add("text-visibility", obj.textVisibility) end - - if obj.textScaleX ~= 1 or obj.textScaleY ~= 1 then - add("text-scale", {obj.textScaleX, obj.textScaleY}) - end - if obj.textOffsetX ~= 0 or obj.textOffsetY ~= 0 then - add("text-offset", {obj.textOffsetX, obj.textOffsetY}) - end - if obj.textShearingFactorX ~= 0 or obj.textShearingFactorY ~= 0 then - add("text-shear", {obj.textShearingFactorX, obj.textShearingFactorY}) - end - - -- Font: emit as {file, size} when a file path is known - if obj.font then - if obj.fontFile then - add("font", {{"file", obj.fontFile}, {"size", obj.font:getHeight()}}) - else - add("font", obj.font:getHeight()) - end - end - end - - -- ── Image-type fields ───────────────────────────────────────────── - if band(obj.type, image) == image and band(obj.type, video) ~= video then - -- Source path is stored via getSource() - local src = obj:getSource and obj:getSource() - if src then add("source", src) end - - if obj.scaleX ~= 1 then add("scale-x", obj.scaleX) end - if obj.scaleY ~= 1 then add("scale-y", obj.scaleY) end - - local ic = colorToYaml(obj.imageColor) - if ic and ic ~= "[255, 255, 255]" then add("image-color", ic) end - if obj.imageVisibility and obj.imageVisibility ~= 1 then - add("image-visibility", obj.imageVisibility) - end - end - - -- ── Video-type fields ───────────────────────────────────────────── - if band(obj.type, video) == video then - local src = obj:getSource and obj:getSource() - if src then add("source", src) end - - local vc = colorToYaml(obj.videoColor) - if vc and vc ~= "[255, 255, 255]" then add("video-color", vc) end - if obj.videoVisibility and obj.videoVisibility ~= 1 then - add("video-visibility", obj.videoVisibility) - end - if obj.audiosource then - add("volume", obj.audiosource:getVolume()) - end - if obj.playing then add("autoplay", true) end - end - - -- ── Events ──────────────────────────────────────────────────────── - -- We can only serialise events when the caller supplies a name map. - -- Otherwise we silently skip them (can't decompile closures). - local connMap = { - ["on-pressed"] = obj.OnPressed, - ["on-released"] = obj.OnReleased, - ["on-released-outer"] = obj.OnReleasedOuter, - ["on-pressed-outer"] = obj.OnPressedOuter, - ["on-enter"] = obj.OnEnter, - ["on-exit"] = obj.OnExit, - ["on-moved"] = obj.OnMoved, - ["on-drag-start"] = obj.OnDragStart, - ["on-dragging"] = obj.OnDragging, - ["on-drag-end"] = obj.OnDragEnd, - ["on-wheel"] = obj.OnWheelMoved, - ["on-size-changed"] = obj.OnSizeChanged, - ["on-position-changed"] = obj.OnPositionChanged, - ["on-destroy"] = obj.OnDestroy, - ["on-load"] = obj.OnLoad, - ["on-return"] = obj.OnReturn, - } - for yamlKey, conn in pairs(connMap) do - if conn and evNames[conn] then - add(yamlKey, evNames[conn]) - end - end - - -- ── Children ────────────────────────────────────────────────────── - if inclChild and obj.children and #obj.children > 0 then - local childDefs = {} - for _, child in ipairs(obj.children) do - -- Recurse, collect as ordered-pair tables for the emitter - local childFields = guiToYaml(child, { - skipDefaults = opts.skipDefaults, - includeChildren = opts.includeChildren, - eventNames = opts.eventNames, - _returnRaw = true, -- internal: return fields table, not string - }) - childDefs[#childDefs+1] = childFields - end - fields[#fields+1] = {"children", childDefs} - end - - -- Internal mode: return the raw ordered-pair table for parent to embed - if opts._returnRaw then return fields end - - return emit(fields, indent) -end - -return guiToYaml \ No newline at end of file