├── .gitignore ├── LICENSE.md ├── PenguinGUI.modinfo ├── README.md ├── Test.lua ├── hooks └── pre-commit ├── lib ├── inspect.lua └── profilerapi.lua ├── penguingui.lua ├── penguingui ├── Align.lua ├── Binding.lua ├── BindingFunctions.lua ├── Button.lua ├── CheckBox.lua ├── Component.lua ├── Frame.lua ├── GUI.lua ├── HorizontalLayout.lua ├── Image.lua ├── Label.lua ├── Line.lua ├── List.lua ├── Panel.lua ├── RadioButton.lua ├── Rectangle.lua ├── Slider.lua ├── TextButton.lua ├── TextField.lua ├── TextRadioButton.lua ├── Util.lua ├── VerticalLayout.lua └── testobject │ ├── ptguitestconsole.lua │ ├── ptguitestobject.frames │ ├── ptguitestobject.lua │ ├── ptguitestobject.object │ ├── ptguitestobject.png │ └── ptguitestobjecticon.png └── testconsole ├── consolebody.png └── consoleheader.png /.gitignore: -------------------------------------------------------------------------------- 1 | GPATH 2 | GTAGS 3 | GRTAGS 4 | .project 5 | .buildpath 6 | .settings/ 7 | 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /PenguinGUI.modinfo: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PenguinGUI", 3 | "version": "Beta v. Upbeat Giraffe", 4 | "path": ".", 5 | "dependencies": [], 6 | "metadata": { 7 | "author": "PenguinToast", 8 | "version": "0.4.1", 9 | "description": "Extensible object-oriented GUI library in LUA for canvases", 10 | "support_url": "http://penguintoast.github.io/PenguinGUI" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PenguinGUI 2 | ========== 3 | 4 | OO GUI library for Starbound lua canvases 5 | 6 | API Docs 7 | ========== 8 | API documentation is available here: http://penguintoast.github.io/PenguinGUI/doc/index.html 9 | 10 | Usage 11 | ========== 12 | See [penguingui/testobject/](penguingui/testobject/) and http://penguintoast.github.io/PenguinGUI for example usage 13 | 14 | Development 15 | ========== 16 | If you plan on forking the repository and developing, make sure you run 17 | ``` 18 | ln -s ../hooks .git/hooks 19 | ``` 20 | in the repository root to have it generate penguingui.lua every time you commit 21 | (Assuming you're on linux). 22 | -------------------------------------------------------------------------------- /Test.lua: -------------------------------------------------------------------------------- 1 | require "penguingui/Util" 2 | require "penguingui/Binding" 3 | require "penguingui/BindingFunctions" 4 | inspect = require "lib/inspect" 5 | 6 | function main() 7 | local a = Binding.proxy({ 8 | a_1 = 0 9 | }) 10 | local b = Binding.proxy({ 11 | b_1 = "" 12 | }) 13 | 14 | b:bind("b_1", Binding(a, "a_1")) 15 | a:bind("a_1", Binding(b, "b_1")) 16 | 17 | locals = {a = a, b = b} 18 | printTables() 19 | 20 | for i=0,20,1 do 21 | a.a_1 = tostring(i) 22 | print("a_1: " .. a.a_1 .. ", b_1: " .. b.b_1) 23 | end 24 | for i=0,20,1 do 25 | b.b_1 = tostring(i) 26 | print("a_1: " .. a.a_1 .. ", b_1: " .. b.b_1) 27 | end 28 | b:unbind("b_1") 29 | a:unbind("a_1") 30 | collectgarbage() 31 | printTables() 32 | end 33 | 34 | function printTables() 35 | print("Tables:") 36 | print(inspect(locals)) 37 | end 38 | main() 39 | -------------------------------------------------------------------------------- /hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | scripts=$(awk ' 4 | BEGIN {inLibrary=0;} 5 | { 6 | if (inLibrary == 1) { 7 | if ($1 != "return" && $1 != "}") { 8 | if ($1 == "end") { 9 | inLibrary=0; 10 | } else { 11 | script=substr($1,3) 12 | gsub(/"|,/, "", script) 13 | if (index(script, "lib/") == 0) { 14 | print script; 15 | } 16 | } 17 | } 18 | } else { 19 | if ($2 == "PtUtil.library()") { 20 | inLibrary=1; 21 | } 22 | } 23 | }' penguingui/Util.lua) 24 | 25 | grep "author\|version\|support" PenguinGUI.modinfo | sed 's/^\s*\(.*\),*$/-- \1/' > penguingui.lua 26 | echo -e "-- This script contains all the scripts in this library, so you only need to\n-- include this script for production purposes." >> penguingui.lua 27 | for filename in $scripts; do 28 | printf "\n" >> penguingui.lua 29 | for i in {1..80}; do printf "-" >> penguingui.lua; done 30 | printf "\n-- $(basename $filename)\n" >> penguingui.lua 31 | for i in {1..80}; do printf "-" >> penguingui.lua; done 32 | printf "\n\n" >> penguingui.lua 33 | grep -v -e "^\s*--" $filename >> penguingui.lua 34 | done 35 | 36 | git add penguingui.lua 37 | -------------------------------------------------------------------------------- /lib/inspect.lua: -------------------------------------------------------------------------------- 1 | inspect ={ 2 | _VERSION = 'inspect.lua 3.0.0', 3 | _URL = 'http://github.com/kikito/inspect.lua', 4 | _DESCRIPTION = 'human-readable representations of tables', 5 | _LICENSE = [[ 6 | MIT LICENSE 7 | 8 | Copyright (c) 2013 Enrique García Cota 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a 11 | copy of this software and associated documentation files (the 12 | "Software"), to deal in the Software without restriction, including 13 | without limitation the rights to use, copy, modify, merge, publish, 14 | distribute, sublicense, and/or sell copies of the Software, and to 15 | permit persons to whom the Software is furnished to do so, subject to 16 | the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included 19 | in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 22 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 24 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 26 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 27 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | ]] 29 | } 30 | 31 | inspect.KEY = setmetatable({}, {__tostring = function() return 'inspect.KEY' end}) 32 | inspect.METATABLE = setmetatable({}, {__tostring = function() return 'inspect.METATABLE' end}) 33 | 34 | -- Apostrophizes the string if it has quotes, but not aphostrophes 35 | -- Otherwise, it returns a regular quoted string 36 | local function smartQuote(str) 37 | if str:match('"') and not str:match("'") then 38 | return "'" .. str .. "'" 39 | end 40 | return '"' .. str:gsub('"', '\\"') .. '"' 41 | end 42 | 43 | local controlCharsTranslation = { 44 | ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", 45 | ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v" 46 | } 47 | 48 | local function escapeChar(c) return controlCharsTranslation[c] end 49 | 50 | local function escape(str) 51 | local result = str:gsub("\\", "\\\\"):gsub("(%c)", escapeChar) 52 | return result 53 | end 54 | 55 | local function isIdentifier(str) 56 | return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" ) 57 | end 58 | 59 | local function isSequenceKey(k, length) 60 | return type(k) == 'number' 61 | and 1 <= k 62 | and k <= length 63 | and math.floor(k) == k 64 | end 65 | 66 | local defaultTypeOrders = { 67 | ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4, 68 | ['function'] = 5, ['userdata'] = 6, ['thread'] = 7 69 | } 70 | 71 | local function sortKeys(a, b) 72 | local ta, tb = type(a), type(b) 73 | 74 | -- strings and numbers are sorted numerically/alphabetically 75 | if ta == tb and (ta == 'string' or ta == 'number') then return a < b end 76 | 77 | local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb] 78 | -- Two default types are compared according to the defaultTypeOrders table 79 | if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb] 80 | elseif dta then return true -- default types before custom ones 81 | elseif dtb then return false -- custom types after default ones 82 | end 83 | 84 | -- custom types are sorted out alphabetically 85 | return ta < tb 86 | end 87 | 88 | local function getNonSequentialKeys(t) 89 | local keys, length = {}, #t 90 | for k,_ in pairs(t) do 91 | if not isSequenceKey(k, length) then table.insert(keys, k) end 92 | end 93 | table.sort(keys, sortKeys) 94 | return keys 95 | end 96 | 97 | local function getToStringResultSafely(t, mt) 98 | local __tostring = type(mt) == 'table' and rawget(mt, '__tostring') 99 | local str, ok 100 | if type(__tostring) == 'function' then 101 | ok, str = pcall(__tostring, t) 102 | str = ok and str or 'error: ' .. tostring(str) 103 | end 104 | if type(str) == 'string' and #str > 0 then return str end 105 | end 106 | 107 | local maxIdsMetaTable = { 108 | __index = function(self, typeName) 109 | rawset(self, typeName, 0) 110 | return 0 111 | end 112 | } 113 | 114 | local idsMetaTable = { 115 | __index = function (self, typeName) 116 | local col = setmetatable({}, {__mode = "kv"}) 117 | rawset(self, typeName, col) 118 | return col 119 | end 120 | } 121 | 122 | local function countTableAppearances(t, tableAppearances) 123 | tableAppearances = tableAppearances or setmetatable({}, {__mode = "k"}) 124 | 125 | if type(t) == 'table' then 126 | if not tableAppearances[t] then 127 | tableAppearances[t] = 1 128 | for k,v in pairs(t) do 129 | countTableAppearances(k, tableAppearances) 130 | countTableAppearances(v, tableAppearances) 131 | end 132 | countTableAppearances(getmetatable(t), tableAppearances) 133 | else 134 | tableAppearances[t] = tableAppearances[t] + 1 135 | end 136 | end 137 | 138 | return tableAppearances 139 | end 140 | 141 | local copySequence = function(s) 142 | local copy, len = {}, #s 143 | for i=1, len do copy[i] = s[i] end 144 | return copy, len 145 | end 146 | 147 | local function makePath(path, ...) 148 | local keys = {...} 149 | local newPath, len = copySequence(path) 150 | for i=1, #keys do 151 | newPath[len + i] = keys[i] 152 | end 153 | return newPath 154 | end 155 | 156 | local function processRecursive(process, item, path) 157 | if item == nil then return nil end 158 | 159 | local processed = process(item, path) 160 | if type(processed) == 'table' then 161 | local processedCopy = {} 162 | local processedKey 163 | 164 | for k,v in pairs(processed) do 165 | processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY)) 166 | if processedKey ~= nil then 167 | processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey)) 168 | end 169 | end 170 | 171 | local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE)) 172 | setmetatable(processedCopy, mt) 173 | processed = processedCopy 174 | end 175 | return processed 176 | end 177 | 178 | 179 | ------------------------------------------------------------------- 180 | 181 | local Inspector = {} 182 | local Inspector_mt = {__index = Inspector} 183 | 184 | function Inspector:puts(...) 185 | local args = {...} 186 | local buffer = self.buffer 187 | local len = #buffer 188 | for i=1, #args do 189 | len = len + 1 190 | buffer[len] = tostring(args[i]) 191 | end 192 | end 193 | 194 | function Inspector:down(f) 195 | self.level = self.level + 1 196 | f() 197 | self.level = self.level - 1 198 | end 199 | 200 | function Inspector:tabify() 201 | self:puts(self.newline, string.rep(self.indent, self.level)) 202 | end 203 | 204 | function Inspector:alreadyVisited(v) 205 | return self.ids[type(v)][v] ~= nil 206 | end 207 | 208 | function Inspector:getId(v) 209 | local tv = type(v) 210 | local id = self.ids[tv][v] 211 | if not id then 212 | id = self.maxIds[tv] + 1 213 | self.maxIds[tv] = id 214 | self.ids[tv][v] = id 215 | end 216 | return id 217 | end 218 | 219 | function Inspector:putKey(k) 220 | if isIdentifier(k) then return self:puts(k) end 221 | self:puts("[") 222 | self:putValue(k) 223 | self:puts("]") 224 | end 225 | 226 | function Inspector:putTable(t) 227 | if t == inspect.KEY or t == inspect.METATABLE then 228 | self:puts(tostring(t)) 229 | elseif self:alreadyVisited(t) then 230 | self:puts('') 231 | elseif self.level >= self.depth then 232 | self:puts('{...}') 233 | else 234 | if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end 235 | 236 | local nonSequentialKeys = getNonSequentialKeys(t) 237 | local length = #t 238 | -- local mt = getmetatable(t) 239 | local toStringResult = getToStringResultSafely(t, mt) 240 | 241 | self:puts('{') 242 | self:down(function() 243 | if toStringResult then 244 | self:puts(' -- ', escape(toStringResult)) 245 | if length >= 1 then self:tabify() end 246 | end 247 | 248 | local count = 0 249 | for i=1, length do 250 | if count > 0 then self:puts(',') end 251 | self:puts(' ') 252 | self:putValue(t[i]) 253 | count = count + 1 254 | end 255 | 256 | for _,k in ipairs(nonSequentialKeys) do 257 | if count > 0 then self:puts(',') end 258 | self:tabify() 259 | self:putKey(k) 260 | self:puts(' = ') 261 | self:putValue(t[k]) 262 | count = count + 1 263 | end 264 | 265 | if mt then 266 | if count > 0 then self:puts(',') end 267 | self:tabify() 268 | self:puts(' = ') 269 | self:putValue(mt) 270 | end 271 | end) 272 | 273 | if #nonSequentialKeys > 0 or mt then -- result is multi-lined. Justify closing } 274 | self:tabify() 275 | elseif length > 0 then -- array tables have one extra space before closing } 276 | self:puts(' ') 277 | end 278 | 279 | self:puts('}') 280 | end 281 | end 282 | 283 | function Inspector:putValue(v) 284 | local tv = type(v) 285 | 286 | if tv == 'string' then 287 | self:puts(smartQuote(escape(v))) 288 | elseif tv == 'number' or tv == 'boolean' or tv == 'nil' then 289 | self:puts(tostring(v)) 290 | elseif tv == 'table' then 291 | self:putTable(v) 292 | else 293 | self:puts('<',tv,' ',self:getId(v),'>') 294 | end 295 | end 296 | 297 | ------------------------------------------------------------------- 298 | 299 | function inspect.inspect(root, options) 300 | options = options or {} 301 | 302 | local depth = options.depth or math.huge 303 | local newline = options.newline or '\n' 304 | local indent = options.indent or ' ' 305 | local process = options.process 306 | 307 | if process then 308 | root = processRecursive(process, root, {}) 309 | end 310 | 311 | local inspector = setmetatable({ 312 | depth = depth, 313 | buffer = {}, 314 | level = 0, 315 | ids = setmetatable({}, idsMetaTable), 316 | maxIds = setmetatable({}, maxIdsMetaTable), 317 | newline = newline, 318 | indent = indent, 319 | tableAppearances = countTableAppearances(root) 320 | }, Inspector_mt) 321 | 322 | inspector:putValue(root) 323 | 324 | return table.concat(inspector.buffer) 325 | end 326 | 327 | setmetatable(inspect, { __call = function(_, ...) return inspect.inspect(...) end }) 328 | 329 | -- return inspect 330 | 331 | -------------------------------------------------------------------------------- /lib/profilerapi.lua: -------------------------------------------------------------------------------- 1 | -- Credit to XspeedPL for writing this (I think) 2 | profilerApi = { 3 | hookedTables = {} 4 | } 5 | 6 | -- Initializes the Profiler and hooks all functions 7 | function profilerApi.init() 8 | if profilerApi.isInit then return end 9 | profilerApi.hooks = {} 10 | profilerApi.start = profilerApi.getTime() 11 | profilerApi.hookAll("", _ENV) 12 | profilerApi.isInit = true 13 | end 14 | 15 | -- Prints all collected data into the log ordered by total time descending 16 | function profilerApi.logData() 17 | local time = profilerApi.getTime() - profilerApi.start 18 | local arr = {} 19 | local len = 8 20 | local cnt = 5 21 | for k,v in pairs(profilerApi.hooks) do 22 | if type(v) == "table" then 23 | if v.t > 0 then 24 | table.insert(arr, k) 25 | local l = string.len(k) 26 | if l > len then len = l end 27 | l = profilerApi.get10pow(profilerApi.hooks[k].c) 28 | if l > cnt then cnt = l end 29 | end 30 | end 31 | end 32 | table.sort(arr, profilerApi.sortHelp) 33 | world.logInfo("Profiler log for console (total profiling time: " .. string.format("%.2f", time) .. ")") 34 | world.logInfo(string.format("%" .. len .. "s | total time | %" .. cnt .. "s | average time | last time", "function", "count")) 35 | for i,k in ipairs(arr) do 36 | local hook = profilerApi.hooks[k] 37 | world.logInfo(string.format("%" .. len .. "s | %.15f | %" .. cnt .. "i | %.15f | %.15f", k, hook.t, hook.c, hook.a, hook.e)) 38 | end 39 | end 40 | 41 | function profilerApi.get10pow(n) 42 | local ret = 1 43 | while n >= 10 do 44 | ret = ret + 1 45 | n = n / 10 46 | end 47 | return ret 48 | end 49 | 50 | function profilerApi.sortHelp(e1, e2) 51 | return profilerApi.hooks[e1].t > profilerApi.hooks[e2].t 52 | end 53 | 54 | function profilerApi.canHook(fn) 55 | local un = { "pairs", "ipairs", "ripairs", "type", "next", "assert", "error", 56 | "print", "setmetatable", "select", "rawset", "rawlen", "pcall", 57 | "unpack" } 58 | for i,v in ipairs(un) do 59 | if v == fn then return false end 60 | end 61 | return true 62 | end 63 | 64 | function profilerApi.hookAll(tn, to) 65 | if (tn == "profilerApi.") or (tn == "table.") or (tn == "coroutine.") or (tn == "os.") then return end 66 | local hookedTables = profilerApi.hookedTables 67 | for k,v in pairs(hookedTables) do 68 | if to == v then 69 | return 70 | end 71 | end 72 | table.insert(profilerApi.hookedTables, to) 73 | 74 | for k,v in pairs(to) do 75 | if type(v) == "function" then 76 | if (tn ~= "") or profilerApi.canHook(k) then 77 | profilerApi.hook(to, tn, v, k) 78 | end 79 | elseif type(v) == "table" then 80 | profilerApi.hookAll(tn .. k .. ".", v) 81 | end 82 | end 83 | end 84 | 85 | function profilerApi.getTime() 86 | return os.clock() 87 | end 88 | 89 | function profilerApi.hook(to, tn, fo, fn) 90 | local full = tn .. fn 91 | profilerApi.hooks[full] = { s = -1, f = fo, e = -1, t = 0, a = 0, c = 0 } 92 | to[fn] = function(...) return profilerApi.hooked(full, ...) end 93 | end 94 | 95 | function profilerApi.hooked(fn, ...) 96 | local hook = profilerApi.hooks[fn] 97 | hook.s = profilerApi.getTime() 98 | local ret = {hook.f(...)} 99 | hook.e = profilerApi.getTime() - hook.s 100 | hook.t = hook.t + hook.e 101 | hook.c = hook.c + 1 102 | hook.a = hook.t / hook.c 103 | return unpack(ret) 104 | end 105 | -------------------------------------------------------------------------------- /penguingui.lua: -------------------------------------------------------------------------------- 1 | -- "version": "Beta v. Upbeat Giraffe", 2 | -- "author": "PenguinToast", 3 | -- "version": "0.4.1", 4 | -- "support_url": "http://penguintoast.github.io/PenguinGUI" 5 | -- This script contains all the scripts in this library, so you only need to 6 | -- include this script for production purposes. 7 | 8 | -------------------------------------------------------------------------------- 9 | -- Util.lua 10 | -------------------------------------------------------------------------------- 11 | 12 | PtUtil = {} 13 | PtUtil.charWidths = {6, 6, 6, 6, 6, 6, 6, 6, 0, 0, 6, 6, 0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 4, 8, 12, 10, 12, 12, 4, 6, 6, 8, 8, 6, 8, 4, 12, 10, 6, 10, 10, 10, 10, 10, 10, 10, 10, 4, 4, 8, 8, 8, 10, 12, 10, 10, 8, 10, 8, 8, 10, 10, 8, 10, 10, 8, 12, 10, 10, 10, 10, 10, 10, 8, 10, 10, 12, 10, 10, 8, 6, 12, 6, 8, 10, 6, 10, 10, 9, 10, 10, 8, 10, 10, 4, 6, 9, 4, 12, 10, 10, 10, 10, 8, 10, 8, 10, 10, 12, 8, 10, 10, 8, 4, 8, 10, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 6, 10, 10, 15, 10, 5, 13, 7, 14, 15, 15, 10, 6, 14, 12, 16, 14, 7, 7, 6, 11, 12, 8, 7, 6, 16, 16, 15, 15, 15, 10, 10, 10, 10, 10, 10, 10, 14, 10, 8, 8, 8, 8, 8, 8, 8, 8, 13, 10, 10, 10, 10, 10, 10, 10, 13, 10, 10, 10, 10, 10, 14, 11, 10, 10, 10, 10, 10, 10, 15, 9, 10, 10, 10, 10, 8, 8, 8, 8, 12, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 15, 10} 14 | 15 | function PtUtil.library() 16 | return { 17 | "/penguingui/Util.lua", 18 | "/penguingui/Binding.lua", 19 | "/penguingui/BindingFunctions.lua", 20 | "/penguingui/GUI.lua", 21 | "/penguingui/Component.lua", 22 | "/penguingui/Line.lua", 23 | "/penguingui/Rectangle.lua", 24 | "/penguingui/Align.lua", 25 | "/penguingui/HorizontalLayout.lua", 26 | "/penguingui/VerticalLayout.lua", 27 | "/penguingui/Panel.lua", 28 | "/penguingui/Frame.lua", 29 | "/penguingui/Button.lua", 30 | "/penguingui/Label.lua", 31 | "/penguingui/TextButton.lua", 32 | "/penguingui/TextField.lua", 33 | "/penguingui/Image.lua", 34 | "/penguingui/CheckBox.lua", 35 | "/penguingui/RadioButton.lua", 36 | "/penguingui/TextRadioButton.lua", 37 | "/penguingui/Slider.lua", 38 | "/penguingui/List.lua", 39 | "/lib/profilerapi.lua", 40 | "/lib/inspect.lua" 41 | } 42 | end 43 | 44 | function PtUtil.drawText(text, options, fontSize, color) 45 | fontSize = fontSize or 16 46 | if text:byte() == 32 then -- If it starts with a space, offset the string 47 | local xOffset = PtUtil.getStringWidth(" ", fontSize) 48 | local oldX = options.position[1] 49 | options.position[1] = oldX + xOffset 50 | console.canvasDrawText(text, options, fontSize, color) 51 | options.position[1] = oldX 52 | else 53 | console.canvasDrawText(text, options, fontSize, color) 54 | end 55 | end 56 | 57 | function PtUtil.getStringWidth(text, fontSize) 58 | local widths = PtUtil.charWidths 59 | local scale = PtUtil.getFontScale(fontSize) 60 | local out = 0 61 | local len = #text 62 | for i=1,len,1 do 63 | out = out + widths[string.byte(text, i)] 64 | end 65 | return out * scale 66 | end 67 | 68 | function PtUtil.getFontScale(size) 69 | return size / 16 70 | end 71 | 72 | PtUtil.specialKeyMap = { 73 | [8] = "backspace", 74 | [13] = "enter", 75 | [127] = "delete", 76 | [275] = "right", 77 | [276] = "left", 78 | [278] = "home", 79 | [279] = "end", 80 | [301] = "capslock", 81 | [303] = "shift", 82 | [304] = "shift" 83 | } 84 | 85 | PtUtil.shiftKeyMap = { 86 | [39] = "\"", 87 | [44] = "<", 88 | [45] = "_", 89 | [46] = ">", 90 | [47] = "?", 91 | [48] = ")", 92 | [49] = "!", 93 | [50] = "@", 94 | [51] = "#", 95 | [52] = "$", 96 | [53] = "%", 97 | [54] = "^", 98 | [55] = "&", 99 | [56] = "*", 100 | [57] = "(", 101 | [59] = ":", 102 | [61] = "+", 103 | [91] = "{", 104 | [92] = "|", 105 | [93] = "}", 106 | [96] = "~" 107 | } 108 | 109 | function PtUtil.getKey(key, shift, capslock) 110 | if (capslock and not shift) or (shift and not capslock) then 111 | if key >= 97 and key <= 122 then 112 | return string.upper(string.char(key)) 113 | end 114 | end 115 | if shift and PtUtil.shiftKeyMap[key] then 116 | return PtUtil.shiftKeyMap[key] 117 | else 118 | if key >= 32 and key <= 122 then 119 | return string.char(key) 120 | elseif PtUtil.specialKeyMap[key] then 121 | return PtUtil.specialKeyMap[key] 122 | else 123 | return "unknown" 124 | end 125 | end 126 | end 127 | 128 | function PtUtil.fillRect(rect, color) 129 | console.canvasDrawRect(rect, color) 130 | end 131 | 132 | function PtUtil.fillPoly(poly, color) 133 | console.logInfo("fillPoly is not functional yet") 134 | end 135 | 136 | function PtUtil.drawLine(p1, p2, color, width) 137 | console.canvasDrawLine(p1, p2, color, width * 2) 138 | end 139 | 140 | function PtUtil.drawRect(rect, color, width) 141 | local minX = rect[1] + width / 2 142 | local minY = math.floor((rect[2] + width / 2) * 2) / 2 143 | local maxX = rect[3] - width / 2 144 | local maxY = math.floor((rect[4] - width / 2) * 2) / 2 145 | PtUtil.drawLine( 146 | {minX - width / 2, minY}, 147 | {maxX + width / 2, minY}, 148 | color, width 149 | ) 150 | PtUtil.drawLine( 151 | {maxX, minY}, 152 | {maxX, maxY}, 153 | color, width 154 | ) 155 | PtUtil.drawLine( 156 | {minX - width / 2, maxY}, 157 | {maxX + width / 2, maxY}, 158 | color, width 159 | ) 160 | PtUtil.drawLine( 161 | {minX, minY}, 162 | {minX, maxY}, 163 | color, width 164 | ) 165 | end 166 | 167 | function PtUtil.drawPoly(poly, color, width) 168 | for i=1,#poly - 1,1 do 169 | PtUtil.drawLine(poly[i], poly[i + 1], color, width) 170 | end 171 | PtUtil.drawLine(poly[#poly], poly[1], color, width) 172 | end 173 | 174 | function PtUtil.drawImage(image, position, scale) 175 | console.canvasDrawImage(image, position, scale) 176 | end 177 | 178 | function ripairs(t) 179 | local function ripairs_it(t,i) 180 | i=i-1 181 | local v=t[i] 182 | if v==nil then return v end 183 | return i,v 184 | end 185 | return ripairs_it, t, #t+1 186 | end 187 | 188 | function PtUtil.removeObject(t, o) 189 | for i,obj in ipairs(t) do 190 | if obj == o then 191 | table.remove(t, i) 192 | return i 193 | end 194 | end 195 | return -1 196 | end 197 | 198 | function class(...) 199 | local cls, bases = {}, {...} 200 | 201 | for i, base in ipairs(bases) do 202 | for k, v in pairs(base) do 203 | cls[k] = v 204 | end 205 | end 206 | 207 | cls.__index, cls.is_a = cls, {[cls] = true} 208 | for i, base in ipairs(bases) do 209 | for c in pairs(base.is_a) do 210 | cls.is_a[c] = true 211 | end 212 | cls.is_a[base] = true 213 | end 214 | setmetatable( 215 | cls, 216 | { 217 | __call = function (c, ...) 218 | local instance = setmetatable({}, c) 219 | instance = Binding.proxy(instance) 220 | 221 | local init = instance._init 222 | if init then init(instance, ...) end 223 | return instance 224 | end 225 | } 226 | ) 227 | return cls 228 | end 229 | 230 | function dump(value, indent, seen) 231 | if type(value) ~= "table" then 232 | if type(value) == "string" then 233 | return string.format('%q', value) 234 | else 235 | return tostring(value) 236 | end 237 | else 238 | if type(seen) ~= "table" then 239 | seen = {} 240 | elseif seen[value] then 241 | return "{...}" 242 | end 243 | seen[value] = true 244 | indent = indent or "" 245 | if next(value) == nil then 246 | return "{}" 247 | end 248 | local str = "{" 249 | local first = true 250 | for k,v in pairs(value) do 251 | if first then 252 | first = false 253 | else 254 | str = str.."," 255 | end 256 | str = str.."\n"..indent.." ".."["..dump(k, "", seen) 257 | .."] = "..dump(v, indent.." ", seen) 258 | end 259 | str = str.."\n"..indent.."}" 260 | return str 261 | end 262 | end 263 | 264 | -------------------------------------------------------------------------------- 265 | -- Binding.lua 266 | -------------------------------------------------------------------------------- 267 | 268 | Binding = setmetatable( 269 | {}, 270 | { 271 | __call = function(t, ...) 272 | return t.value(...) 273 | end 274 | } 275 | ) 276 | 277 | Binding.proxyTable = { 278 | __index = function(t, k) 279 | local out = t._instance[k] 280 | if out ~= nil then 281 | return out 282 | else 283 | return Binding.proxyTable[k] 284 | end 285 | end, 286 | __newindex = function(t, k, v) 287 | local instance = t._instance 288 | local old = instance[k] 289 | local new = v 290 | instance[k] = new 291 | if old ~= v then 292 | local listeners = instance.listeners 293 | if listeners and listeners[k] then 294 | local keyListeners = listeners[k] 295 | for _,keyListener in ipairs(keyListeners) do 296 | new = keyListener(t, k, old, new) or new 297 | end 298 | end 299 | local bindings = instance.bindings 300 | if bindings and bindings[k] then 301 | local keyBindings = bindings[k] 302 | for _,keyBinding in ipairs(keyBindings) do 303 | keyBinding:valueChanged(old, new) 304 | end 305 | end 306 | end 307 | end, 308 | __pairs = function(t) 309 | return pairs(t._instance) 310 | end, 311 | __ipairs = function(t) 312 | return ipairs(t._instance) 313 | end, 314 | __add = function(a, b) 315 | return a._instance + (type(b) == "table" and b._instance or b) 316 | end, 317 | __sub = function(a, b) 318 | return a._instance - (type(b) == "table" and b._instance or b) 319 | end, 320 | __mul = function(a, b) 321 | return a._instance * (type(b) == "table" and b._instance or b) 322 | end, 323 | __div = function(a, b) 324 | return a._instance / (type(b) == "table" and b._instance or b) 325 | end, 326 | __mod = function(a, b) 327 | return a._instance % (type(b) == "table" and b._instance or b) 328 | end, 329 | __pow = function(a, b) 330 | return a._instance ^ (type(b) == "table" and b._instance or b) 331 | end, 332 | __unm = function(a) 333 | return -a._instance 334 | end, 335 | __concat = function(a, b) 336 | return a._instance .. (type(b) == "table" and b._instance or b) 337 | end, 338 | __len = function(a) 339 | return #a._instance 340 | end, 341 | __eq = function(a, b) 342 | return a._instance == b._instance 343 | end, 344 | __lt = function(a, b) 345 | return a._instance < b._instance 346 | end, 347 | __le = function(a, b) 348 | return a._instance <= b._instance 349 | end, 350 | __call = function(t, ...) 351 | return t._instance(...) 352 | end 353 | } 354 | 355 | function Binding.isValue(object) 356 | return type(object) == "table" 357 | and getmetatable(object._instance) == Binding.valueTable 358 | end 359 | 360 | Binding.weakMetaTable = { 361 | __mode = "v" 362 | } 363 | 364 | Binding.valueTable = {} 365 | 366 | Binding.valueTable.__index = Binding.valueTable 367 | 368 | 369 | function Binding.valueTable:addValueListener(listener) 370 | return self:addListener("value", listener) 371 | end 372 | 373 | function Binding.valueTable:removeValueListener(listener) 374 | return self:removeListener("value", listener) 375 | end 376 | 377 | function Binding.valueTable:addValueBinding(binding) 378 | return self:addBinding("value", binding) 379 | end 380 | 381 | function Binding.valueTable:removeValueBinding(binding) 382 | return self:removeBinding("value", binding) 383 | end 384 | 385 | function Binding.valueTable:unbind() 386 | local bindings = self.bindings 387 | if bindings and bindings.value then 388 | local valueBindings = bindings.value 389 | for _,binding in ipairs(valueBindings) do 390 | binding:unbind() 391 | end 392 | end 393 | local boundto = self.boundto 394 | for _,bound in ipairs(boundto) do 395 | for _,boundTable in pairs(bound.bindings) do 396 | PtUtil.removeObject(boundTable, self) 397 | end 398 | end 399 | self.boundto = nil 400 | local bindTargets = self.bindTargets 401 | if bindTargets then 402 | for bindTarget,_ in pairs(bindTargets) do 403 | local bindTargetBoundto = bindTarget.boundto 404 | for key,binding in pairs(bindTargetBoundto) do 405 | if binding == self then 406 | bindTargetBoundto[key] = nil 407 | end 408 | end 409 | end 410 | end 411 | end 412 | 413 | 414 | function Binding.unbindChain(binding) 415 | Binding.valueTable.unbind(binding) 416 | local bindingTable = binding.bindingTable 417 | for i=1, #bindingTable - 1, 1 do 418 | bindingTable[i]:unbind() 419 | bindingTable[i] = nil 420 | end 421 | end 422 | 423 | function Binding.value(t, k) 424 | local out = Binding.proxy(setmetatable({}, Binding.valueTable)) 425 | if type(k) == "string" then -- Single key 426 | out.value = t[k] 427 | out.valueChanged = function(binding, old, new) 428 | binding.value = new 429 | end 430 | t:addBinding(k, out) 431 | out.boundto = {t} 432 | return out 433 | else -- Table of keys 434 | local numKeys = #k 435 | local currTable = t 436 | local bindingTable = {} 437 | for i=1, numKeys - 1, 1 do 438 | local currBinding = Binding.proxy(setmetatable({}, Binding.valueTable)) 439 | local currKey = k[i] 440 | local index = i 441 | 442 | currBinding.valueChanged = function(binding, old, new) 443 | if old == new then return end 444 | local oldTable = old 445 | local newTable = new 446 | local subKey 447 | local transplant 448 | for j=index + 1, numKeys, 1 do 449 | subKey = k[j] 450 | transplant = bindingTable[j] 451 | transplant.boundto[1] = newTable 452 | oldTable:removeBinding(subKey, transplant) 453 | newTable:addBinding(subKey, transplant) 454 | 455 | if j < numKeys then 456 | oldTable = oldTable[subKey] 457 | newTable = newTable[subKey] 458 | end 459 | end 460 | end 461 | currBinding.boundto = {currTable} 462 | currTable:addBinding(currKey, currBinding) 463 | bindingTable[index] = currBinding 464 | 465 | currTable = t[currKey] 466 | end 467 | out.valueChanged = function(binding, old, new) 468 | binding.value = new 469 | end 470 | out.bindingTable = bindingTable 471 | out.boundto = {currTable} 472 | out.unbind = Binding.unbindChain 473 | currTable:addBinding(k[numKeys], out) 474 | bindingTable[numKeys] = out 475 | return out 476 | end 477 | end 478 | 479 | 480 | function Binding.proxyTable:addListener(key, listener) 481 | local listeners = self.listeners 482 | if not listeners then 483 | listeners = {} 484 | self.listeners = listeners 485 | end 486 | local keyListeners = listeners[key] 487 | if not keyListeners then 488 | keyListeners = {} 489 | listeners[key] = keyListeners 490 | end 491 | table.insert(keyListeners, listener) 492 | return listener 493 | end 494 | 495 | function Binding.proxyTable:removeListener(key, listener) 496 | local keyListeners = self.listeners[key] 497 | return PtUtil.removeObject(keyListeners, listener) ~= -1 498 | end 499 | 500 | function Binding.proxyTable:addBinding(key, binding) 501 | local bindings = self.bindings 502 | if not bindings then 503 | bindings = {} 504 | self.bindings = bindings 505 | end 506 | local keyBindings = bindings[key] 507 | if not keyBindings then 508 | keyBindings = setmetatable({}, Binding.weakMetaTable) 509 | bindings[key] = keyBindings 510 | end 511 | table.insert(keyBindings, binding) 512 | binding:valueChanged(self[key], self[key]) 513 | return binding 514 | end 515 | 516 | function Binding.proxyTable:removeBinding(key, binding) 517 | local keyBindings = self.bindings[key] 518 | return PtUtil.removeObject(keyBindings, binding) ~= -1 519 | end 520 | 521 | 522 | function Binding.bind(target, key, value) 523 | local listener = function(t, k, old, new) 524 | target[key] = new 525 | end 526 | value:addValueListener(listener) 527 | 528 | local boundto = target.boundto 529 | if not boundto then 530 | boundto = {} 531 | target.boundto = boundto 532 | end 533 | local boundtoKey = boundto[key] 534 | assert(not boundtoKey, key .. " is already bound to another value") 535 | boundto[key] = value 536 | 537 | local bindTargets = value.bindTargets 538 | 539 | if not bindTargets then 540 | bindTargets = {} 541 | value.bindTargets = bindTargets 542 | end 543 | local bindKeyTargets = bindTargets[target] 544 | if not bindKeyTargets then 545 | bindKeyTargets = {} 546 | bindTargets[target] = bindKeyTargets 547 | end 548 | bindKeyTargets[key] = listener 549 | 550 | target[key] = value.value 551 | end 552 | 553 | Binding.proxyTable.bind = Binding.bind 554 | 555 | function Binding.bindBidirectional(t1, k1, t2, k2) 556 | t1:bind(k1, Binding(t2, k2)) 557 | t2:bind(k2, Binding(t1, k1)) 558 | end 559 | 560 | Binding.proxyTable.bindBidirectional = Binding.bindBidirectional 561 | 562 | function Binding.unbind(target, key) 563 | local binding = target.boundto[key] 564 | if binding then 565 | binding:removeValueListener(binding.bindTargets[target][key]) 566 | binding.bindTargets[target][key] = nil 567 | target.boundto[key] = nil 568 | end 569 | end 570 | 571 | Binding.proxyTable.unbind = Binding.unbind 572 | 573 | function Binding.proxy(instance) 574 | return setmetatable( 575 | {_instance = instance}, 576 | Binding.proxyTable 577 | ) 578 | end 579 | 580 | -------------------------------------------------------------------------------- 581 | -- BindingFunctions.lua 582 | -------------------------------------------------------------------------------- 583 | 584 | 585 | local createFunction = function(f) 586 | return function(...) 587 | local out = Binding.proxy(setmetatable({}, Binding.valueTable)) 588 | local getters = {} 589 | local boundto = {self} 590 | local args = table.pack(...) 591 | local numArgs = args.n 592 | for i = 1, numArgs, 1 do 593 | local value = args[i] 594 | local getter 595 | if Binding.isValue(value) then 596 | getter = function() 597 | return value.value 598 | end 599 | else 600 | getter = function() 601 | return value 602 | end 603 | end 604 | getters[i] = getter 605 | end 606 | out.valueChanged = function(binding, old, new) 607 | out.value = f(table.unpack(getters)) 608 | end 609 | for i = 1, numArgs, 1 do 610 | local value = args[i] 611 | if Binding.isValue(value) then 612 | value:addValueBinding(out) 613 | end 614 | table.insert(boundto, value) 615 | end 616 | out.boundto = boundto 617 | return out 618 | end 619 | end 620 | 621 | 622 | Binding.valueTable.tostring = createFunction( 623 | function(value) 624 | return tostring(value()) 625 | end 626 | ) 627 | Binding.tostring = Binding.valueTable.tostring 628 | 629 | Binding.valueTable.tonumber = createFunction( 630 | function(value) 631 | return tonumber(value()) 632 | end 633 | ) 634 | Binding.tonumber = Binding.valueTable.tonumber 635 | 636 | Binding.valueTable.add = createFunction( 637 | function(first, ...) 638 | local out = first() 639 | local args = table.pack(...) 640 | for _,value in ipairs(args) do 641 | out = out + value() 642 | end 643 | return out 644 | end 645 | ) 646 | Binding.add = Binding.valueTable.add 647 | 648 | Binding.valueTable.sub = createFunction( 649 | function(first, ...) 650 | local out = first() 651 | local args = table.pack(...) 652 | for _,value in ipairs(args) do 653 | out = out - value() 654 | end 655 | return out 656 | end 657 | ) 658 | Binding.sub = Binding.valueTable.sub 659 | 660 | Binding.valueTable.mul = createFunction( 661 | function(first, ...) 662 | local out = first() 663 | local args = table.pack(...) 664 | for _,value in ipairs(args) do 665 | out = out * value() 666 | end 667 | return out 668 | end 669 | ) 670 | Binding.mul = Binding.valueTable.mul 671 | 672 | Binding.valueTable.div = createFunction( 673 | function(first, ...) 674 | local out = first() 675 | local args = table.pack(...) 676 | for _,value in ipairs(args) do 677 | out = out / value() 678 | end 679 | return out 680 | end 681 | ) 682 | Binding.div = Binding.valueTable.div 683 | 684 | Binding.valueTable.mod = createFunction( 685 | function(first, ...) 686 | local out = first() 687 | local args = table.pack(...) 688 | for _,value in ipairs(args) do 689 | out = out % value() 690 | end 691 | return out 692 | end 693 | ) 694 | Binding.mod = Binding.valueTable.mod 695 | 696 | Binding.valueTable.pow = createFunction( 697 | function(first, ...) 698 | local out = first() 699 | local args = table.pack(...) 700 | for _,value in ipairs(args) do 701 | out = out ^ value() 702 | end 703 | return out 704 | end 705 | ) 706 | Binding.pow = Binding.valueTable.pow 707 | 708 | Binding.valueTable.negate = createFunction( 709 | function(value) 710 | return -value() 711 | end 712 | ) 713 | Binding.negate = Binding.valueTable.negate 714 | 715 | Binding.valueTable.concat = createFunction( 716 | function(first, ...) 717 | local out = first() 718 | local args = table.pack(...) 719 | for _,value in ipairs(args) do 720 | out = out .. value() 721 | end 722 | return out 723 | end 724 | ) 725 | Binding.concat = Binding.valueTable.concat 726 | 727 | Binding.valueTable.len = createFunction( 728 | function(value) 729 | return #value() 730 | end 731 | ) 732 | Binding.len = Binding.valueTable.len 733 | 734 | Binding.valueTable.eq = createFunction( 735 | function(a, b) 736 | return a() == b() 737 | end 738 | ) 739 | Binding.eq = Binding.valueTable.eq 740 | 741 | Binding.valueTable.ne = createFunction( 742 | function(a, b) 743 | return a() ~= b() 744 | end 745 | ) 746 | Binding.ne = Binding.valueTable.ne 747 | 748 | Binding.valueTable.lt = createFunction( 749 | function(a, b) 750 | return a() < b() 751 | end 752 | ) 753 | Binding.lt = Binding.valueTable.lt 754 | 755 | Binding.valueTable.gt = createFunction( 756 | function(a, b) 757 | return a() > b() 758 | end 759 | ) 760 | Binding.gt = Binding.valueTable.gt 761 | 762 | Binding.valueTable.le = createFunction( 763 | function(a, b) 764 | return a() <= b() 765 | end 766 | ) 767 | Binding.le = Binding.valueTable.le 768 | 769 | Binding.valueTable.ge = createFunction( 770 | function(a, b) 771 | return a() >= b() 772 | end 773 | ) 774 | Binding.ge = Binding.valueTable.ge 775 | 776 | Binding.valueTable.AND = createFunction( 777 | function(first, ...) 778 | local out = first() 779 | local args = table.pack(...) 780 | for _,value in ipairs(args) do 781 | out = out and value() 782 | end 783 | return out 784 | end 785 | ) 786 | Binding.AND = Binding.valueTable.AND 787 | 788 | Binding.valueTable.OR = createFunction( 789 | function(first, ...) 790 | local out = first() 791 | local args = table.pack(...) 792 | for _,value in ipairs(args) do 793 | out = out or value() 794 | end 795 | return out 796 | end 797 | ) 798 | Binding.OR = Binding.valueTable.OR 799 | 800 | Binding.valueTable.NOT = createFunction( 801 | function(value) 802 | return not value() 803 | end 804 | ) 805 | Binding.NOT = Binding.valueTable.NOT 806 | 807 | Binding.valueTable.THEN = function(self, ifTrue, ifFalse) 808 | local out = Binding.proxy(setmetatable({}, Binding.valueTable)) 809 | local trueFunction 810 | local falseFunction 811 | local boundto = {self} 812 | if Binding.isValue(ifTrue) then 813 | trueFunction = function() 814 | return ifTrue.value 815 | end 816 | else 817 | trueFunction = function() 818 | return ifTrue 819 | end 820 | end 821 | if Binding.isValue(ifFalse) then 822 | falseFunction = function() 823 | return ifFalse.value 824 | end 825 | else 826 | falseFunction = function() 827 | return ifFalse 828 | end 829 | end 830 | out.valueChanged = function(binding, old, new) 831 | if self.value then 832 | out.value = trueFunction() 833 | else 834 | out.value = falseFunction() 835 | end 836 | end 837 | self:addValueBinding(out) 838 | if Binding.isValue(ifTrue) then 839 | ifTrue:addValueBinding(out) 840 | table.insert(boundto, ifTrue) 841 | end 842 | if Binding.isValue(ifFalse) then 843 | ifFalse:addValueBinding(out) 844 | table.insert(boundto, ifFalse) 845 | end 846 | out.boundto = boundto 847 | return out 848 | end 849 | Binding.THEN = Binding.valueTable.THEN 850 | 851 | 852 | -------------------------------------------------------------------------------- 853 | -- GUI.lua 854 | -------------------------------------------------------------------------------- 855 | 856 | 857 | GUI = { 858 | components = {}, 859 | mouseState = {}, 860 | keyState = {}, 861 | mousePosition = {0, 0} 862 | } 863 | 864 | function GUI.add(component) 865 | GUI.components[#GUI.components + 1] = component 866 | component:setParent(nil) 867 | end 868 | 869 | function GUI.remove(component) 870 | for index,comp in ripairs(GUI.components) do 871 | if (comp == component) then 872 | component:removeSelf() 873 | return true 874 | end 875 | end 876 | return false 877 | end 878 | 879 | function GUI.setFocusedComponent(component) 880 | local focusedComponent = GUI.focusedComponent 881 | if focusedComponent then 882 | focusedComponent.hasFocus = false 883 | end 884 | GUI.focusedComponent = component 885 | component.hasFocus = true 886 | end 887 | 888 | function GUI.clickEvent(position, button, pressed) 889 | GUI.mouseState[button] = pressed 890 | local components = GUI.components 891 | local topFound = false 892 | for index,component in ripairs(components) do 893 | if component.visible ~= false then 894 | if not topFound then 895 | if component:contains(position) then 896 | table.remove(components, index) 897 | components[#components + 1] = component 898 | topFound = true 899 | end 900 | end 901 | if GUI.clickEventHelper(component, position, button, pressed) then 902 | break 903 | end 904 | end 905 | end 906 | end 907 | 908 | function GUI.clickEventHelper(component, position, button, pressed) 909 | local children = component.children 910 | for _,child in ripairs(children) do 911 | if child.visible ~= false then 912 | if GUI.clickEventHelper(child, position, button, pressed) then 913 | return true 914 | end 915 | end 916 | end 917 | if component.clickEvent then 918 | if component:contains(position) then 919 | if component:clickEvent(position, button, pressed) then 920 | GUI.setFocusedComponent(component) 921 | return true 922 | end 923 | end 924 | end 925 | end 926 | 927 | function GUI.keyEvent(key, pressed) 928 | GUI.keyState[key] = pressed 929 | local component = GUI.focusedComponent 930 | while component do 931 | if component.visible ~= false then 932 | local keyEvent = component.keyEvent 933 | if keyEvent then 934 | if keyEvent(component, key, pressed) then 935 | return 936 | end 937 | end 938 | end 939 | component = component.parent 940 | end 941 | end 942 | 943 | function GUI.step(dt) 944 | GUI.mousePosition = console.canvasMousePosition() 945 | local hoverComponent 946 | for _,component in ipairs(GUI.components) do 947 | if component.visible ~= false then 948 | hoverComponent = component:step(dt) or hoverComponent 949 | end 950 | end 951 | if hoverComponent then 952 | hoverComponent.mouseOver = true 953 | end 954 | end 955 | 956 | 957 | -------------------------------------------------------------------------------- 958 | -- Component.lua 959 | -------------------------------------------------------------------------------- 960 | 961 | Component = class() 962 | Component.x = 0 963 | Component.y = 0 964 | Component.width = 0 965 | Component.height = 0 966 | 967 | Component.mouseOver = nil 968 | 969 | Component.hasFocus = nil 970 | 971 | 972 | function Component:_init() 973 | self.children = {} 974 | self.offset = Binding.proxy({0, 0}) 975 | end 976 | 977 | 978 | function Component:add(child) 979 | local children = self.children 980 | children[#children + 1] = child 981 | child:setParent(self) 982 | end 983 | 984 | function Component:remove(child) 985 | local children = self.children 986 | for index,comp in ripairs(children) do 987 | if (comp == child) then 988 | child:removeSelf() 989 | return true 990 | end 991 | end 992 | return false 993 | end 994 | 995 | function Component:removeSelf() 996 | local siblings 997 | if self.parent then 998 | siblings = self.parent.children 999 | else 1000 | siblings = GUI.components 1001 | end 1002 | for index,sibling in ripairs(siblings) do 1003 | if sibling == self then 1004 | table.remove(siblings, index) 1005 | return 1006 | end 1007 | end 1008 | end 1009 | 1010 | function Component:pack(padding) 1011 | local width = 0 1012 | local height = 0 1013 | for _,child in ipairs(self.children) do 1014 | width = math.max(width, child.x + child.width) 1015 | height = math.max(height, child.y + child.height) 1016 | end 1017 | if padding == nil then 1018 | if self.width < width then 1019 | self.width = width 1020 | end 1021 | if self.height < height then 1022 | self.height = height 1023 | end 1024 | else 1025 | self.width = width + padding 1026 | self.height = height + padding 1027 | end 1028 | end 1029 | 1030 | function Component:step(dt) 1031 | local hoverComponent 1032 | if self.mouseOver ~= nil then 1033 | if self:contains(GUI.mousePosition) then 1034 | hoverComponent = self 1035 | else 1036 | self.mouseOver = false 1037 | end 1038 | end 1039 | 1040 | self:update(dt) 1041 | 1042 | local layout = self.layout 1043 | if layout then 1044 | self:calculateOffset() 1045 | self.layout = false 1046 | end 1047 | self:draw(dt) 1048 | 1049 | for _,child in ipairs(self.children) do 1050 | if layout then 1051 | child.layout = true 1052 | end 1053 | if child.visible ~= false then 1054 | hoverComponent = child:step(dt) or hoverComponent 1055 | end 1056 | end 1057 | return hoverComponent 1058 | end 1059 | 1060 | function Component:update(dt) 1061 | end 1062 | 1063 | function Component:draw(dt) 1064 | end 1065 | 1066 | function Component:setParent(parent) 1067 | self.parent = parent 1068 | if parent then 1069 | parent.layout = true 1070 | end 1071 | end 1072 | 1073 | function Component:calculateOffset() 1074 | local offset = self.offset 1075 | 1076 | local parent = self.parent 1077 | if parent then 1078 | offset[1] = parent.offset[1] + parent.x 1079 | offset[2] = parent.offset[2] + parent.y 1080 | else 1081 | offset[1] = 0 1082 | offset[2] = 0 1083 | end 1084 | 1085 | return offset 1086 | end 1087 | 1088 | function Component:contains(position) 1089 | local pos = {position[1] - self.offset[1], position[2] - self.offset[2]} 1090 | 1091 | if pos[1] >= self.x and pos[1] <= self.x + self.width 1092 | and pos[2] >= self.y and pos[2] <= self.y + self.height 1093 | then 1094 | return true 1095 | end 1096 | return false 1097 | end 1098 | 1099 | 1100 | 1101 | -------------------------------------------------------------------------------- 1102 | -- Line.lua 1103 | -------------------------------------------------------------------------------- 1104 | 1105 | Line = class(Component) 1106 | Line.color = {0, 0, 0} 1107 | Line.size = 1 1108 | 1109 | 1110 | function Line:_init(x, y, endX, endY, color, lineSize) 1111 | Component._init(self) 1112 | self.x = x 1113 | self.y = y 1114 | self.endX = x 1115 | self.endY = y 1116 | self.width = endX - x 1117 | self.height = endY - y 1118 | self.color = color 1119 | self.size = size 1120 | end 1121 | 1122 | 1123 | function Line:draw(dt) 1124 | local offset = self.offset 1125 | local startX = self.x + offset[1] 1126 | local startY = self.y + offset[2] 1127 | local endX = self.endX + offset[1] 1128 | local endY = self.endY + offset[2] 1129 | 1130 | local size = self.size 1131 | local color = self.color 1132 | PtUtil.drawLine({startX, startY}, {endX, endY}, color, width) 1133 | end 1134 | 1135 | 1136 | -------------------------------------------------------------------------------- 1137 | -- Rectangle.lua 1138 | -------------------------------------------------------------------------------- 1139 | 1140 | Rectangle = class(Component) 1141 | Rectangle.color = {0, 0, 0} 1142 | Rectangle.lineSize = nil 1143 | 1144 | 1145 | function Rectangle:_init(x, y, width, height, color, lineSize) 1146 | Component._init(self) 1147 | self.x = x 1148 | self.y = y 1149 | self.width = width 1150 | self.height = height 1151 | self.color = color 1152 | self.lineSize = lineSize 1153 | end 1154 | 1155 | 1156 | function Rectangle:draw(dt) 1157 | local startX = self.x + self.offset[1] 1158 | local startY = self.y + self.offset[2] 1159 | local w = self.width 1160 | local h = self.height 1161 | 1162 | local rect = {startX, startY, startX + w, startY + h} 1163 | local lineSize = self.lineSize 1164 | local color = self.color 1165 | if lineSize then 1166 | PtUtil.drawRect(rect, color, lineSize) 1167 | else 1168 | PtUtil.fillRect(rect, color) 1169 | end 1170 | end 1171 | 1172 | -------------------------------------------------------------------------------- 1173 | -- Align.lua 1174 | -------------------------------------------------------------------------------- 1175 | 1176 | Align = {} 1177 | 1178 | Align.LEFT = 0 1179 | Align.CENTER = 1 1180 | Align.RIGHT = 2 1181 | Align.TOP = 3 1182 | Align.BOTTOM = 4 1183 | 1184 | -------------------------------------------------------------------------------- 1185 | -- HorizontalLayout.lua 1186 | -------------------------------------------------------------------------------- 1187 | 1188 | HorizontalLayout = class() 1189 | 1190 | HorizontalLayout.padding = nil 1191 | HorizontalLayout.vAlignment = Align.CENTER 1192 | HorizontalLayout.hAlignment = Align.LEFT 1193 | 1194 | 1195 | function HorizontalLayout:_init(padding, hAlign, vAlign) 1196 | self.padding = padding or 0 1197 | if hAlign then 1198 | self.hAlignment = hAlign 1199 | end 1200 | if vAlign then 1201 | self.vAlignment = vAlign 1202 | end 1203 | end 1204 | 1205 | 1206 | function HorizontalLayout:layout() 1207 | local vAlign = self.vAlignment 1208 | local hAlign = self.hAlignment 1209 | local padding = self.padding 1210 | 1211 | local container = self.container 1212 | local components = container.children 1213 | local totalWidth = 0 1214 | for _,component in ipairs(components) do 1215 | totalWidth = totalWidth + component.width 1216 | end 1217 | totalWidth = totalWidth + (#components - 1) * padding 1218 | 1219 | local startX 1220 | if hAlign == Align.LEFT then 1221 | startX = 0 1222 | elseif hAlign == Align.CENTER then 1223 | startX = (container.width - totalWidth) / 2 1224 | else -- ALIGN_RIGHT 1225 | startX = container.width - totalWidth 1226 | end 1227 | 1228 | for _,component in ipairs(components) do 1229 | component.x = startX 1230 | if vAlign == Align.TOP then 1231 | component.y = container.height - component.height 1232 | elseif vAlign == Align.CENTER then 1233 | component.y = (container.height - component.height) / 2 1234 | else -- ALIGN_BOTTOM 1235 | component.y = 0 1236 | end 1237 | startX = startX + component.width + padding 1238 | end 1239 | end 1240 | 1241 | -------------------------------------------------------------------------------- 1242 | -- VerticalLayout.lua 1243 | -------------------------------------------------------------------------------- 1244 | 1245 | VerticalLayout = class() 1246 | 1247 | VerticalLayout.padding = nil 1248 | VerticalLayout.vAlignment = Align.TOP 1249 | VerticalLayout.hAlignment = Align.CENTER 1250 | 1251 | 1252 | function VerticalLayout:_init(padding, vAlign, hAlign) 1253 | self.padding = padding or 0 1254 | if hAlign then 1255 | self.hAlignment = hAlign 1256 | end 1257 | if vAlign then 1258 | self.vAlignment = vAlign 1259 | end 1260 | end 1261 | 1262 | 1263 | function VerticalLayout:layout() 1264 | local vAlign = self.vAlignment 1265 | local hAlign = self.hAlignment 1266 | local padding = self.padding 1267 | 1268 | local container = self.container 1269 | local components = container.children 1270 | local totalHeight = 0 1271 | for _,component in ipairs(components) do 1272 | totalHeight = totalHeight + component.height 1273 | end 1274 | totalHeight = totalHeight + (#components - 1) * padding 1275 | 1276 | local startY 1277 | if vAlign == Align.TOP then 1278 | startY = container.height 1279 | elseif vAlign == Align.CENTER then 1280 | startY = container.height - (container.height - totalHeight) / 2 1281 | else -- ALIGN_BOTTOM 1282 | startY = totalHeight 1283 | end 1284 | 1285 | for _,component in ipairs(components) do 1286 | component.y = startY - component.height 1287 | if hAlign == Align.LEFT then 1288 | component.x = 0 1289 | elseif hAlign == Align.CENTER then 1290 | component.x = (container.width - component.width) / 2 1291 | else -- ALIGN_RIGHT 1292 | component.x = container.width - component.width 1293 | end 1294 | startY = startY - (component.height + padding) 1295 | end 1296 | end 1297 | 1298 | -------------------------------------------------------------------------------- 1299 | -- Panel.lua 1300 | -------------------------------------------------------------------------------- 1301 | 1302 | Panel = class(Component) 1303 | 1304 | 1305 | function Panel:_init(x, y, width, height) 1306 | Component._init(self) 1307 | self.x = x 1308 | self.y = y 1309 | if width then 1310 | self.width = width 1311 | end 1312 | if height then 1313 | self.height = height 1314 | end 1315 | end 1316 | 1317 | 1318 | function Panel:add(child) 1319 | Component.add(self, child) 1320 | self:updateLayoutManager() 1321 | if not self.layoutManager then 1322 | self:pack() 1323 | end 1324 | end 1325 | 1326 | function Panel:setLayoutManager(layout) 1327 | self.layoutManager = layout 1328 | layout.container = self 1329 | self:updateLayoutManager() 1330 | end 1331 | 1332 | function Panel:updateLayoutManager() 1333 | local layout = self.layoutManager 1334 | if layout then 1335 | layout:layout() 1336 | end 1337 | end 1338 | 1339 | -------------------------------------------------------------------------------- 1340 | -- Frame.lua 1341 | -------------------------------------------------------------------------------- 1342 | 1343 | Frame = class(Panel) 1344 | Frame.borderColor = {0, 0, 0} 1345 | Frame.borderThickness = 1 1346 | Frame.backgroundColor = {35, 35, 35} 1347 | 1348 | 1349 | function Frame:_init(x, y) 1350 | Panel._init(self, x, y) 1351 | end 1352 | 1353 | 1354 | function Frame:update(dt) 1355 | if self.dragging then 1356 | if self.hasFocus then 1357 | local mousePos = GUI.mousePosition 1358 | self.x = self.x + (mousePos[1] - self.dragOrigin[1]) 1359 | self.y = self.y + (mousePos[2] - self.dragOrigin[2]) 1360 | self.layout = true 1361 | self.dragOrigin = mousePos 1362 | else 1363 | self.dragging = false 1364 | end 1365 | end 1366 | end 1367 | 1368 | function Frame:draw(dt) 1369 | local startX = self.x - self.offset[1] 1370 | local startY = self.y - self.offset[2] 1371 | local w = self.width 1372 | local h = self.height 1373 | local border = self.borderThickness 1374 | 1375 | local borderRect = { 1376 | startX, startY, 1377 | startX + w, startY + h 1378 | } 1379 | local backgroundRect = { 1380 | startX + border, startY + border, 1381 | startX + w - border, startY + h - border 1382 | } 1383 | 1384 | PtUtil.drawRect(borderRect, self.borderColor, border) 1385 | PtUtil.fillRect(backgroundRect, self.backgroundColor) 1386 | end 1387 | 1388 | function Frame:clickEvent(position, button, pressed) 1389 | if pressed then 1390 | self.dragging = true 1391 | self.dragOrigin = position 1392 | else 1393 | self.dragging = false 1394 | end 1395 | return true 1396 | end 1397 | 1398 | -------------------------------------------------------------------------------- 1399 | -- Button.lua 1400 | -------------------------------------------------------------------------------- 1401 | 1402 | Button = class(Component) 1403 | Button.outerBorderColor = {0, 0, 0} --black 1404 | Button.innerBorderColor = {84, 84, 84} --#545454 1405 | Button.innerBorderHoverColor = {147, 147, 147} --#939393 1406 | Button.color = {38, 38, 38} --#262626 1407 | Button.hoverColor = {84, 84, 84} --#545454 1408 | 1409 | 1410 | function Button:_init(x, y, width, height) 1411 | world.logInfo("Button init with "..x..","..y..","..width..","..height..".") 1412 | 1413 | Component._init(self) 1414 | self.mouseOver = false 1415 | 1416 | self.x = x 1417 | self.y = y 1418 | self.width = width 1419 | self.height = height 1420 | end 1421 | 1422 | 1423 | function Button:update(dt) 1424 | if self.pressed and not self.mouseOver then 1425 | self:setPressed(false) 1426 | end 1427 | end 1428 | 1429 | function Button:draw(dt) 1430 | local startX = self.x + self.offset[1] 1431 | local startY = self.y + self.offset[2] 1432 | local w = self.width 1433 | local h = self.height 1434 | 1435 | local borderPoly = { 1436 | {startX + 1, startY + 0.5}, 1437 | {startX + w - 1, startY + 0.5}, 1438 | {startX + w - 0.5, startY + 1}, 1439 | {startX + w - 0.5, startY + h - 1}, 1440 | {startX + w - 1, startY + h - 0.5}, 1441 | {startX + 1, startY + h - 0.5}, 1442 | {startX + 0.5, startY + h - 1}, 1443 | {startX + 0.5, startY + 1}, 1444 | } 1445 | local innerBorderRect = { 1446 | startX + 1, startY + 1, startX + w - 1, startY + h - 1 1447 | } 1448 | local rectOffset = 1.5 1449 | local rect = { 1450 | startX + rectOffset, startY + rectOffset, startX + w - rectOffset, startY + h - rectOffset 1451 | } 1452 | 1453 | world.logInfo("drawing now...") 1454 | 1455 | PtUtil.drawPoly(borderPoly, self.outerBorderColor, 1) 1456 | if self.mouseOver then 1457 | PtUtil.drawRect(innerBorderRect, self.innerBorderHoverColor, 0.5) 1458 | PtUtil.fillRect(rect, self.hoverColor) 1459 | else 1460 | PtUtil.drawRect(innerBorderRect, self.innerBorderColor, 0.5) 1461 | PtUtil.fillRect(rect, self.color) 1462 | end 1463 | end 1464 | 1465 | function Button:setPressed(pressed) 1466 | if pressed and not self.pressed then 1467 | self.x = self.x + 1 1468 | self.y = self.y - 1 1469 | self.layout = true 1470 | end 1471 | if not pressed and self.pressed then 1472 | self.x = self.x - 1 1473 | self.y = self.y + 1 1474 | self.layout = true 1475 | end 1476 | self.pressed = pressed 1477 | end 1478 | 1479 | function Button:clickEvent(position, button, pressed) 1480 | if button <= 3 then 1481 | if self.onClick and not pressed and self.pressed then 1482 | self:onClick(button) 1483 | end 1484 | self:setPressed(pressed) 1485 | return true 1486 | end 1487 | end 1488 | 1489 | 1490 | -------------------------------------------------------------------------------- 1491 | -- Label.lua 1492 | -------------------------------------------------------------------------------- 1493 | 1494 | Label = class(Component) 1495 | Label.text = nil 1496 | 1497 | 1498 | function Label:_init(x, y, text, fontSize, fontColor) 1499 | Component._init(self) 1500 | fontSize = fontSize or 10 1501 | self.fontSize = fontSize 1502 | self.fontColor = fontColor or {255, 255, 255} 1503 | self.text = text 1504 | self.x = x 1505 | self.y = y 1506 | self:addListener("text", self.recalculateBounds) 1507 | self:recalculateBounds() 1508 | end 1509 | 1510 | 1511 | function Label:recalculateBounds() 1512 | self.width = PtUtil.getStringWidth(self.text, self.fontSize) 1513 | self.height = self.fontSize 1514 | end 1515 | 1516 | function Label:draw(dt) 1517 | local startX = self.x + self.offset[1] 1518 | local startY = self.y + self.offset[2] 1519 | 1520 | PtUtil.drawText(self.text, { 1521 | position = {startX, startY}, 1522 | verticalAnchor = "bottom" 1523 | }, self.fontSize, self.fontColor) 1524 | end 1525 | 1526 | -------------------------------------------------------------------------------- 1527 | -- TextButton.lua 1528 | -------------------------------------------------------------------------------- 1529 | 1530 | TextButton = class(Button) 1531 | TextButton.text = nil 1532 | TextButton.textPadding = 2 1533 | 1534 | 1535 | function TextButton:_init(x, y, width, height, text, fontColor) 1536 | Button._init(self, x, y, width, height) 1537 | local padding = self.textPadding 1538 | local fontSize = height - padding * 2 1539 | local label = Label(0, padding, text, fontSize, fontColor) 1540 | self.text = text 1541 | 1542 | self.label = label 1543 | self:add(label) 1544 | 1545 | self:addListener( 1546 | "text", 1547 | function(t, k, old, new) 1548 | t.label.text = new 1549 | t:repositionLabel() 1550 | end 1551 | ) 1552 | 1553 | self:repositionLabel() 1554 | end 1555 | 1556 | 1557 | function TextButton:repositionLabel() 1558 | local label = self.label 1559 | local text = label.text 1560 | local padding = self.textPadding 1561 | local maxHeight = self.height - padding * 2 1562 | local maxWidth = self.width - padding * 2 1563 | if label.height < maxHeight then 1564 | label.fontSize = maxHeight 1565 | label:recalculateBounds() 1566 | end 1567 | while label.width > maxWidth do 1568 | label.fontSize = label.fontSize - 1 1569 | label:recalculateBounds() 1570 | end 1571 | label.x = (self.width - label.width) / 2 1572 | label.y = (self.height - label.height) / 2 1573 | end 1574 | 1575 | 1576 | -------------------------------------------------------------------------------- 1577 | -- TextField.lua 1578 | -------------------------------------------------------------------------------- 1579 | 1580 | TextField = class(Component) 1581 | TextField.vPadding = 3 1582 | TextField.hPadding = 4 1583 | TextField.borderColor = {84, 84, 84} 1584 | TextField.backgroundColor = {0, 0, 0} 1585 | TextField.textColor = {255, 255, 255} 1586 | TextField.textHoverColor = {153, 153, 153} 1587 | TextField.defaultTextColor = {51, 51, 51} 1588 | TextField.defaultTextHoverColor = {119, 119, 119} 1589 | TextField.cursorColor = {255, 255, 255} 1590 | TextField.cursorRate = 1 1591 | TextField.filter = nil 1592 | 1593 | TextField.repeatDelay = 0.5 1594 | TextField.repeatInterval = 0.05 1595 | 1596 | 1597 | function TextField:_init(x, y, width, height, defaultText) 1598 | Component._init(self) 1599 | self.x = x 1600 | self.y = y 1601 | self.width = width 1602 | self.height = height 1603 | self.fontSize = height - self.vPadding * 2 1604 | self.cursorPosition = 0 1605 | self.cursorX = 0 1606 | self.cursorTimer = self.cursorRate 1607 | self.text = "" 1608 | self.defaultText = defaultText 1609 | self.textOffset = 0 1610 | self.textClip = nil 1611 | self.mouseOver = false 1612 | self.keyTimes = {} 1613 | end 1614 | 1615 | 1616 | function TextField:update(dt) 1617 | if self.hasFocus then 1618 | local keyTimes = self.keyTimes 1619 | for key,dur in pairs(keyTimes) do 1620 | local time = dur + dt 1621 | keyTimes[key] = time 1622 | if time > self.repeatDelay + self.repeatInterval then 1623 | self:keyEvent(key, true) 1624 | keyTimes[key] = self.repeatDelay 1625 | end 1626 | end 1627 | 1628 | local timer = self.cursorTimer 1629 | local rate = self.cursorRate 1630 | timer = timer - dt 1631 | if timer < 0 then 1632 | timer = rate 1633 | end 1634 | self.cursorTimer = timer 1635 | end 1636 | end 1637 | 1638 | function TextField:draw(dt) 1639 | local startX = self.x + self.offset[1] 1640 | local startY = self.y + self.offset[2] 1641 | local w = self.width 1642 | local h = self.height 1643 | 1644 | local borderRect = { 1645 | startX, startY, startX + w, startY + h 1646 | } 1647 | local backgroundRect = { 1648 | startX + 1, startY + 1, startX + w - 1, startY + h - 1 1649 | } 1650 | PtUtil.fillRect(borderRect, self.borderColor) 1651 | PtUtil.fillRect(backgroundRect, self.backgroundColor) 1652 | 1653 | local text = self.text 1654 | local default = (text == "") and (self.defaultText ~= nil) 1655 | 1656 | local textColor 1657 | if self.mouseOver then 1658 | textColor = default and self.defaultTextHoverColor or self.textHoverColor 1659 | else 1660 | textColor = default and self.defaultTextColor or self.textColor 1661 | end 1662 | 1663 | local cursorPosition = self.cursorPosition 1664 | text = default and self.defaultText 1665 | or text:sub(self.textOffset + 1, self.textOffset 1666 | + (self.textClip or #text)) 1667 | 1668 | PtUtil.drawText(text, { 1669 | position = { 1670 | startX + self.hPadding, 1671 | startY + self.vPadding 1672 | }, 1673 | verticalAnchor = "bottom" 1674 | }, self.fontSize, textColor) 1675 | 1676 | if self.hasFocus then 1677 | local timer = self.cursorTimer 1678 | local rate = self.cursorRate 1679 | 1680 | if timer > rate / 2 then -- Draw cursor 1681 | local cursorX = startX + self.cursorX + self.hPadding 1682 | local cursorY = startY + self.vPadding 1683 | PtUtil.drawLine({cursorX, cursorY}, 1684 | {cursorX, cursorY + h - self.vPadding * 2}, 1685 | self.cursorColor, 1686 | 1) 1687 | end 1688 | end 1689 | end 1690 | 1691 | function TextField:setCursorPosition(pos) 1692 | if pos > #self.text then 1693 | pos = #self.text 1694 | end 1695 | self.cursorPosition = pos 1696 | 1697 | if pos < self.textOffset then 1698 | self.textOffset = pos 1699 | end 1700 | self:calculateTextClip() 1701 | local textClip = self.textClip 1702 | while (textClip) and (pos > self.textOffset + textClip) do 1703 | self.textOffset = self.textOffset + 1 1704 | self:calculateTextClip() 1705 | textClip = self.textClip 1706 | end 1707 | while self.textOffset > 0 and not textClip do 1708 | self.textOffset = self.textOffset - 1 1709 | self:calculateTextClip() 1710 | textClip = self.textClip 1711 | if textClip then 1712 | self.textOffset = self.textOffset + 1 1713 | self:calculateTextClip() 1714 | end 1715 | end 1716 | 1717 | local text = self.text 1718 | local cursorX = 0 1719 | for i=self.textOffset + 1,pos,1 do 1720 | local charWidth = PtUtil.getStringWidth(text:sub(i, i), self.fontSize) 1721 | cursorX = cursorX + charWidth 1722 | end 1723 | self.cursorX = cursorX 1724 | self.cursorTimer = self.cursorRate 1725 | end 1726 | 1727 | function TextField:calculateTextClip() 1728 | local maxX = self.width - self.hPadding * 2 1729 | local text = self.text 1730 | local totalWidth = 0 1731 | local startI = self.textOffset + 1 1732 | for i=startI,#text,1 do 1733 | totalWidth = totalWidth 1734 | + PtUtil.getStringWidth(text:sub(i, i), self.fontSize) 1735 | if totalWidth > maxX then 1736 | self.textClip = i - startI 1737 | return 1738 | end 1739 | end 1740 | self.textClip = nil 1741 | end 1742 | 1743 | function TextField:clickEvent(position, button, pressed) 1744 | if button <= 3 then 1745 | local xPos = position[1] - self.x - self.offset[1] - self.hPadding 1746 | 1747 | local text = self.text 1748 | local totalWidth = 0 1749 | for i=self.textOffset + 1,#text,1 do 1750 | local charWidth = PtUtil.getStringWidth(text:sub(i, i), self.fontSize) 1751 | if xPos < (totalWidth + charWidth * 0.6) then 1752 | self:setCursorPosition(i - 1) 1753 | return true 1754 | end 1755 | totalWidth = totalWidth + charWidth 1756 | end 1757 | self:setCursorPosition(#text) 1758 | 1759 | return true 1760 | end 1761 | end 1762 | 1763 | function TextField:keyEvent(keyCode, pressed) 1764 | if pressed then 1765 | self.keyTimes[keyCode] = self.keyTimes[keyCode] or 0 1766 | else 1767 | self.keyTimes[keyCode] = nil 1768 | end 1769 | 1770 | local keyState = GUI.keyState 1771 | if not pressed 1772 | or keyState[305] or keyState[306] 1773 | or keyState[307] or keyState[308] 1774 | then 1775 | return 1776 | end 1777 | 1778 | local shift = keyState[303] or keyState[304] 1779 | local caps = keyState[301] 1780 | local key = PtUtil.getKey(keyCode, shift, caps) 1781 | 1782 | local text = self.text 1783 | local cursorPos = self.cursorPosition 1784 | 1785 | local filter = self.filter 1786 | if #key == 1 then -- Type a character 1787 | text = text:sub(1, cursorPos) .. key .. text:sub(cursorPos + 1) 1788 | if filter then 1789 | if not text:match(filter) then 1790 | return true 1791 | end 1792 | end 1793 | self.text = text 1794 | self:setCursorPosition(cursorPos + 1) 1795 | else -- Special character 1796 | if key == "backspace" then 1797 | if cursorPos > 0 then 1798 | text = text:sub(1, cursorPos - 1) .. text:sub(cursorPos + 1) 1799 | if filter then 1800 | if not text:match(filter) then 1801 | return true 1802 | end 1803 | end 1804 | self.text = text 1805 | self:setCursorPosition(cursorPos - 1) 1806 | end 1807 | elseif key == "enter" then 1808 | if self.onEnter then 1809 | self:onEnter() 1810 | end 1811 | elseif key == "delete" then 1812 | if cursorPos < #text then 1813 | text = text:sub(1, cursorPos) .. text:sub(cursorPos + 2) 1814 | if filter then 1815 | if not text:match(filter) then 1816 | return true 1817 | end 1818 | end 1819 | self.text = text 1820 | end 1821 | elseif key == "right" then 1822 | self:setCursorPosition(math.min(cursorPos + 1, #text)) 1823 | elseif key == "left" then 1824 | self:setCursorPosition(math.max(0, cursorPos - 1)) 1825 | elseif key == "home" then 1826 | self:setCursorPosition(0) 1827 | elseif key == "end" then 1828 | self:setCursorPosition(#text) 1829 | end 1830 | end 1831 | return true 1832 | end 1833 | 1834 | 1835 | -------------------------------------------------------------------------------- 1836 | -- Image.lua 1837 | -------------------------------------------------------------------------------- 1838 | 1839 | Image = class(Component) 1840 | 1841 | 1842 | function Image:_init(x, y, image, scale) 1843 | Component._init(self) 1844 | scale = scale or 1 1845 | 1846 | self.x = x 1847 | self.y = y 1848 | local imageSize = root.imageSize(image) 1849 | self.width = imageSize[1] * scale 1850 | self.height = imageSize[2] * scale 1851 | self.image = image 1852 | self.scale = scale 1853 | end 1854 | 1855 | 1856 | function Image:draw(dt) 1857 | local startX = self.x + self.offset[1] 1858 | local startY = self.y + self.offset[2] 1859 | local image = self.image 1860 | local scale = self.scale 1861 | 1862 | PtUtil.drawImage(image, {startX, startY}, scale) 1863 | end 1864 | 1865 | -------------------------------------------------------------------------------- 1866 | -- CheckBox.lua 1867 | -------------------------------------------------------------------------------- 1868 | 1869 | CheckBox = class(Component) 1870 | CheckBox.borderColor = {84, 84, 84} 1871 | CheckBox.backgroundColor = {0, 0, 0} 1872 | CheckBox.hoverColor = {28, 28, 28} 1873 | CheckBox.checkColor = {197, 26, 11} 1874 | CheckBox.pressedColor = {52, 52, 52} 1875 | 1876 | 1877 | function CheckBox:_init(x, y, size) 1878 | Component._init(self) 1879 | self.mouseOver = false 1880 | 1881 | self.x = x 1882 | self.y = y 1883 | self.width = size 1884 | self.height = size 1885 | 1886 | self.selected = false 1887 | end 1888 | 1889 | 1890 | function CheckBox:update(dt) 1891 | if self.pressed and not self.mouseOver then 1892 | self.pressed = false 1893 | end 1894 | end 1895 | 1896 | function CheckBox:draw(dt) 1897 | local startX = self.x + self.offset[1] 1898 | local startY = self.y + self.offset[2] 1899 | local w = self.width 1900 | local h = self.height 1901 | 1902 | local borderRect = {startX, startY, startX + w, startY + h} 1903 | local rect = {startX + 1, startY + 1, startX + w - 1, startY + h - 1} 1904 | PtUtil.drawRect(borderRect, self.borderColor, 1) 1905 | 1906 | if self.pressed then 1907 | PtUtil.fillRect(rect, self.pressedColor) 1908 | elseif self.mouseOver then 1909 | PtUtil.fillRect(rect, self.hoverColor) 1910 | else 1911 | PtUtil.fillRect(rect, self.backgroundColor) 1912 | end 1913 | 1914 | if self.selected then 1915 | self:drawCheck(dt) 1916 | end 1917 | end 1918 | 1919 | function CheckBox:drawCheck(dt) 1920 | local startX = self.x + self.offset[1] 1921 | local startY = self.y + self.offset[2] 1922 | local w = self.width 1923 | local h = self.height 1924 | PtUtil.drawLine( 1925 | {startX + w / 4, startY + w / 2}, 1926 | {startX + w / 3, startY + h / 4}, 1927 | self.checkColor, 1) 1928 | PtUtil.drawLine( 1929 | {startX + w / 3, startY + h / 4}, 1930 | {startX + 3 * w / 4, startY + 3 * h / 4}, 1931 | self.checkColor, 1) 1932 | end 1933 | 1934 | function CheckBox:clickEvent(position, button, pressed) 1935 | if button <= 3 then 1936 | if not pressed and self.pressed then 1937 | self.selected = not self.selected 1938 | end 1939 | self.pressed = pressed 1940 | return true 1941 | end 1942 | end 1943 | 1944 | -------------------------------------------------------------------------------- 1945 | -- RadioButton.lua 1946 | -------------------------------------------------------------------------------- 1947 | 1948 | RadioButton = class(CheckBox) 1949 | 1950 | 1951 | 1952 | 1953 | function RadioButton:drawCheck(dt) 1954 | local startX = self.x + self.offset[1] 1955 | local startY = self.y + self.offset[2] 1956 | local w = self.width 1957 | local h = self.height 1958 | local checkRect = {startX + w / 4, startY + h / 4, 1959 | startX + 3 * w / 4, startY + 3 * h / 4} 1960 | PtUtil.fillRect(checkRect, self.checkColor) 1961 | end 1962 | 1963 | function RadioButton:select() 1964 | local siblings 1965 | if self.parent == nil then 1966 | siblings = GUI.components 1967 | else 1968 | siblings = self.parent.children 1969 | end 1970 | 1971 | local selectedButton 1972 | for _,sibling in ipairs(siblings) do 1973 | if sibling ~= self and sibling.is_a[RadioButton] 1974 | and sibling.selected 1975 | then 1976 | selectedButton = sibling 1977 | end 1978 | end 1979 | if selectedButton then 1980 | selectedButton.selected = false 1981 | end 1982 | 1983 | if not self.selected then 1984 | self.selected = true 1985 | end 1986 | end 1987 | 1988 | function RadioButton:setParent(parent) 1989 | Component.setParent(self, parent) 1990 | local siblings 1991 | if self.parent == nil then 1992 | siblings = GUI.components 1993 | else 1994 | siblings = self.parent.children 1995 | end 1996 | 1997 | for _,sibling in ipairs(siblings) do 1998 | if sibling ~= self and sibling.is_a[RadioButton] and sibling.selected then 1999 | return 2000 | end 2001 | end 2002 | self.selected = true 2003 | end 2004 | 2005 | function RadioButton:removeSelf() 2006 | CheckBox.removeSelf(self) 2007 | if self.selected then 2008 | local siblings 2009 | if self.parent == nil then 2010 | siblings = GUI.components 2011 | else 2012 | siblings = self.parent.children 2013 | end 2014 | 2015 | for _,sibling in ipairs(siblings) do 2016 | if sibling.is_a[RadioButton] then 2017 | sibling:select() 2018 | return 2019 | end 2020 | end 2021 | end 2022 | end 2023 | 2024 | function RadioButton:clickEvent(position, button, pressed) 2025 | if button <= 3 then 2026 | if not pressed and self.pressed then 2027 | self:select() 2028 | end 2029 | self.pressed = pressed 2030 | return true 2031 | end 2032 | end 2033 | 2034 | -------------------------------------------------------------------------------- 2035 | -- TextRadioButton.lua 2036 | -------------------------------------------------------------------------------- 2037 | 2038 | TextRadioButton = class(RadioButton) 2039 | TextRadioButton.hoverColor = {31, 31, 31} 2040 | TextRadioButton.pressedColor = {69, 69, 69} 2041 | TextRadioButton.checkColor = {52, 52, 52} 2042 | TextRadioButton.text = nil 2043 | TextRadioButton.textPadding = 2 2044 | 2045 | 2046 | function TextRadioButton:_init(x, y, width, height, text) 2047 | RadioButton._init(self, x, y, 0) 2048 | self.width = width 2049 | self.height = height 2050 | 2051 | local padding = self.textPadding 2052 | local fontSize = height - padding * 2 2053 | local label = Label(0, padding, text, fontSize, fontColor) 2054 | 2055 | self.label = label 2056 | self:add(label) 2057 | 2058 | self.text = text 2059 | self:addListener( 2060 | "text", 2061 | function(t, k, old, new) 2062 | t.label.text = new 2063 | t:repositionLabel() 2064 | end 2065 | ) 2066 | self:repositionLabel() 2067 | end 2068 | 2069 | 2070 | function TextRadioButton:drawCheck(dt) 2071 | local startX = self.x + self.offset[1] 2072 | local startY = self.y + self.offset[2] 2073 | local w = self.width 2074 | local h = self.height 2075 | local checkRect = {startX + 1, startY + 1, 2076 | startX + w - 1, startY + h - 1} 2077 | PtUtil.fillRect(checkRect, self.checkColor) 2078 | end 2079 | 2080 | TextRadioButton.repositionLabel = TextButton.repositionLabel 2081 | 2082 | -------------------------------------------------------------------------------- 2083 | -- Slider.lua 2084 | -------------------------------------------------------------------------------- 2085 | 2086 | Slider = class(Component) 2087 | Slider.lineColor = {0, 0, 0} 2088 | Slider.lineSize = 2 2089 | Slider.handleBorderColor = {177, 177, 177} 2090 | Slider.handleBorderSize = 1 2091 | Slider.handleColor = Slider.lineColor 2092 | Slider.handleHoverColor = {50, 50, 50} 2093 | Slider.handlePressedColor = {84, 84, 84} 2094 | Slider.handleSize = 5 2095 | Slider.value = nil 2096 | Slider.maxValue = nil 2097 | Slider.minValue = nil 2098 | 2099 | 2100 | function Slider:_init(x, y, width, height, min, max, step, vertical) 2101 | Component._init(self) 2102 | self.x = x 2103 | self.y = y 2104 | self.width = width 2105 | self.height = height 2106 | self.mouseOver = false 2107 | self.minValue = min or 0 2108 | self.maxValue = max or 1 2109 | self.valueStep = step 2110 | self.vertical = vertical 2111 | self.value = self.minValue 2112 | self:addListener( 2113 | "maxValue", 2114 | function(t, k, old, new) 2115 | if t.value > new then 2116 | t.value = new 2117 | end 2118 | end 2119 | ) 2120 | end 2121 | 2122 | 2123 | function Slider:update(dt) 2124 | if self.dragging then 2125 | if not GUI.mouseState[1] then 2126 | self.dragging = false 2127 | else 2128 | local mousePos = GUI.mousePosition 2129 | local lineSize = self.lineSize 2130 | local min = self.minValue 2131 | local max = self.maxValue 2132 | local len = max - min 2133 | local step = self.valueStep 2134 | local sliderValue 2135 | if self.vertical then 2136 | sliderValue = (mousePos[2] - self.dragOffset 2137 | - (self.y + self.offset[2] + lineSize) 2138 | ) / (self.height - lineSize * 2 - self.handleSize) * len 2139 | else 2140 | sliderValue = (mousePos[1] - self.dragOffset 2141 | - (self.x + self.offset[1] + lineSize) 2142 | ) / (self.width - lineSize * 2 - self.handleSize) * len 2143 | end 2144 | if sliderValue ~= sliderValue then -- sliderValue is NaN 2145 | sliderValue = 0 2146 | end 2147 | sliderValue = math.max(sliderValue, 0) 2148 | sliderValue = math.min(sliderValue, len) 2149 | if step then 2150 | local stepFreq = 1 / step 2151 | sliderValue = math.floor(sliderValue * stepFreq + 0.5) / stepFreq 2152 | end 2153 | sliderValue = sliderValue + min 2154 | self.value = sliderValue 2155 | end 2156 | end 2157 | if self.moving ~= nil then 2158 | if not GUI.mouseState[1] then 2159 | self.moving = nil 2160 | else 2161 | local step = self.valueStep 2162 | local direction = self.moving 2163 | local max = self.maxValue 2164 | local min = self.minValue 2165 | local len = max - min 2166 | if not step then 2167 | step = len / 100 2168 | end 2169 | local value = self.value 2170 | if direction then 2171 | self.value = math.min(value + step, max) 2172 | else 2173 | self.value = math.max(value - step, min) 2174 | end 2175 | end 2176 | end 2177 | end 2178 | 2179 | function Slider:draw(dt) 2180 | local startX = self.x + self.offset[1] 2181 | local startY = self.y + self.offset[2] 2182 | local w = self.width 2183 | local h = self.height 2184 | 2185 | local lineSize = self.lineSize 2186 | local lineColor = self.lineColor 2187 | local percentage = self:getPercentage() 2188 | local handleBorderSize = self.handleBorderSize 2189 | local handleSize = self.handleSize 2190 | local slidableLength 2191 | local handleBorderRect 2192 | local handleRect 2193 | if self.vertical then 2194 | PtUtil.drawLine({startX + w / 2, startY}, 2195 | {startX + w / 2, startY + h}, lineColor, lineSize) 2196 | PtUtil.drawLine({startX, startY + lineSize / 2} 2197 | , {startX + w, startY + lineSize / 2}, lineColor, lineSize) 2198 | PtUtil.drawLine({startX, startY + h - lineSize / 2} 2199 | , {startX + w, startY + h - lineSize / 2}, lineColor, lineSize) 2200 | 2201 | slidableLength = h - lineSize * 2 - handleSize 2202 | local sliderY = startY + lineSize + percentage * slidableLength 2203 | handleBorderRect = {startX, sliderY, startX + w, sliderY + handleSize} 2204 | handleRect = {startX + handleBorderSize, sliderY + handleBorderSize 2205 | , startX + w - handleBorderSize 2206 | , sliderY + handleSize - handleBorderSize} 2207 | else 2208 | PtUtil.drawLine({startX, startY + h / 2}, 2209 | {startX + w, startY + h / 2}, self.lineColor, self.lineSize) 2210 | PtUtil.drawLine({startX + lineSize / 2, startY}, 2211 | {startX + lineSize / 2, startY + h}, lineColor, lineSize) 2212 | PtUtil.drawLine({startX + w - lineSize / 2, startY}, 2213 | {startX + w - lineSize / 2, startY + h}, lineColor, lineSize) 2214 | 2215 | slidableLength = w - lineSize * 2 - handleSize 2216 | local sliderX = startX + lineSize + percentage * slidableLength 2217 | handleBorderRect = {sliderX, startY, sliderX + handleSize, startY + h} 2218 | handleRect = {sliderX + handleBorderSize, startY + handleBorderSize 2219 | , sliderX + handleSize - handleBorderSize 2220 | , startY + h - handleBorderSize} 2221 | end 2222 | PtUtil.drawRect(handleBorderRect, self.handleBorderColor, handleBorderSize) 2223 | local handleColor 2224 | if self.dragging then 2225 | handleColor = self.handlePressedColor 2226 | elseif self.mouseOver then 2227 | handleColor = self.handleHoverColor 2228 | else 2229 | handleColor = self.handleColor 2230 | end 2231 | PtUtil.fillRect(handleRect, handleColor) 2232 | end 2233 | 2234 | function Slider:getPercentage() 2235 | local min = self.minValue 2236 | local max = self.maxValue 2237 | local len = max - min 2238 | if len == 0 then 2239 | return 0 2240 | else 2241 | return (self.value - min) / len 2242 | end 2243 | end 2244 | 2245 | function Slider:clickEvent(position, button, pressed) 2246 | if button == 1 then -- Only react to LMB 2247 | if pressed then 2248 | local startX = self.x + self.offset[1] 2249 | local startY = self.y + self.offset[2] 2250 | local w = self.width 2251 | local h = self.height 2252 | 2253 | local lineSize = self.lineSize 2254 | local handleSize = self.handleSize 2255 | local percentage = self:getPercentage() 2256 | local handleX 2257 | local handleY 2258 | local handleWidth 2259 | local handleHeight 2260 | if self.vertical then 2261 | local slidableLength = h - lineSize * 2 - handleSize 2262 | handleX = startX 2263 | handleY = startY + lineSize + percentage * slidableLength 2264 | handleWidth = w 2265 | handleHeight = handleSize 2266 | else 2267 | local slidableLength = w - lineSize * 2 - handleSize 2268 | handleX = startX + lineSize + percentage * slidableLength 2269 | handleY = startY 2270 | handleWidth = handleSize 2271 | handleHeight = h 2272 | end 2273 | if position[1] >= handleX and position[1] <= handleX + handleWidth 2274 | and position[2] >= handleY and position[2] <= handleY + handleHeight 2275 | then 2276 | local dragOffset 2277 | if self.vertical then 2278 | dragOffset = position[2] - handleY 2279 | else 2280 | dragOffset = position[1] - handleX 2281 | end 2282 | self.dragOffset = dragOffset 2283 | self.dragging = true 2284 | else 2285 | if self.vertical then 2286 | if position[2] < handleY then 2287 | self.moving = false 2288 | else 2289 | self.moving = true 2290 | end 2291 | else 2292 | if position[1] < handleX then 2293 | self.moving = false 2294 | else 2295 | self.moving = true 2296 | end 2297 | end 2298 | end 2299 | end 2300 | return true 2301 | end 2302 | end 2303 | 2304 | -------------------------------------------------------------------------------- 2305 | -- List.lua 2306 | -------------------------------------------------------------------------------- 2307 | 2308 | List = class(Component) 2309 | List.borderColor = {84, 84, 84} 2310 | List.borderSize = 1 2311 | List.backgroundColor = {0, 0, 0} 2312 | List.itemPadding = 2 2313 | List.scrollBarSize = 3 2314 | 2315 | 2316 | function List:_init(x, y, width, height, itemSize, itemFactory, horizontal) 2317 | Component._init(self) 2318 | self.x = x 2319 | self.y = y 2320 | self.width = width 2321 | self.height = height 2322 | self.itemSize = itemSize 2323 | self.itemFactory = itemFactory or TextRadioButton 2324 | self.items = {} 2325 | self.topIndex = 1 2326 | self.bottomIndex = 1 2327 | self.itemCount = 0 2328 | self.horizontal = horizontal 2329 | self.mouseOver = false 2330 | 2331 | local borderSize = self.borderSize 2332 | local barSize = self.scrollBarSize 2333 | local slider 2334 | if horizontal then 2335 | slider = Slider(borderSize + 0.5, borderSize + 0.5 2336 | , width - borderSize * 2 - 1, barSize, 0, 0, 1, false) 2337 | else 2338 | slider = Slider(width - borderSize - barSize - 0.5 2339 | , borderSize + 0.5, barSize 2340 | , height - borderSize * 2 - 1, 0, 0, 1, true) 2341 | end 2342 | slider.lineSize = 0 2343 | slider.handleBorderSize = 0 2344 | slider.handleColor = {84, 84, 84} 2345 | slider.handleHoverColor = {120, 120, 120} 2346 | slider.handlePressedColor = {160, 160, 160} 2347 | slider:addListener( 2348 | "value", 2349 | function(t, k, old, new) 2350 | local list = t.parent 2351 | if list.horizontal then 2352 | list.topIndex = new + 1 2353 | else 2354 | list.topIndex = t.maxValue - t.value + 1 2355 | end 2356 | list:positionItems() 2357 | end 2358 | ) 2359 | self.slider = slider 2360 | self:add(slider) 2361 | 2362 | self:positionItems() 2363 | end 2364 | 2365 | 2366 | function List:draw(dt) 2367 | local startX = self.x + self.offset[1] 2368 | local startY = self.y + self.offset[2] 2369 | local w = self.width 2370 | local h = self.height 2371 | 2372 | local borderSize = self.borderSize 2373 | local borderColor = self.borderColor 2374 | local borderRect = {startX, startY, startX + w, startY + h} 2375 | local rect = {startX + 1, startY + 1, startX + w - 1, startY + h - 1} 2376 | PtUtil.drawRect(borderRect, borderColor, borderSize) 2377 | PtUtil.fillRect(rect, self.backgroundColor) 2378 | 2379 | local scrollBarSize = self.scrollBarSize 2380 | if self.horizontal then 2381 | local lineY = startY + borderSize + scrollBarSize + 1.5 2382 | PtUtil.drawLine({startX, lineY}, {startX + w, lineY}, borderColor, 1) 2383 | else 2384 | local lineX = startX + w - borderSize - scrollBarSize - 1.5 2385 | PtUtil.drawLine({lineX, startY}, {lineX, startY + h}, borderColor, 1) 2386 | end 2387 | end 2388 | 2389 | function List:emplaceItem(...) 2390 | local width 2391 | local height 2392 | if self.horizontal then 2393 | width = self.itemSize 2394 | height = self.height - (self.borderSize * 2 2395 | + self.itemPadding * 2 2396 | + self.scrollBarSize + 2) 2397 | else 2398 | width = self.width - (self.borderSize * 2 2399 | + self.itemPadding * 2 2400 | + self.scrollBarSize + 2) 2401 | height = self.itemSize 2402 | end 2403 | item = self.itemFactory(0, 0, width, height, ...) 2404 | return self:addItem(item) 2405 | end 2406 | 2407 | function List:addItem(item) 2408 | self:add(item) 2409 | local items = self.items 2410 | local index = #items + 1 2411 | items[index] = item 2412 | self.itemCount = self.itemCount + 1 2413 | self:positionItems() 2414 | return item, index 2415 | end 2416 | 2417 | function List:removeItem(target) 2418 | local item 2419 | local index 2420 | if type(target) == "number" then -- Remove by index 2421 | index = target 2422 | item = table.remove(self.items, index) 2423 | if not item then 2424 | return nil, -1 2425 | end 2426 | else -- Remove by item 2427 | if target == nil then 2428 | return nil 2429 | end 2430 | item = target 2431 | index = PtUtil.removeObject(self.items, item) 2432 | if index == -1 then 2433 | return nil, -1 2434 | end 2435 | end 2436 | self:remove(item) 2437 | if not item.filtered then 2438 | self.itemCount = self.itemCount - 1 2439 | end 2440 | if self.bottomIndex > self.itemCount + 1 then 2441 | self:scroll(true) 2442 | else 2443 | self:positionItems() 2444 | end 2445 | return item, index 2446 | end 2447 | 2448 | function List:clearItems() 2449 | for index,item in ripairs(self.items) do 2450 | self:removeItem(index) 2451 | end 2452 | end 2453 | 2454 | function List:getItem(index) 2455 | return self.items[index] 2456 | end 2457 | 2458 | function List:indexOfItem(item) 2459 | for index,obj in ipairs(self.items) do 2460 | if item == obj then 2461 | return index 2462 | end 2463 | end 2464 | return -1 2465 | end 2466 | 2467 | function List:filter(filter) 2468 | local itemCount = 0 2469 | if filter then 2470 | for _,item in ipairs(self.items) do 2471 | if not filter(item) then 2472 | item.filtered = true 2473 | else 2474 | itemCount = itemCount + 1 2475 | item.filtered = nil 2476 | end 2477 | end 2478 | else 2479 | for _,item in ipairs(self.items) do 2480 | itemCount = itemCount + 1 2481 | item.filtered = nil 2482 | end 2483 | end 2484 | self.itemCount = itemCount 2485 | self.topIndex = 1 2486 | self:positionItems() 2487 | end 2488 | 2489 | function List:positionItems() 2490 | local items = self.items 2491 | local padding = self.itemPadding 2492 | local border = self.borderSize 2493 | local topIndex = self.topIndex 2494 | local itemSize = self.itemSize 2495 | local current 2496 | local min 2497 | if self.horizontal then 2498 | current = border 2499 | min = border + padding 2500 | else 2501 | current = self.height - border 2502 | min = border + padding 2503 | end 2504 | local past = false 2505 | local itemCount = 0 2506 | local possibleItemCount = 1 2507 | for i,item in ipairs(items) do 2508 | if possibleItemCount < topIndex and not item.filtered then 2509 | item.visible = false 2510 | possibleItemCount = possibleItemCount + 1 2511 | elseif past or item.filtered then 2512 | item.visible = false 2513 | else 2514 | itemCount = itemCount + 1 2515 | item.visible = nil 2516 | if self.horizontal then 2517 | item.y = min 2518 | current = current + (padding + itemSize) 2519 | item.x = current 2520 | if current + itemSize > self.width - borderSize then 2521 | item.visible = false 2522 | self.bottomIndex = itemCount + topIndex - 1 2523 | past = true 2524 | end 2525 | else 2526 | item.x = min 2527 | current = current - (padding + itemSize) 2528 | item.y = current 2529 | if current < border then 2530 | item.visible = false 2531 | self.bottomIndex = itemCount + topIndex - 1 2532 | past = true 2533 | end 2534 | end 2535 | end 2536 | item.layout = true 2537 | end 2538 | if not past then 2539 | self.bottomIndex = topIndex + itemCount 2540 | end 2541 | self:updateScrollBar() 2542 | end 2543 | 2544 | function List:updateScrollBar() 2545 | local maxLength 2546 | local slider = self.slider 2547 | local offset 2548 | if self.horizontal then 2549 | maxLength = slider.width 2550 | else 2551 | maxLength = slider.height 2552 | end 2553 | local items = self.items 2554 | local topIndex = self.topIndex 2555 | local bottomIndex = self.bottomIndex 2556 | local itemCount = self.itemCount 2557 | if bottomIndex > itemCount and topIndex == 1 then 2558 | slider.handleSize = maxLength 2559 | slider.maxValue = 0 2560 | else 2561 | local numItems = bottomIndex - topIndex -- Number of displayed items 2562 | local barLength = math.max( 2563 | numItems * maxLength / itemCount, 2564 | self.scrollBarSize) 2565 | slider.handleSize = barLength 2566 | slider.maxValue = itemCount - numItems 2567 | if self.horizontal then 2568 | slider.value = topIndex - 1 2569 | else 2570 | slider.value = slider.maxValue - (topIndex - 1) 2571 | end 2572 | end 2573 | end 2574 | 2575 | function List:scroll(up) 2576 | if up then 2577 | self.topIndex = math.max(self.topIndex - 1, 1) 2578 | else 2579 | if self.bottomIndex <= self.itemCount then 2580 | self.topIndex = self.topIndex + 1 2581 | end 2582 | end 2583 | self:positionItems() 2584 | end 2585 | 2586 | function List:clickEvent(position, button, pressed) 2587 | if button >= 4 then -- scroll 2588 | if pressed then 2589 | if button == 4 then -- Scroll up 2590 | self:scroll(true) 2591 | else -- Scroll down 2592 | self:scroll(false) 2593 | end 2594 | end 2595 | return true 2596 | end 2597 | end 2598 | -------------------------------------------------------------------------------- /penguingui/Align.lua: -------------------------------------------------------------------------------- 1 | --- Alignment enum 2 | Align = {} 3 | 4 | --- Align to the left. 5 | Align.LEFT = 0 6 | --- Align to the center. 7 | Align.CENTER = 1 8 | --- Align to the right. 9 | Align.RIGHT = 2 10 | --- Align to the top. 11 | Align.TOP = 3 12 | --- Align to the bottom. 13 | Align.BOTTOM = 4 14 | -------------------------------------------------------------------------------- /penguingui/Binding.lua: -------------------------------------------------------------------------------- 1 | --- Adds listeners or bindings to objects. 2 | -- @module Binding 3 | -- @usage -- Add a listener to print whenever a value is changed. 4 | -- local sometable = { a = "somevalue" } 5 | -- sometable = Binding.proxy(sometable) 6 | -- sometable:addListener("a", function(t, k, old, new) 7 | -- print("Key " .. k .. " changed from " .. old .. " to " .. new) 8 | -- end 9 | -- @usage -- Bind a value in a table to the sum of two other values. 10 | -- local table1 = Binding.proxy({ a = 4 }) 11 | -- local table2 = Binding.proxy({ a = 5 }) 12 | -- local sumtable = Binding.proxy({}) 13 | -- sumtable:bind("sum", Binding(table1, "a"):add(Binding(table2, "a"))) 14 | -- -- sumtable.sum == 9 15 | -- table1.a = 6 16 | -- -- sumtable.sum == 11 17 | Binding = setmetatable( 18 | {}, 19 | { 20 | __call = function(t, ...) 21 | return t.value(...) 22 | end 23 | } 24 | ) 25 | 26 | -- Proxy metatable to add listeners to a table 27 | Binding.proxyTable = { 28 | __index = function(t, k) 29 | local out = t._instance[k] 30 | if out ~= nil then 31 | return out 32 | else 33 | return Binding.proxyTable[k] 34 | end 35 | end, 36 | __newindex = function(t, k, v) 37 | local instance = t._instance 38 | local old = instance[k] 39 | local new = v 40 | instance[k] = new 41 | if old ~= v then 42 | local listeners = instance.listeners 43 | if listeners and listeners[k] then 44 | local keyListeners = listeners[k] 45 | for _,keyListener in ipairs(keyListeners) do 46 | new = keyListener(t, k, old, new) or new 47 | end 48 | end 49 | local bindings = instance.bindings 50 | if bindings and bindings[k] then 51 | local keyBindings = bindings[k] 52 | for _,keyBinding in ipairs(keyBindings) do 53 | keyBinding:valueChanged(old, new) 54 | end 55 | end 56 | end 57 | end, 58 | __pairs = function(t) 59 | return pairs(t._instance) 60 | end, 61 | __ipairs = function(t) 62 | return ipairs(t._instance) 63 | end, 64 | __add = function(a, b) 65 | return a._instance + (type(b) == "table" and b._instance or b) 66 | end, 67 | __sub = function(a, b) 68 | return a._instance - (type(b) == "table" and b._instance or b) 69 | end, 70 | __mul = function(a, b) 71 | return a._instance * (type(b) == "table" and b._instance or b) 72 | end, 73 | __div = function(a, b) 74 | return a._instance / (type(b) == "table" and b._instance or b) 75 | end, 76 | __mod = function(a, b) 77 | return a._instance % (type(b) == "table" and b._instance or b) 78 | end, 79 | __pow = function(a, b) 80 | return a._instance ^ (type(b) == "table" and b._instance or b) 81 | end, 82 | __unm = function(a) 83 | return -a._instance 84 | end, 85 | __concat = function(a, b) 86 | return a._instance .. (type(b) == "table" and b._instance or b) 87 | end, 88 | __len = function(a) 89 | return #a._instance 90 | end, 91 | __eq = function(a, b) 92 | return a._instance == b._instance 93 | end, 94 | __lt = function(a, b) 95 | return a._instance < b._instance 96 | end, 97 | __le = function(a, b) 98 | return a._instance <= b._instance 99 | end, 100 | __call = function(t, ...) 101 | return t._instance(...) 102 | end 103 | } 104 | 105 | --- Whether the table is a Binding or not. 106 | -- 107 | -- @param object The table to check. 108 | -- @return True if the table is a binding, false if not. 109 | function Binding.isValue(object) 110 | return type(object) == "table" 111 | and getmetatable(object._instance) == Binding.valueTable 112 | end 113 | 114 | Binding.weakMetaTable = { 115 | __mode = "v" 116 | } 117 | 118 | Binding.valueTable = {} 119 | 120 | Binding.valueTable.__index = Binding.valueTable 121 | 122 | --- Contains a value that is based off whatever this binding is bound to. 123 | -- @type Binding 124 | 125 | --- Adds a listener to the value of this binding. 126 | -- @function addValueListener 127 | -- @param listener The listener to add. 128 | -- @return The added listener. 129 | function Binding.valueTable:addValueListener(listener) 130 | return self:addListener("value", listener) 131 | end 132 | 133 | --- Removes a listener to the value of this binding. 134 | -- @function removeValueListener 135 | -- @param listener The listener to remove. 136 | -- @return Whether a listener was removed. 137 | function Binding.valueTable:removeValueListener(listener) 138 | return self:removeListener("value", listener) 139 | end 140 | 141 | --- Convenience method for adding a binding to "value" 142 | -- @function addValueBinding 143 | -- @param binding The binding to add. 144 | -- @return The added binding. 145 | function Binding.valueTable:addValueBinding(binding) 146 | return self:addBinding("value", binding) 147 | end 148 | 149 | --- Convenience method for removing a binding from "value" 150 | -- @function removeValueBinding 151 | -- @param binding The binding to remove. 152 | -- @return Whether a binding was removed. 153 | function Binding.valueTable:removeValueBinding(binding) 154 | return self:removeBinding("value", binding) 155 | end 156 | 157 | --- Unbinds this binding, as well as anything bound to it. 158 | -- @function unbind 159 | function Binding.valueTable:unbind() 160 | local bindings = self.bindings 161 | if bindings and bindings.value then 162 | local valueBindings = bindings.value 163 | for _,binding in ipairs(valueBindings) do 164 | binding:unbind() 165 | end 166 | end 167 | local boundto = self.boundto 168 | for _,bound in ipairs(boundto) do 169 | for _,boundTable in pairs(bound.bindings) do 170 | PtUtil.removeObject(boundTable, self) 171 | end 172 | end 173 | self.boundto = nil 174 | local bindTargets = self.bindTargets 175 | if bindTargets then 176 | for bindTarget,_ in pairs(bindTargets) do 177 | local bindTargetBoundto = bindTarget.boundto 178 | for key,binding in pairs(bindTargetBoundto) do 179 | if binding == self then 180 | bindTargetBoundto[key] = nil 181 | end 182 | end 183 | end 184 | end 185 | end 186 | 187 | --- @type end 188 | 189 | function Binding.unbindChain(binding) 190 | Binding.valueTable.unbind(binding) 191 | local bindingTable = binding.bindingTable 192 | for i=1, #bindingTable - 1, 1 do 193 | bindingTable[i]:unbind() 194 | bindingTable[i] = nil 195 | end 196 | end 197 | 198 | --- Creates a binding bound to the given key in the given table. 199 | -- 200 | -- @param t The table the binding is bound to. 201 | -- @param k The key the binding is bound to. 202 | -- @return A binding to the key in the given table. 203 | function Binding.value(t, k) 204 | local out = Binding.proxy(setmetatable({}, Binding.valueTable)) 205 | if type(k) == "string" then -- Single key 206 | out.value = t[k] 207 | out.valueChanged = function(binding, old, new) 208 | binding.value = new 209 | end 210 | t:addBinding(k, out) 211 | out.boundto = {t} 212 | return out 213 | else -- Table of keys 214 | local numKeys = #k 215 | local currTable = t 216 | local bindingTable = {} 217 | for i=1, numKeys - 1, 1 do 218 | local currBinding = Binding.proxy(setmetatable({}, Binding.valueTable)) 219 | local currKey = k[i] 220 | local index = i 221 | 222 | currBinding.valueChanged = function(binding, old, new) 223 | if old == new then return end 224 | -- Transplant bindings from old tables to new tables 225 | local oldTable = old 226 | local newTable = new 227 | local subKey 228 | local transplant 229 | for j=index + 1, numKeys, 1 do 230 | subKey = k[j] 231 | transplant = bindingTable[j] 232 | transplant.boundto[1] = newTable 233 | oldTable:removeBinding(subKey, transplant) 234 | newTable:addBinding(subKey, transplant) 235 | 236 | if j < numKeys then 237 | oldTable = oldTable[subKey] 238 | newTable = newTable[subKey] 239 | end 240 | end 241 | end 242 | currBinding.boundto = {currTable} 243 | currTable:addBinding(currKey, currBinding) 244 | bindingTable[index] = currBinding 245 | 246 | currTable = t[currKey] 247 | end 248 | out.valueChanged = function(binding, old, new) 249 | binding.value = new 250 | end 251 | out.bindingTable = bindingTable 252 | out.boundto = {currTable} 253 | out.unbind = Binding.unbindChain 254 | currTable:addBinding(k[numKeys], out) 255 | bindingTable[numKeys] = out 256 | return out 257 | end 258 | end 259 | 260 | --- A proxy to allow listeners & bindings to be attached. 261 | -- @type Proxy 262 | 263 | --- Adds a listener to the specified key that is called when the key's value 264 | -- changes. 265 | -- @function addListener 266 | -- 267 | -- @param key The key to track changes to 268 | -- @param listener The function to call upon the value of the key changing. 269 | -- The function should have the arguments (t, k, old, new) where: 270 | -- t is the table in which the change happened. 271 | -- k is the key whose value changed. 272 | -- old is the old value of the key. 273 | -- new is the new value of the key. 274 | -- If the function changes the key's value, it should return the new value. 275 | -- @return The added listener. 276 | function Binding.proxyTable:addListener(key, listener) 277 | local listeners = self.listeners 278 | if not listeners then 279 | listeners = {} 280 | self.listeners = listeners 281 | end 282 | local keyListeners = listeners[key] 283 | if not keyListeners then 284 | keyListeners = {} 285 | listeners[key] = keyListeners 286 | end 287 | table.insert(keyListeners, listener) 288 | return listener 289 | end 290 | 291 | --- Removes the first instance of the given listener from the given key. 292 | -- @function removeListener 293 | -- 294 | -- @param key The key the listener is attached to. 295 | -- @param listener The listener to remove. 296 | -- 297 | -- @return Whether a listener was removed. 298 | function Binding.proxyTable:removeListener(key, listener) 299 | local keyListeners = self.listeners[key] 300 | return PtUtil.removeObject(keyListeners, listener) ~= -1 301 | end 302 | 303 | --- Adds a binding to the specified key in this table. 304 | -- @function addBinding 305 | -- 306 | -- @param key The key to bind to. 307 | -- @param binding The binding to attach. 308 | -- @return The added binding. 309 | function Binding.proxyTable:addBinding(key, binding) 310 | local bindings = self.bindings 311 | if not bindings then 312 | bindings = {} 313 | self.bindings = bindings 314 | end 315 | local keyBindings = bindings[key] 316 | if not keyBindings then 317 | keyBindings = setmetatable({}, Binding.weakMetaTable) 318 | bindings[key] = keyBindings 319 | end 320 | table.insert(keyBindings, binding) 321 | binding:valueChanged(self[key], self[key]) 322 | return binding 323 | end 324 | 325 | --- Removes a binding from a key in this table. 326 | -- @function removeBinding 327 | -- 328 | -- @param key The key to remove a binding from. 329 | -- @param binding The binding to remove. 330 | -- @return Whether a binding was removed. 331 | function Binding.proxyTable:removeBinding(key, binding) 332 | local keyBindings = self.bindings[key] 333 | return PtUtil.removeObject(keyBindings, binding) ~= -1 334 | end 335 | 336 | --- @type end 337 | 338 | --- Binds the key in the specified table to the given value 339 | -- 340 | -- @param target The table where the key to be bound is. 341 | -- @param key The key to be bound. 342 | -- @param value The value to bind to. 343 | function Binding.bind(target, key, value) 344 | local listener = function(t, k, old, new) 345 | target[key] = new 346 | end 347 | value:addValueListener(listener) 348 | 349 | -- Put reference to this binding into the target table to keep this binding 350 | -- alive. 351 | local boundto = target.boundto 352 | if not boundto then 353 | boundto = {} 354 | target.boundto = boundto 355 | end 356 | local boundtoKey = boundto[key] 357 | assert(not boundtoKey, key .. " is already bound to another value") 358 | boundto[key] = value 359 | 360 | local bindTargets = value.bindTargets 361 | 362 | -- Keep references to the table this binding is bound to, so we can clean 363 | -- up if this binding is unbound. 364 | if not bindTargets then 365 | bindTargets = {} 366 | value.bindTargets = bindTargets 367 | end 368 | local bindKeyTargets = bindTargets[target] 369 | if not bindKeyTargets then 370 | bindKeyTargets = {} 371 | bindTargets[target] = bindKeyTargets 372 | end 373 | bindKeyTargets[key] = listener 374 | 375 | target[key] = value.value 376 | end 377 | 378 | Binding.proxyTable.bind = Binding.bind 379 | 380 | --- Binds the key in the specified table to the key in the other table, and 381 | -- vice versa. 382 | -- 383 | -- @param t1 The first table where the key to be bound is. 384 | -- @param k1 The key in the first table to be bound. 385 | -- @param t2 The second table where the key to be bound is. 386 | -- @param k2 The key in the second table to be bound. 387 | function Binding.bindBidirectional(t1, k1, t2, k2) 388 | t1:bind(k1, Binding(t2, k2)) 389 | t2:bind(k2, Binding(t1, k1)) 390 | end 391 | 392 | Binding.proxyTable.bindBidirectional = Binding.bindBidirectional 393 | 394 | --- Removes the binding on the given key in the given target. 395 | -- 396 | -- @param target The table to remove the binding from. 397 | -- @param key The key to unbind. 398 | function Binding.unbind(target, key) 399 | local binding = target.boundto[key] 400 | if binding then 401 | binding:removeValueListener(binding.bindTargets[target][key]) 402 | binding.bindTargets[target][key] = nil 403 | target.boundto[key] = nil 404 | end 405 | end 406 | 407 | Binding.proxyTable.unbind = Binding.unbind 408 | 409 | --- Returns a proxy to a table that allows listeners and bindings to be attached. 410 | -- 411 | -- @param instance The table to proxy. 412 | -- @return A proxy table to the given instance. 413 | function Binding.proxy(instance) 414 | return setmetatable( 415 | {_instance = instance}, 416 | Binding.proxyTable 417 | ) 418 | end 419 | -------------------------------------------------------------------------------- /penguingui/BindingFunctions.lua: -------------------------------------------------------------------------------- 1 | --- @module Binding 2 | 3 | -- Creates a binding function (convenience function) 4 | -- 5 | -- @param f A function that returns this value's new value after the value 6 | -- that it is bound to changes. 7 | -- @param numArgs The number of arguments to the function to create. 8 | local createFunction = function(f) 9 | return function(...) 10 | local out = Binding.proxy(setmetatable({}, Binding.valueTable)) 11 | local getters = {} 12 | local boundto = {self} 13 | local args = table.pack(...) 14 | local numArgs = args.n 15 | for i = 1, numArgs, 1 do 16 | local value = args[i] 17 | local getter 18 | if Binding.isValue(value) then 19 | getter = function() 20 | return value.value 21 | end 22 | else 23 | getter = function() 24 | return value 25 | end 26 | end 27 | getters[i] = getter 28 | end 29 | out.valueChanged = function(binding, old, new) 30 | out.value = f(table.unpack(getters)) 31 | end 32 | for i = 1, numArgs, 1 do 33 | local value = args[i] 34 | if Binding.isValue(value) then 35 | value:addValueBinding(out) 36 | end 37 | table.insert(boundto, value) 38 | end 39 | out.boundto = boundto 40 | return out 41 | end 42 | end 43 | 44 | --- @type Binding 45 | 46 | --- Creates a binding containing the string representation of this binding. 47 | -- @function tostring 48 | -- @return A new binding. 49 | Binding.valueTable.tostring = createFunction( 50 | function(value) 51 | return tostring(value()) 52 | end 53 | ) 54 | Binding.tostring = Binding.valueTable.tostring 55 | 56 | --- Creates a binding containing the number value of this binding. 57 | -- @function tonumber 58 | -- @return A new binding. 59 | Binding.valueTable.tonumber = createFunction( 60 | function(value) 61 | return tonumber(value()) 62 | end 63 | ) 64 | Binding.tonumber = Binding.valueTable.tonumber 65 | 66 | --- Creates a new binding containing the sum of this binding and others. 67 | -- @function add 68 | -- @param ... Can be bindings, or constants (number, etc.) 69 | -- @return A new binding. 70 | Binding.valueTable.add = createFunction( 71 | function(first, ...) 72 | local out = first() 73 | local args = table.pack(...) 74 | for _,value in ipairs(args) do 75 | out = out + value() 76 | end 77 | return out 78 | end 79 | ) 80 | Binding.add = Binding.valueTable.add 81 | 82 | --- Creates a new binding containing the difference of this binding and others. 83 | -- @function sub 84 | -- @param ... Can be bindings, or constants (number, etc.) 85 | -- @return A new binding. 86 | Binding.valueTable.sub = createFunction( 87 | function(first, ...) 88 | local out = first() 89 | local args = table.pack(...) 90 | for _,value in ipairs(args) do 91 | out = out - value() 92 | end 93 | return out 94 | end 95 | ) 96 | Binding.sub = Binding.valueTable.sub 97 | 98 | --- Creates a new binding containing the product of this binding and others. 99 | -- @function mul 100 | -- @param ... Can be bindings, or constants (number, etc.) 101 | -- @return A new binding. 102 | Binding.valueTable.mul = createFunction( 103 | function(first, ...) 104 | local out = first() 105 | local args = table.pack(...) 106 | for _,value in ipairs(args) do 107 | out = out * value() 108 | end 109 | return out 110 | end 111 | ) 112 | Binding.mul = Binding.valueTable.mul 113 | 114 | --- Creates a new binding containing the quotient of this binding and others. 115 | -- @function div 116 | -- @param ... Can be bindings, or constants (number, etc.) 117 | -- @return A new binding. 118 | Binding.valueTable.div = createFunction( 119 | function(first, ...) 120 | local out = first() 121 | local args = table.pack(...) 122 | for _,value in ipairs(args) do 123 | out = out / value() 124 | end 125 | return out 126 | end 127 | ) 128 | Binding.div = Binding.valueTable.div 129 | 130 | --- Creates a new binding containing the modulus of this binding and others. 131 | -- @function mod 132 | -- @param ... Can be bindings, or constants (number, etc.) 133 | -- @return A new binding. 134 | Binding.valueTable.mod = createFunction( 135 | function(first, ...) 136 | local out = first() 137 | local args = table.pack(...) 138 | for _,value in ipairs(args) do 139 | out = out % value() 140 | end 141 | return out 142 | end 143 | ) 144 | Binding.mod = Binding.valueTable.mod 145 | 146 | --- Creates a new binding containing the exponentiation of this binding and 147 | -- others. 148 | -- @function pow 149 | -- @param ... Can be bindings, or constants (number, etc.) 150 | -- @return A new binding. 151 | Binding.valueTable.pow = createFunction( 152 | function(first, ...) 153 | local out = first() 154 | local args = table.pack(...) 155 | for _,value in ipairs(args) do 156 | out = out ^ value() 157 | end 158 | return out 159 | end 160 | ) 161 | Binding.pow = Binding.valueTable.pow 162 | 163 | --- Creates a new binding containing the negation of this binding. 164 | -- @function negate 165 | -- @return A new binding. 166 | Binding.valueTable.negate = createFunction( 167 | function(value) 168 | return -value() 169 | end 170 | ) 171 | Binding.negate = Binding.valueTable.negate 172 | 173 | --- Creates a new binding containing the concatenation of this binding and 174 | -- others. 175 | -- @function concat 176 | -- @param ... Can be bindings, or constants (number, etc.) 177 | -- @return A new binding. 178 | Binding.valueTable.concat = createFunction( 179 | function(first, ...) 180 | local out = first() 181 | local args = table.pack(...) 182 | for _,value in ipairs(args) do 183 | out = out .. value() 184 | end 185 | return out 186 | end 187 | ) 188 | Binding.concat = Binding.valueTable.concat 189 | 190 | --- Creates a new binding containing the length of this binding. 191 | -- @function len 192 | -- @return A new binding. 193 | Binding.valueTable.len = createFunction( 194 | function(value) 195 | return #value() 196 | end 197 | ) 198 | Binding.len = Binding.valueTable.len 199 | 200 | --- Creates a new binding representing if this value is equal to another. 201 | -- @function eq 202 | -- @param other Can be another binding or a constant (number, etc.) 203 | -- @return A new binding. 204 | Binding.valueTable.eq = createFunction( 205 | function(a, b) 206 | return a() == b() 207 | end 208 | ) 209 | Binding.eq = Binding.valueTable.eq 210 | 211 | --- Creates a new binding representing if this value is not equal to another. 212 | -- @function ne 213 | -- @param other Can be another binding or a constant (number, etc.) 214 | -- @return A new binding. 215 | Binding.valueTable.ne = createFunction( 216 | function(a, b) 217 | return a() ~= b() 218 | end 219 | ) 220 | Binding.ne = Binding.valueTable.ne 221 | 222 | --- Creates a new binding representing if this value is less than to another. 223 | -- @function lt 224 | -- @param other Can be another binding or a constant (number, etc.) 225 | -- @return A new binding. 226 | Binding.valueTable.lt = createFunction( 227 | function(a, b) 228 | return a() < b() 229 | end 230 | ) 231 | Binding.lt = Binding.valueTable.lt 232 | 233 | --- Creates a new binding representing if this value is greater than to another. 234 | -- @function gt 235 | -- @param other Can be another binding or a constant (number, etc.) 236 | -- @return A new binding. 237 | Binding.valueTable.gt = createFunction( 238 | function(a, b) 239 | return a() > b() 240 | end 241 | ) 242 | Binding.gt = Binding.valueTable.gt 243 | 244 | --- Creates a new binding representing if this value is less than or equal 245 | -- to another. 246 | -- @function le 247 | -- @param other Can be another binding or a constant (number, etc.) 248 | -- @return A new binding. 249 | Binding.valueTable.le = createFunction( 250 | function(a, b) 251 | return a() <= b() 252 | end 253 | ) 254 | Binding.le = Binding.valueTable.le 255 | 256 | --- Creates a new binding representing if this value is greater than or equal 257 | -- to another. 258 | -- @function ge 259 | -- @param other Can be another binding or a constant (number, etc.) 260 | -- @return A new binding. 261 | Binding.valueTable.ge = createFunction( 262 | function(a, b) 263 | return a() >= b() 264 | end 265 | ) 266 | Binding.ge = Binding.valueTable.ge 267 | 268 | --- Creates a new binding containing this binding AND others. 269 | -- @function AND 270 | -- @param ... Can be bindings, or constants (number, etc.) 271 | -- @return A new binding. 272 | Binding.valueTable.AND = createFunction( 273 | function(first, ...) 274 | local out = first() 275 | local args = table.pack(...) 276 | for _,value in ipairs(args) do 277 | out = out and value() 278 | end 279 | return out 280 | end 281 | ) 282 | Binding.AND = Binding.valueTable.AND 283 | 284 | --- Creates a new binding containing this binding OR others. 285 | -- @function OR 286 | -- @param ... Can be bindings, or constants (number, etc.) 287 | -- @return A new binding. 288 | Binding.valueTable.OR = createFunction( 289 | function(first, ...) 290 | local out = first() 291 | local args = table.pack(...) 292 | for _,value in ipairs(args) do 293 | out = out or value() 294 | end 295 | return out 296 | end 297 | ) 298 | Binding.OR = Binding.valueTable.OR 299 | 300 | --- Creates a new binding containing NOT this binding. 301 | -- @function NOT 302 | -- @return A new binding. 303 | Binding.valueTable.NOT = createFunction( 304 | function(value) 305 | return not value() 306 | end 307 | ) 308 | Binding.NOT = Binding.valueTable.NOT 309 | 310 | --- Creates a new binding with the value of the first value if this binding is 311 | -- true, or the second value if this binding is false. 312 | -- @function THEN 313 | -- 314 | -- @param ifTrue The value the new binding will be set to when this value is 315 | -- true. Can either be another value, or a constant (number, etc.) 316 | -- @param ifFalse The value the new binding will be set to when this value is 317 | -- false. Can either be another value, or a constant (number, etc.) 318 | -- @return A new binding. 319 | Binding.valueTable.THEN = function(self, ifTrue, ifFalse) 320 | local out = Binding.proxy(setmetatable({}, Binding.valueTable)) 321 | local trueFunction 322 | local falseFunction 323 | local boundto = {self} 324 | if Binding.isValue(ifTrue) then 325 | trueFunction = function() 326 | return ifTrue.value 327 | end 328 | else 329 | trueFunction = function() 330 | return ifTrue 331 | end 332 | end 333 | if Binding.isValue(ifFalse) then 334 | falseFunction = function() 335 | return ifFalse.value 336 | end 337 | else 338 | falseFunction = function() 339 | return ifFalse 340 | end 341 | end 342 | out.valueChanged = function(binding, old, new) 343 | if self.value then 344 | out.value = trueFunction() 345 | else 346 | out.value = falseFunction() 347 | end 348 | end 349 | self:addValueBinding(out) 350 | if Binding.isValue(ifTrue) then 351 | ifTrue:addValueBinding(out) 352 | table.insert(boundto, ifTrue) 353 | end 354 | if Binding.isValue(ifFalse) then 355 | ifFalse:addValueBinding(out) 356 | table.insert(boundto, ifFalse) 357 | end 358 | out.boundto = boundto 359 | return out 360 | end 361 | Binding.THEN = Binding.valueTable.THEN 362 | 363 | --- @type end 364 | -------------------------------------------------------------------------------- /penguingui/Button.lua: -------------------------------------------------------------------------------- 1 | --- A clickable button. 2 | -- @classmod Button 3 | -- @usage -- Create an empty button that prints when it is clicked 4 | -- local button = Button(0, 0, 100, 100) 5 | -- button.onClick = function(component, button) 6 | -- print("Clicked with mouse button " .. button) 7 | -- end 8 | Button = class(Component) 9 | --- The color of the outer border of this button. 10 | Button.outerBorderColor = {0, 0, 0} 11 | --- The color of the inner border of this button. 12 | Button.innerBorderColor = {84, 84, 84} 13 | --- The color of the inner border of this button when the mouse is over it. 14 | Button.innerBorderHoverColor = {147, 147, 147} 15 | --- The color of this button. 16 | Button.color = {38, 38, 38} 17 | --- The color of this button when the mouse is over it. 18 | Button.hoverColor = {84, 84, 84} 19 | 20 | --- Constructor 21 | -- @section 22 | 23 | --- Constructs a new Button. 24 | -- 25 | -- @param x The x coordinate of the new component, relative to its parent. 26 | -- @param y The y coordinate of the new component, relative to its parent. 27 | -- @param width The width of the new component. 28 | -- @param height The height of the new component. 29 | function Button:_init(x, y, width, height) 30 | Component._init(self) 31 | self.mouseOver = false 32 | 33 | self.x = x 34 | self.y = y 35 | self.width = width 36 | self.height = height 37 | end 38 | 39 | --- @section end 40 | 41 | function Button:update(dt) 42 | if self.pressed and not self.mouseOver then 43 | self:setPressed(false) 44 | end 45 | end 46 | 47 | function Button:draw(dt) 48 | local startX = self.x + self.offset[1] 49 | local startY = self.y + self.offset[2] 50 | local w = self.width 51 | local h = self.height 52 | 53 | local borderPoly = { 54 | {startX + 1, startY + 0.5}, 55 | {startX + w - 1, startY + 0.5}, 56 | {startX + w - 0.5, startY + 1}, 57 | {startX + w - 0.5, startY + h - 1}, 58 | {startX + w - 1, startY + h - 0.5}, 59 | {startX + 1, startY + h - 0.5}, 60 | {startX + 0.5, startY + h - 1}, 61 | {startX + 0.5, startY + 1}, 62 | } 63 | local innerBorderRect = { 64 | startX + 1, startY + 1, startX + w - 1, startY + h - 1 65 | } 66 | local rectOffset = 1.5 67 | local rect = { 68 | startX + rectOffset, startY + rectOffset, startX + w - rectOffset, startY + h - rectOffset 69 | } 70 | 71 | PtUtil.drawPoly(borderPoly, self.outerBorderColor, 1) 72 | if self.mouseOver then 73 | PtUtil.drawRect(innerBorderRect, self.innerBorderHoverColor, 0.5) 74 | PtUtil.fillRect(rect, self.hoverColor) 75 | else 76 | PtUtil.drawRect(innerBorderRect, self.innerBorderColor, 0.5) 77 | PtUtil.fillRect(rect, self.color) 78 | end 79 | end 80 | 81 | function Button:setPressed(pressed) 82 | if pressed and not self.pressed then 83 | self.x = self.x + 1 84 | self.y = self.y - 1 85 | self.layout = true 86 | end 87 | if not pressed and self.pressed then 88 | self.x = self.x - 1 89 | self.y = self.y + 1 90 | self.layout = true 91 | end 92 | self.pressed = pressed 93 | end 94 | 95 | function Button:clickEvent(position, button, pressed) 96 | if button <= 3 then 97 | if self.onClick and not pressed and self.pressed then 98 | self:onClick(button) 99 | end 100 | self:setPressed(pressed) 101 | return true 102 | end 103 | end 104 | 105 | --- Called when this button is clicked. 106 | -- @function onClick 107 | -- 108 | -- @param button The mouse button that was used. 109 | -------------------------------------------------------------------------------- /penguingui/CheckBox.lua: -------------------------------------------------------------------------------- 1 | --- A check box. 2 | -- @classmod CheckBox 3 | -- @usage -- Create a checkbox that prints when it is checked 4 | -- local checkbox = CheckBox(0, 0, 16) 5 | -- checkbox:addListener("selected", function(t, k, old, new) 6 | -- print("The checkbox was " .. (new and "checked" or "unchecked")) 7 | -- end 8 | CheckBox = class(Component) 9 | --- The color of the border of this checkbox. 10 | CheckBox.borderColor = {84, 84, 84} 11 | --- The color of the checkbox. 12 | CheckBox.backgroundColor = {0, 0, 0} 13 | --- The color of this checkbox when the mouse is over it. 14 | CheckBox.hoverColor = {28, 28, 28} 15 | --- The color of the check. 16 | CheckBox.checkColor = {197, 26, 11} 17 | --- The color of this checkbox when it is pressed. 18 | CheckBox.pressedColor = {52, 52, 52} 19 | 20 | --- Constructor 21 | -- @section 22 | 23 | --- Constructs a new CheckBox. 24 | -- 25 | -- @param x The x coordinate of the new component, relative to its parent. 26 | -- @param y The y coordinate of the new component, relative to its parent. 27 | -- @param size The width and height of the new component. 28 | function CheckBox:_init(x, y, size) 29 | Component._init(self) 30 | self.mouseOver = false 31 | 32 | self.x = x 33 | self.y = y 34 | self.width = size 35 | self.height = size 36 | 37 | self.selected = false 38 | end 39 | 40 | --- @section end 41 | 42 | function CheckBox:update(dt) 43 | if self.pressed and not self.mouseOver then 44 | self.pressed = false 45 | end 46 | end 47 | 48 | function CheckBox:draw(dt) 49 | local startX = self.x + self.offset[1] 50 | local startY = self.y + self.offset[2] 51 | local w = self.width 52 | local h = self.height 53 | 54 | local borderRect = {startX, startY, startX + w, startY + h} 55 | local rect = {startX + 1, startY + 1, startX + w - 1, startY + h - 1} 56 | PtUtil.drawRect(borderRect, self.borderColor, 1) 57 | 58 | if self.pressed then 59 | PtUtil.fillRect(rect, self.pressedColor) 60 | elseif self.mouseOver then 61 | PtUtil.fillRect(rect, self.hoverColor) 62 | else 63 | PtUtil.fillRect(rect, self.backgroundColor) 64 | end 65 | 66 | -- Draw check, if needed 67 | if self.selected then 68 | self:drawCheck(dt) 69 | end 70 | end 71 | 72 | --- Draw the checkbox 73 | -- @param dt The time elapsed since the last draw. 74 | function CheckBox:drawCheck(dt) 75 | local startX = self.x + self.offset[1] 76 | local startY = self.y + self.offset[2] 77 | local w = self.width 78 | local h = self.height 79 | PtUtil.drawLine( 80 | {startX + w / 4, startY + w / 2}, 81 | {startX + w / 3, startY + h / 4}, 82 | self.checkColor, 1) 83 | PtUtil.drawLine( 84 | {startX + w / 3, startY + h / 4}, 85 | {startX + 3 * w / 4, startY + 3 * h / 4}, 86 | self.checkColor, 1) 87 | end 88 | 89 | function CheckBox:clickEvent(position, button, pressed) 90 | if button <= 3 then 91 | if not pressed and self.pressed then 92 | self.selected = not self.selected 93 | end 94 | self.pressed = pressed 95 | return true 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /penguingui/Component.lua: -------------------------------------------------------------------------------- 1 | --- Superclass for all GUI components. 2 | -- @usage 3 | -- -- Creating a custom component: 4 | -- CustomComponent = class(Component) -- Your new component needs to extend Component 5 | -- -- Set any variables that have a default value 6 | -- CustomComponent.somefield = "somedefault" 7 | -- 8 | -- -- Constructor for your component 9 | -- function CustomComponent:_init(args) 10 | -- Component._init(self) -- This line must be the first line of your constructor 11 | -- -- Do other initialization stuff 12 | -- self.someotherfield = "somevalue" 13 | -- -- If you want your component to block the mouse (i.e. know when the mouse 14 | -- -- is over it, you must set mouseOver to not nil 15 | -- self.mouseOver = false 16 | -- end 17 | -- 18 | -- -- Put all your component logic in update 19 | -- function CustomComponent:update(dt) 20 | -- -- Do some logic 21 | -- end 22 | -- 23 | -- -- Put all your component rendering in draw 24 | -- function CustomComponent:draw(dt) 25 | -- -- Do some drawing 26 | -- end 27 | -- 28 | -- -- If you want your component to be notified of click events, create a 29 | -- -- clickEvent method. 30 | -- function CustomComponent:clickEvent(position, button, pressed) 31 | -- -- Process click 32 | -- end 33 | -- 34 | -- -- If you want your component to be notified of key events, create a keyEvent 35 | -- -- method. 36 | -- function CustomComponent:keyEvent(keyCode, pressed) 37 | -- -- Process key 38 | -- end 39 | -- @classmod Component 40 | Component = class() 41 | --- The x location of this component, relative to its parent. 42 | Component.x = 0 43 | --- The y location of this component, relative to its parent. 44 | Component.y = 0 45 | --- The width of this component. 46 | Component.width = 0 47 | --- The height of this component. 48 | Component.height = 0 49 | 50 | --- Whether the mouse is hovering over this component. 51 | -- Set this to not nil to allow this to be set. 52 | Component.mouseOver = nil 53 | 54 | --- Whether this component has keyboard focus. 55 | -- @{mouseOver} needs to be not nil for this to be set. 56 | Component.hasFocus = nil 57 | 58 | --- Constructor 59 | -- @section 60 | 61 | --- Constructs a component. 62 | function Component:_init() 63 | self.children = {} 64 | self.offset = Binding.proxy({0, 0}) 65 | end 66 | 67 | --- @section end 68 | 69 | --- Adds a child component to this component. 70 | -- 71 | -- @param child The component to add. 72 | function Component:add(child) 73 | local children = self.children 74 | children[#children + 1] = child 75 | child:setParent(self) 76 | end 77 | 78 | --- Removes a child component. 79 | -- 80 | -- @param child The component to remove 81 | -- @return Whether or not the child was removed 82 | function Component:remove(child) 83 | local children = self.children 84 | for index,comp in ripairs(children) do 85 | if (comp == child) then 86 | child:removeSelf() 87 | return true 88 | end 89 | end 90 | return false 91 | end 92 | 93 | --- Remove self from parent. 94 | function Component:removeSelf() 95 | local siblings 96 | if self.parent then 97 | siblings = self.parent.children 98 | else 99 | siblings = GUI.components 100 | end 101 | for index,sibling in ripairs(siblings) do 102 | if sibling == self then 103 | table.remove(siblings, index) 104 | return 105 | end 106 | end 107 | end 108 | 109 | --- Resizes this component around its children. 110 | -- 111 | -- @param[opt] padding Amount of padding to put between the component's 112 | -- children and this component's borders. If nil, this will 113 | -- not shrink the component. 114 | function Component:pack(padding) 115 | local width = 0 116 | local height = 0 117 | for _,child in ipairs(self.children) do 118 | width = math.max(width, child.x + child.width) 119 | height = math.max(height, child.y + child.height) 120 | end 121 | if padding == nil then 122 | if self.width < width then 123 | self.width = width 124 | end 125 | if self.height < height then 126 | self.height = height 127 | end 128 | else 129 | self.width = width + padding 130 | self.height = height + padding 131 | end 132 | end 133 | 134 | -- Draws and updates this component, and any children. 135 | function Component:step(dt) 136 | local hoverComponent 137 | if self.mouseOver ~= nil then 138 | if self:contains(GUI.mousePosition) then 139 | hoverComponent = self 140 | else 141 | self.mouseOver = false 142 | end 143 | end 144 | 145 | self:update(dt) 146 | 147 | local layout = self.layout 148 | if layout then 149 | self:calculateOffset() 150 | self.layout = false 151 | end 152 | self:draw(dt) 153 | 154 | for _,child in ipairs(self.children) do 155 | if layout then 156 | child.layout = true 157 | end 158 | if child.visible ~= false then 159 | hoverComponent = child:step(dt) or hoverComponent 160 | end 161 | end 162 | return hoverComponent 163 | end 164 | 165 | --- Updates this component. 166 | -- 167 | -- Components should override this to implement their own update functions. 168 | -- @param dt The time elapsed since the last update, in seconds. 169 | function Component:update(dt) 170 | end 171 | 172 | --- Draws this component 173 | -- 174 | -- Components should override this to implement their own draw functions. 175 | -- @param dt The time elapsed since the last update, in seconds. 176 | function Component:draw(dt) 177 | end 178 | 179 | --- Sets the parent of this component, and updates the offset of this component. 180 | -- 181 | -- @param parent The new parent of this component, or nil if this is to be a 182 | -- top level component. 183 | function Component:setParent(parent) 184 | self.parent = parent 185 | -- self:calculateOffset() 186 | if parent then 187 | parent.layout = true 188 | end 189 | end 190 | 191 | --- Calculates the offset from the origin that this component should use, based 192 | -- on its parents. 193 | -- 194 | -- @return The calculated offset. 195 | function Component:calculateOffset() 196 | local offset = self.offset 197 | 198 | local parent = self.parent 199 | if parent then 200 | offset[1] = parent.offset[1] + parent.x 201 | offset[2] = parent.offset[2] + parent.y 202 | else 203 | offset[1] = 0 204 | offset[2] = 0 205 | end 206 | 207 | return offset 208 | end 209 | 210 | --- Checks if the given position is within this component. 211 | -- 212 | -- @param position The position to check. 213 | -- @return Whether or not the position is within the bounds of this component. 214 | function Component:contains(position) 215 | local pos = {position[1] - self.offset[1], position[2] - self.offset[2]} 216 | 217 | if pos[1] >= self.x and pos[1] <= self.x + self.width 218 | and pos[2] >= self.y and pos[2] <= self.y + self.height 219 | then 220 | return true 221 | end 222 | return false 223 | end 224 | 225 | --- Called when the mouse is pressed or released over this component. 226 | -- @function clickEvent 227 | -- @param position The position of the mouse where the click happened. 228 | -- @param button The mouse button used. 229 | -- @param pressed Whether the mouse was pressed or released. 230 | -- @return If true, consumes the mouse event, blocking it from any underlying 231 | -- components. 232 | 233 | --- Called when the user presses or releases a key when this component has focus. 234 | -- @function keyEvent 235 | -- @param keyCode The key code that was pressed or released. 236 | -- @param pressed Whether the key was pressed or released. 237 | -- @return If true, consumes the key event, blocking it from any parents. 238 | -------------------------------------------------------------------------------- /penguingui/Frame.lua: -------------------------------------------------------------------------------- 1 | --- A window. 2 | -- @classmod Frame 3 | -- @usage 4 | -- -- Create a window 5 | -- local frame = Frame(0, 0) 6 | -- frame.width = 100 7 | -- frame.height = 100 8 | Frame = class(Panel) 9 | --- The color of this frame's border. 10 | Frame.borderColor = {0, 0, 0} 11 | --- The thickness of this frame's border. 12 | Frame.borderThickness = 1 13 | --- The color of this frame. 14 | Frame.backgroundColor = {35, 35, 35} 15 | 16 | --- Constructor 17 | -- @section 18 | 19 | --- Constructs a Frame. 20 | -- 21 | -- @param x The x coordinate of the new component, relative to its parent. 22 | -- @param y The y coordinate of the new component, relative to its parent. 23 | function Frame:_init(x, y) 24 | Panel._init(self, x, y) 25 | end 26 | 27 | --- @section end 28 | 29 | function Frame:update(dt) 30 | if self.dragging then 31 | if self.hasFocus then 32 | local mousePos = GUI.mousePosition 33 | self.x = self.x + (mousePos[1] - self.dragOrigin[1]) 34 | self.y = self.y + (mousePos[2] - self.dragOrigin[2]) 35 | self.layout = true 36 | self.dragOrigin = mousePos 37 | else 38 | self.dragging = false 39 | end 40 | end 41 | end 42 | 43 | function Frame:draw(dt) 44 | local startX = self.x - self.offset[1] 45 | local startY = self.y - self.offset[2] 46 | local w = self.width 47 | local h = self.height 48 | local border = self.borderThickness 49 | 50 | local borderRect = { 51 | startX, startY, 52 | startX + w, startY + h 53 | } 54 | local backgroundRect = { 55 | startX + border, startY + border, 56 | startX + w - border, startY + h - border 57 | } 58 | 59 | PtUtil.drawRect(borderRect, self.borderColor, border) 60 | PtUtil.fillRect(backgroundRect, self.backgroundColor) 61 | end 62 | 63 | function Frame:clickEvent(position, button, pressed) 64 | if pressed then 65 | self.dragging = true 66 | self.dragOrigin = position 67 | else 68 | self.dragging = false 69 | end 70 | return true 71 | end 72 | -------------------------------------------------------------------------------- /penguingui/GUI.lua: -------------------------------------------------------------------------------- 1 | --- Base handler for all the GUI stuff. 2 | -- @module GUI 3 | -- @usage -- A simple script that creates a button that closes the console. 4 | -- function init() 5 | -- local button = TextButton(0, 0, 100, 16, "Close") 6 | -- button.onClick = function(mouseButton) 7 | -- console.dismiss() 8 | -- end 9 | -- GUI.add(button) 10 | -- end 11 | -- 12 | -- function update(dt) 13 | -- GUI.step(dt) 14 | -- end 15 | -- 16 | -- function canvasClickEvent(position, button, pressed) 17 | -- GUI.clickEvent(position, button, pressed) 18 | -- end 19 | -- 20 | -- function canvasKeyEvent(key, isKeyDown) 21 | -- GUI.keyEvent(key, isKeyDown) 22 | -- end 23 | 24 | --- GUI Table 25 | -- @field components Top-level components to be managed. 26 | -- @field mouseState The state of each of the mouse buttons. 27 | -- @field keyState The state of each keyboard key. 28 | -- @field mousePosition The current position of the mouse. 29 | -- @table GUI 30 | GUI = { 31 | components = {}, 32 | mouseState = {}, 33 | keyState = {}, 34 | mousePosition = {0, 0} 35 | } 36 | 37 | --- Add a new top-level component to be handled by PenguinGUI. 38 | -- @param component The top-level component to be added. 39 | function GUI.add(component) 40 | GUI.components[#GUI.components + 1] = component 41 | component:setParent(nil) 42 | end 43 | 44 | --- Removes a component to be handled. 45 | -- 46 | -- @param component The component to be removed. 47 | -- @return Whether the component was removed. 48 | function GUI.remove(component) 49 | for index,comp in ripairs(GUI.components) do 50 | if (comp == component) then 51 | component:removeSelf() 52 | return true 53 | end 54 | end 55 | return false 56 | end 57 | 58 | --- Sets the keyboard focus of the GUI to the specified component. 59 | -- @param component The component to receive keyboard focus. 60 | function GUI.setFocusedComponent(component) 61 | local focusedComponent = GUI.focusedComponent 62 | if focusedComponent then 63 | focusedComponent.hasFocus = false 64 | end 65 | GUI.focusedComponent = component 66 | component.hasFocus = true 67 | end 68 | 69 | --- Must be called in canvasClickEvent to handle mouse events. 70 | -- 71 | -- @param position The position of the click. 72 | -- @param button The mouse button of the click. 73 | -- @param pressed Whether the event is pressed or released. 74 | function GUI.clickEvent(position, button, pressed) 75 | GUI.mouseState[button] = pressed 76 | local components = GUI.components 77 | local topFound = false 78 | for index,component in ripairs(components) do 79 | if component.visible ~= false then 80 | -- Focus top-level components 81 | if not topFound then 82 | if component:contains(position) then 83 | table.remove(components, index) 84 | components[#components + 1] = component 85 | topFound = true 86 | end 87 | end 88 | if GUI.clickEventHelper(component, position, button, pressed) then 89 | -- The click was consumed 90 | break 91 | end 92 | end 93 | end 94 | end 95 | 96 | function GUI.clickEventHelper(component, position, button, pressed) 97 | local children = component.children 98 | for _,child in ripairs(children) do 99 | if child.visible ~= false then 100 | if GUI.clickEventHelper(child, position, button, pressed) then 101 | -- The click was consumed 102 | return true 103 | end 104 | end 105 | end 106 | -- Only check bounds if the component has a clickEvent 107 | if component.clickEvent then 108 | if component:contains(position) then 109 | if component:clickEvent(position, button, pressed) then 110 | -- The click was consumed 111 | GUI.setFocusedComponent(component) 112 | return true 113 | end 114 | end 115 | end 116 | end 117 | 118 | --- Must be called in canvasKeyEvent to handle keyboard events. 119 | -- 120 | -- @param key The keycode of the key that spawned the event. 121 | -- @param pressed Whether the key was pressed or released. 122 | function GUI.keyEvent(key, pressed) 123 | GUI.keyState[key] = pressed 124 | local component = GUI.focusedComponent 125 | while component do 126 | if component.visible ~= false then 127 | local keyEvent = component.keyEvent 128 | if keyEvent then 129 | if keyEvent(component, key, pressed) then 130 | -- Key was consumed 131 | return 132 | end 133 | end 134 | end 135 | component = component.parent 136 | end 137 | end 138 | 139 | --- Draws and updates every component managed by this GUI. 140 | -- 141 | -- Must be called in update() for the GUI to update and render. 142 | -- @param dt The time elapsed since the last draw, in seconds. 143 | function GUI.step(dt) 144 | GUI.mousePosition = console.canvasMousePosition() 145 | local hoverComponent 146 | for _,component in ipairs(GUI.components) do 147 | if component.visible ~= false then 148 | hoverComponent = component:step(dt) or hoverComponent 149 | end 150 | end 151 | if hoverComponent then 152 | hoverComponent.mouseOver = true 153 | end 154 | end 155 | 156 | -------------------------------------------------------------------------------- /penguingui/HorizontalLayout.lua: -------------------------------------------------------------------------------- 1 | --- Lays out components horizontally. 2 | -- @classmod HorizontalLayout 3 | -- @usage 4 | -- -- Create a horizontal layout manager with padding of 2. 5 | -- local layout = HorizontalLayout(2) 6 | HorizontalLayout = class() 7 | 8 | --- Padding between contained components. 9 | HorizontalLayout.padding = nil 10 | --- Vertical alignment of contained components. 11 | -- Default center. 12 | HorizontalLayout.vAlignment = Align.CENTER 13 | --- Horizontal alignment of contained components. 14 | -- Default left. 15 | HorizontalLayout.hAlignment = Align.LEFT 16 | 17 | --- Constructor 18 | -- @section 19 | 20 | --- Constructs a HorizontalLayout. 21 | -- 22 | -- @param[opt=0] padding The padding between components within this layout. 23 | -- @param[opt=Align.LEFT] hAlign The horizontal alignment of the components. 24 | -- @param[opt=Align.CENTER] vAlign The vertical alignment of the components. 25 | function HorizontalLayout:_init(padding, hAlign, vAlign) 26 | self.padding = padding or 0 27 | if hAlign then 28 | self.hAlignment = hAlign 29 | end 30 | if vAlign then 31 | self.vAlignment = vAlign 32 | end 33 | end 34 | 35 | --- @section end 36 | 37 | function HorizontalLayout:layout() 38 | local vAlign = self.vAlignment 39 | local hAlign = self.hAlignment 40 | local padding = self.padding 41 | 42 | local container = self.container 43 | local components = container.children 44 | local totalWidth = 0 45 | for _,component in ipairs(components) do 46 | totalWidth = totalWidth + component.width 47 | end 48 | totalWidth = totalWidth + (#components - 1) * padding 49 | 50 | local startX 51 | if hAlign == Align.LEFT then 52 | startX = 0 53 | elseif hAlign == Align.CENTER then 54 | startX = (container.width - totalWidth) / 2 55 | else -- ALIGN_RIGHT 56 | startX = container.width - totalWidth 57 | end 58 | 59 | for _,component in ipairs(components) do 60 | component.x = startX 61 | if vAlign == Align.TOP then 62 | component.y = container.height - component.height 63 | elseif vAlign == Align.CENTER then 64 | component.y = (container.height - component.height) / 2 65 | else -- ALIGN_BOTTOM 66 | component.y = 0 67 | end 68 | startX = startX + component.width + padding 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /penguingui/Image.lua: -------------------------------------------------------------------------------- 1 | --- An image. 2 | -- @classmod Image 3 | -- @usage 4 | -- -- Create a image 5 | -- local image = Image(0, 0, "/path/to/image.png") 6 | Image = class(Component) 7 | 8 | --- Constructor 9 | -- @section 10 | 11 | --- Constructs a new Image. 12 | -- 13 | -- @param x The x coordinate of the new component, relative to its parent. 14 | -- @param y The y coordinate of the new component, relative to its parent. 15 | -- @param image The path to the image. 16 | -- @param scale (Optional) Factor to scale the image by. 17 | function Image:_init(x, y, image, scale) 18 | Component._init(self) 19 | scale = scale or 1 20 | 21 | self.x = x 22 | self.y = y 23 | local imageSize = root.imageSize(image) 24 | self.width = imageSize[1] * scale 25 | self.height = imageSize[2] * scale 26 | self.image = image 27 | self.scale = scale 28 | end 29 | 30 | --- @section end 31 | 32 | function Image:draw(dt) 33 | local startX = self.x + self.offset[1] 34 | local startY = self.y + self.offset[2] 35 | local image = self.image 36 | local scale = self.scale 37 | 38 | PtUtil.drawImage(image, {startX, startY}, scale) 39 | end 40 | -------------------------------------------------------------------------------- /penguingui/Label.lua: -------------------------------------------------------------------------------- 1 | --- A text label for displaying text. 2 | -- @classmod Label 3 | -- @usage 4 | -- -- Create a label with the text "Hello" 5 | -- local label = Label(0, 0, "Hello") 6 | Label = class(Component) 7 | --- The text of the label. 8 | Label.text = nil 9 | 10 | --- Constructor 11 | -- @section 12 | 13 | --- Constructs a new Label. 14 | -- 15 | -- @param x The x coordinate of the new component, relative to its parent. 16 | -- @param y The y coordinate of the new component, relative to its parent. 17 | -- @param text The text string to display on the button. The button's size will 18 | -- be based on this string. 19 | -- @param fontSize (optional) The font size of the text to display, default 10. 20 | -- @param fontColor (optional) The color of the text to display, default white. 21 | function Label:_init(x, y, text, fontSize, fontColor) 22 | Component._init(self) 23 | fontSize = fontSize or 10 24 | self.fontSize = fontSize 25 | self.fontColor = fontColor or {255, 255, 255} 26 | self.text = text 27 | self.x = x 28 | self.y = y 29 | self:addListener("text", self.recalculateBounds) 30 | self:recalculateBounds() 31 | end 32 | 33 | --- @section end 34 | 35 | -- Recalculates the bounds of the label based on its text and font size. 36 | function Label:recalculateBounds() 37 | self.width = PtUtil.getStringWidth(self.text, self.fontSize) 38 | self.height = self.fontSize 39 | end 40 | 41 | function Label:draw(dt) 42 | local startX = self.x + self.offset[1] 43 | local startY = self.y + self.offset[2] 44 | 45 | --PtUtil.fillRect({startX, startY, startX + self.width, startY + self.height}, 46 | -- "red") 47 | PtUtil.drawText(self.text, { 48 | position = {startX, startY}, 49 | verticalAnchor = "bottom" 50 | }, self.fontSize, self.fontColor) 51 | end 52 | -------------------------------------------------------------------------------- /penguingui/Line.lua: -------------------------------------------------------------------------------- 1 | --- A line. 2 | -- @classmod Line 3 | -- @usage 4 | -- -- Create a horizontal line 5 | -- local line = Line(0, 10, 20, 10) 6 | Line = class(Component) 7 | --- The color of the line. 8 | Line.color = {0, 0, 0} 9 | --- The width of the line. 10 | Line.size = 1 11 | 12 | --- Constructor 13 | -- @section 14 | 15 | --- Constructs a new Line. 16 | -- @param x The x coordinate of the new component, relative to its parent. 17 | -- @param y The y coordinate of the new component, relative to its parent. 18 | -- @param endX The ending x coordinate of the line. 19 | -- @param endY The ending y coordinate of the line. 20 | -- @param[opt="black"] color The color of the new component. 21 | -- @param[opt=1] lineSize The width of the line. 22 | function Line:_init(x, y, endX, endY, color, lineSize) 23 | Component._init(self) 24 | self.x = x 25 | self.y = y 26 | self.endX = x 27 | self.endY = y 28 | self.width = endX - x 29 | self.height = endY - y 30 | self.color = color 31 | self.size = size 32 | end 33 | 34 | -- @section end 35 | 36 | function Line:draw(dt) 37 | local offset = self.offset 38 | local startX = self.x + offset[1] 39 | local startY = self.y + offset[2] 40 | local endX = self.endX + offset[1] 41 | local endY = self.endY + offset[2] 42 | 43 | local size = self.size 44 | local color = self.color 45 | PtUtil.drawLine({startX, startY}, {endX, endY}, color, width) 46 | end 47 | 48 | -------------------------------------------------------------------------------- /penguingui/List.lua: -------------------------------------------------------------------------------- 1 | --- A scrollable list 2 | -- @classmod List 3 | -- @usage 4 | -- -- Create a list of 20 items 5 | -- local list = List(0, 0, 100, 100, 12) 6 | -- for i=1,20,1 do 7 | -- local item = list:emplaceItem("Item " .. i) 8 | -- item:addListener("selected", function() world.logInfo(item.text) end) 9 | -- end 10 | List = class(Component) 11 | --- The border color of this list. 12 | List.borderColor = {84, 84, 84} 13 | --- The thickness of this list's border. 14 | List.borderSize = 1 15 | --- The background color of this list. 16 | List.backgroundColor = {0, 0, 0} 17 | --- Padding in between items. 18 | List.itemPadding = 2 19 | --- Thickness of the scroll bar 20 | List.scrollBarSize = 3 21 | 22 | --- Constructor 23 | -- @section 24 | 25 | --- Constructs a new List. 26 | -- New lists are by default vertical. 27 | -- @param x The x coordinate of the new component, relative to its parent. 28 | -- @param y The y coordinate of the new component, relative to its parent. 29 | -- @param width The width of the new component. 30 | -- @param height The height of the new component. 31 | -- @param itemSize The height(if vertical) or width (if horizontal) of each 32 | -- list item. 33 | -- @param[opt] itemFactory A function to return a new item. The arguments should 34 | -- be x, y, width, height. Defaults to creating TextRadioButtons. 35 | -- @param[opt=false] horizontal If true, this list will be horizontal. 36 | function List:_init(x, y, width, height, itemSize, itemFactory, horizontal) 37 | Component._init(self) 38 | self.x = x 39 | self.y = y 40 | self.width = width 41 | self.height = height 42 | self.itemSize = itemSize 43 | self.itemFactory = itemFactory or TextRadioButton 44 | self.items = {} 45 | self.topIndex = 1 46 | self.bottomIndex = 1 47 | self.itemCount = 0 48 | self.horizontal = horizontal 49 | self.mouseOver = false 50 | 51 | -- Create scrollbar 52 | local borderSize = self.borderSize 53 | local barSize = self.scrollBarSize 54 | local slider 55 | if horizontal then 56 | slider = Slider(borderSize + 0.5, borderSize + 0.5 57 | , width - borderSize * 2 - 1, barSize, 0, 0, 1, false) 58 | else 59 | slider = Slider(width - borderSize - barSize - 0.5 60 | , borderSize + 0.5, barSize 61 | , height - borderSize * 2 - 1, 0, 0, 1, true) 62 | end 63 | slider.lineSize = 0 64 | slider.handleBorderSize = 0 65 | slider.handleColor = {84, 84, 84} 66 | slider.handleHoverColor = {120, 120, 120} 67 | slider.handlePressedColor = {160, 160, 160} 68 | slider:addListener( 69 | "value", 70 | function(t, k, old, new) 71 | local list = t.parent 72 | if list.horizontal then 73 | list.topIndex = new + 1 74 | else 75 | list.topIndex = t.maxValue - t.value + 1 76 | end 77 | list:positionItems() 78 | end 79 | ) 80 | self.slider = slider 81 | self:add(slider) 82 | 83 | self:positionItems() 84 | end 85 | 86 | --- @section end 87 | 88 | function List:draw(dt) 89 | local startX = self.x + self.offset[1] 90 | local startY = self.y + self.offset[2] 91 | local w = self.width 92 | local h = self.height 93 | 94 | local borderSize = self.borderSize 95 | local borderColor = self.borderColor 96 | local borderRect = {startX, startY, startX + w, startY + h} 97 | local rect = {startX + 1, startY + 1, startX + w - 1, startY + h - 1} 98 | PtUtil.drawRect(borderRect, borderColor, borderSize) 99 | PtUtil.fillRect(rect, self.backgroundColor) 100 | 101 | -- Draw scroll bar border 102 | local scrollBarSize = self.scrollBarSize 103 | if self.horizontal then 104 | local lineY = startY + borderSize + scrollBarSize + 1.5 105 | PtUtil.drawLine({startX, lineY}, {startX + w, lineY}, borderColor, 1) 106 | else 107 | local lineX = startX + w - borderSize - scrollBarSize - 1.5 108 | PtUtil.drawLine({lineX, startY}, {lineX, startY + h}, borderColor, 1) 109 | end 110 | end 111 | 112 | --- Constructs and adds an item to this list. 113 | -- @param ... Arguments to pass to the item constructor. 114 | -- @return The newly created list item. 115 | -- @return The list index of the new list item. 116 | function List:emplaceItem(...) 117 | local width 118 | local height 119 | if self.horizontal then 120 | width = self.itemSize 121 | height = self.height - (self.borderSize * 2 122 | + self.itemPadding * 2 123 | + self.scrollBarSize + 2) 124 | else 125 | width = self.width - (self.borderSize * 2 126 | + self.itemPadding * 2 127 | + self.scrollBarSize + 2) 128 | height = self.itemSize 129 | end 130 | item = self.itemFactory(0, 0, width, height, ...) 131 | return self:addItem(item) 132 | end 133 | 134 | --- Adds an item to this list. 135 | -- @param item The item to add to this list. 136 | -- @return The item added to this list. 137 | -- @return The list index of the added item. 138 | function List:addItem(item) 139 | self:add(item) 140 | local items = self.items 141 | local index = #items + 1 142 | items[index] = item 143 | self.itemCount = self.itemCount + 1 144 | self:positionItems() 145 | return item, index 146 | end 147 | 148 | --- Removes an item from this list. 149 | -- @param target Either the item to remove, or the index of the item to remove. 150 | -- @return The removed item, or nil if the item was not removed. 151 | -- @return The index of the removed item, or -1 if the item was not removed. 152 | function List:removeItem(target) 153 | local item 154 | local index 155 | if type(target) == "number" then -- Remove by index 156 | index = target 157 | item = table.remove(self.items, index) 158 | if not item then 159 | return nil, -1 160 | end 161 | else -- Remove by item 162 | if target == nil then 163 | return nil 164 | end 165 | item = target 166 | index = PtUtil.removeObject(self.items, item) 167 | if index == -1 then 168 | return nil, -1 169 | end 170 | end 171 | self:remove(item) 172 | if not item.filtered then 173 | self.itemCount = self.itemCount - 1 174 | end 175 | if self.bottomIndex > self.itemCount + 1 then 176 | self:scroll(true) 177 | else 178 | self:positionItems() 179 | end 180 | return item, index 181 | end 182 | 183 | --- Removes all items from this list. 184 | function List:clearItems() 185 | for index,item in ripairs(self.items) do 186 | self:removeItem(index) 187 | end 188 | end 189 | 190 | --- Retrieve the item at a given index. 191 | -- @param index The index to retrieve the item from. 192 | -- @return The item at the given index, or nil if there is none. 193 | function List:getItem(index) 194 | return self.items[index] 195 | end 196 | 197 | --- Get the index of the given item. 198 | -- @param item The item to find the index of. 199 | -- @return The index of the item, or -1 if the item was not found. 200 | function List:indexOfItem(item) 201 | for index,obj in ipairs(self.items) do 202 | if item == obj then 203 | return index 204 | end 205 | end 206 | return -1 207 | end 208 | 209 | --- Filters the items in this list. 210 | -- @param filter A function that should take a list item as the argument, and 211 | -- return true to show that item or false to hide that item. If nil, show 212 | -- all items. 213 | function List:filter(filter) 214 | local itemCount = 0 215 | if filter then 216 | for _,item in ipairs(self.items) do 217 | if not filter(item) then 218 | item.filtered = true 219 | else 220 | itemCount = itemCount + 1 221 | item.filtered = nil 222 | end 223 | end 224 | else 225 | for _,item in ipairs(self.items) do 226 | itemCount = itemCount + 1 227 | item.filtered = nil 228 | end 229 | end 230 | self.itemCount = itemCount 231 | self.topIndex = 1 232 | self:positionItems() 233 | end 234 | 235 | -- Positions and clips items 236 | function List:positionItems() 237 | local items = self.items 238 | local padding = self.itemPadding 239 | local border = self.borderSize 240 | local topIndex = self.topIndex 241 | local itemSize = self.itemSize 242 | local current 243 | local min 244 | if self.horizontal then 245 | current = border 246 | min = border + padding 247 | else 248 | current = self.height - border 249 | min = border + padding 250 | end 251 | local past = false 252 | local itemCount = 0 253 | local possibleItemCount = 1 254 | for i,item in ipairs(items) do 255 | if possibleItemCount < topIndex and not item.filtered then 256 | item.visible = false 257 | possibleItemCount = possibleItemCount + 1 258 | elseif past or item.filtered then 259 | item.visible = false 260 | else 261 | itemCount = itemCount + 1 262 | item.visible = nil 263 | if self.horizontal then 264 | item.y = min 265 | current = current + (padding + itemSize) 266 | item.x = current 267 | if current + itemSize > self.width - borderSize then 268 | item.visible = false 269 | self.bottomIndex = itemCount + topIndex - 1 270 | past = true 271 | end 272 | else 273 | item.x = min 274 | current = current - (padding + itemSize) 275 | item.y = current 276 | if current < border then 277 | item.visible = false 278 | self.bottomIndex = itemCount + topIndex - 1 279 | past = true 280 | end 281 | end 282 | end 283 | item.layout = true 284 | end 285 | if not past then 286 | self.bottomIndex = topIndex + itemCount 287 | end 288 | self:updateScrollBar() 289 | end 290 | 291 | -- Calculate scroll bar stuff 292 | function List:updateScrollBar() 293 | local maxLength 294 | local slider = self.slider 295 | local offset 296 | if self.horizontal then 297 | maxLength = slider.width 298 | else 299 | maxLength = slider.height 300 | end 301 | local items = self.items 302 | local topIndex = self.topIndex 303 | local bottomIndex = self.bottomIndex 304 | local itemCount = self.itemCount 305 | if bottomIndex > itemCount and topIndex == 1 then 306 | slider.handleSize = maxLength 307 | slider.maxValue = 0 308 | else 309 | local numItems = bottomIndex - topIndex -- Number of displayed items 310 | local barLength = math.max( 311 | numItems * maxLength / itemCount, 312 | self.scrollBarSize) 313 | slider.handleSize = barLength 314 | slider.maxValue = itemCount - numItems 315 | if self.horizontal then 316 | slider.value = topIndex - 1 317 | else 318 | slider.value = slider.maxValue - (topIndex - 1) 319 | end 320 | end 321 | end 322 | 323 | --- Scroll the list one item. 324 | -- 325 | -- @param up Up or down. 326 | function List:scroll(up) 327 | if up then 328 | self.topIndex = math.max(self.topIndex - 1, 1) 329 | else 330 | if self.bottomIndex <= self.itemCount then 331 | self.topIndex = self.topIndex + 1 332 | end 333 | end 334 | self:positionItems() 335 | end 336 | 337 | function List:clickEvent(position, button, pressed) 338 | if button >= 4 then -- scroll 339 | if pressed then 340 | if button == 4 then -- Scroll up 341 | self:scroll(true) 342 | else -- Scroll down 343 | self:scroll(false) 344 | end 345 | end 346 | return true 347 | end 348 | end 349 | -------------------------------------------------------------------------------- /penguingui/Panel.lua: -------------------------------------------------------------------------------- 1 | --- A group of components. 2 | -- @classmod Panel 3 | -- @usage 4 | -- -- Create an empty panel. 5 | -- local panel = Panel(0, 0) 6 | Panel = class(Component) 7 | 8 | --- Constructor 9 | -- @section 10 | 11 | --- Constructs a Panel. 12 | -- 13 | -- @param x The x coordinate of the new component, relative to its parent. 14 | -- @param y The y coordinate of the new component, relative to its parent. 15 | -- @param width[opt] The width of the new component. 16 | -- @param height[opt] The height of the new component. 17 | function Panel:_init(x, y, width, height) 18 | Component._init(self) 19 | self.x = x 20 | self.y = y 21 | if width then 22 | self.width = width 23 | end 24 | if height then 25 | self.height = height 26 | end 27 | end 28 | 29 | --- @section end 30 | 31 | function Panel:add(child) 32 | Component.add(self, child) 33 | self:updateLayoutManager() 34 | if not self.layoutManager then 35 | self:pack() 36 | end 37 | end 38 | 39 | --- Sets the layout manager for this Panel. 40 | -- THIS IS WIP. THE API IS LIKELY TO CHANGE. 41 | -- @param layout The new layout manager for this panel, or nil to clear the 42 | -- layout manager. 43 | function Panel:setLayoutManager(layout) 44 | self.layoutManager = layout 45 | layout.container = self 46 | self:updateLayoutManager() 47 | end 48 | 49 | --- Updates the components in this panel according to its layout manager. 50 | function Panel:updateLayoutManager() 51 | local layout = self.layoutManager 52 | if layout then 53 | layout:layout() 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /penguingui/RadioButton.lua: -------------------------------------------------------------------------------- 1 | --- A radio button 2 | -- 3 | -- Extends @{CheckBox}, so the fields RadioButton uses for drawing are the same 4 | -- as those of @{CheckBox}. 5 | -- @classmod RadioButton 6 | -- @usage 7 | -- -- Create a group of buttons that print when they are selected. 8 | -- local group = Panel(0, 0) 9 | -- local numButtons = 10 10 | -- for i = 1, numButtons, 1 do 11 | -- local button = RadioButton(i * 20, 0, 16) 12 | -- button:addListener("selected", function(t, k, old, new) 13 | -- if new then 14 | -- print("Button " .. i .. " was selected") 15 | -- end 16 | -- end) 17 | -- group:add(button) 18 | -- end 19 | RadioButton = class(CheckBox) 20 | 21 | --- Constructor 22 | -- @section 23 | 24 | --- Constructs a new RadioButton. 25 | -- @function _init 26 | -- @param x The x coordinate of the new component, relative to its parent. 27 | -- @param y The y coordinate of the new component, relative to its parent. 28 | -- @param size The width and height of the new component. 29 | 30 | --- @section end 31 | 32 | function RadioButton:drawCheck(dt) 33 | -- Draw squares since no efficient way to fill circles yet. 34 | local startX = self.x + self.offset[1] 35 | local startY = self.y + self.offset[2] 36 | local w = self.width 37 | local h = self.height 38 | local checkRect = {startX + w / 4, startY + h / 4, 39 | startX + 3 * w / 4, startY + 3 * h / 4} 40 | PtUtil.fillRect(checkRect, self.checkColor) 41 | end 42 | 43 | -- Select this RadioButton 44 | function RadioButton:select() 45 | local siblings 46 | if self.parent == nil then 47 | siblings = GUI.components 48 | else 49 | siblings = self.parent.children 50 | end 51 | 52 | local selectedButton 53 | for _,sibling in ipairs(siblings) do 54 | if sibling ~= self and sibling.is_a[RadioButton] 55 | and sibling.selected 56 | then 57 | selectedButton = sibling 58 | end 59 | end 60 | if selectedButton then 61 | selectedButton.selected = false 62 | end 63 | 64 | if not self.selected then 65 | self.selected = true 66 | end 67 | end 68 | 69 | function RadioButton:setParent(parent) 70 | Component.setParent(self, parent) 71 | local siblings 72 | if self.parent == nil then 73 | siblings = GUI.components 74 | else 75 | siblings = self.parent.children 76 | end 77 | 78 | for _,sibling in ipairs(siblings) do 79 | if sibling ~= self and sibling.is_a[RadioButton] and sibling.selected then 80 | return 81 | end 82 | end 83 | self.selected = true 84 | end 85 | 86 | function RadioButton:removeSelf() 87 | CheckBox.removeSelf(self) 88 | if self.selected then 89 | local siblings 90 | if self.parent == nil then 91 | siblings = GUI.components 92 | else 93 | siblings = self.parent.children 94 | end 95 | 96 | for _,sibling in ipairs(siblings) do 97 | if sibling.is_a[RadioButton] then 98 | sibling:select() 99 | return 100 | end 101 | end 102 | end 103 | end 104 | 105 | function RadioButton:clickEvent(position, button, pressed) 106 | if button <= 3 then 107 | if not pressed and self.pressed then 108 | self:select() 109 | end 110 | self.pressed = pressed 111 | return true 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /penguingui/Rectangle.lua: -------------------------------------------------------------------------------- 1 | --- A rectangle. 2 | -- @classmod Rectangle 3 | -- @usage 4 | -- -- Create a filled square 5 | -- local rect = Rectangle(0, 0, 10, 10) 6 | Rectangle = class(Component) 7 | --- The color of the rectangle. 8 | Rectangle.color = {0, 0, 0} 9 | --- The width of the line, if not filled. 10 | Rectangle.lineSize = nil 11 | 12 | --- Constructor 13 | -- @section 14 | 15 | --- Constructs a new Rectangle. 16 | -- @param x The x coordinate of the new component, relative to its parent. 17 | -- @param y The y coordinate of the new component, relative to its parent. 18 | -- @param width The width of the new component. 19 | -- @param height The height of the new component. 20 | -- @param[opt="black"] color The color of the new component. 21 | -- @param[opt=nil] lineSize The width of the line, if nil, the rectangle will 22 | -- be filled. 23 | function Rectangle:_init(x, y, width, height, color, lineSize) 24 | Component._init(self) 25 | self.x = x 26 | self.y = y 27 | self.width = width 28 | self.height = height 29 | self.color = color 30 | self.lineSize = lineSize 31 | end 32 | 33 | -- @section end 34 | 35 | function Rectangle:draw(dt) 36 | local startX = self.x + self.offset[1] 37 | local startY = self.y + self.offset[2] 38 | local w = self.width 39 | local h = self.height 40 | 41 | local rect = {startX, startY, startX + w, startY + h} 42 | local lineSize = self.lineSize 43 | local color = self.color 44 | if lineSize then 45 | PtUtil.drawRect(rect, color, lineSize) 46 | else 47 | PtUtil.fillRect(rect, color) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /penguingui/Slider.lua: -------------------------------------------------------------------------------- 1 | --- A slider 2 | -- @classmod Slider 3 | -- @usage 4 | -- -- Create a slider 5 | -- local slider = Slider(0, 0, 100, 12) 6 | -- slider:addListener( 7 | -- "value", 8 | -- function(t, k, old, new) 9 | -- world.logInfo("Slider is now at %s", new) 10 | -- end) 11 | Slider = class(Component) 12 | --- The color of the slider line. 13 | Slider.lineColor = {0, 0, 0} 14 | --- The thickness of the slider line. 15 | Slider.lineSize = 2 16 | --- The color of the slider handle border. 17 | Slider.handleBorderColor = {177, 177, 177} 18 | --- The thickness of the slider handle border. 19 | Slider.handleBorderSize = 1 20 | --- The color of the slider handle. 21 | Slider.handleColor = Slider.lineColor 22 | --- The color of the slider handle when the mouse is over it. 23 | Slider.handleHoverColor = {50, 50, 50} 24 | --- The color of the slider handle when it is being dragged. 25 | Slider.handlePressedColor = {84, 84, 84} 26 | --- The size of the slider handle. 27 | Slider.handleSize = 5 28 | --- The current value of the slider. 29 | Slider.value = nil 30 | --- The maximum value of the slider. 31 | Slider.maxValue = nil 32 | --- The minimum value of the slider. 33 | Slider.minValue = nil 34 | 35 | --- Constructor 36 | -- @section 37 | 38 | --- Constructs a new Slider. 39 | -- New sliders are by default horizontal. 40 | -- @param x The x coordinate of the new component, relative to its parent. 41 | -- @param y The y coordinate of the new component, relative to its parent. 42 | -- @param width The width of the new component. 43 | -- @param height The height of the new component. 44 | -- @param[opt=0.0] min The maximum value of this slider. The slider will slide from 45 | -- min to max. 46 | -- @param[opt=1.0] max The maximum value of this slider. The slider will slide from 47 | -- min to max. 48 | -- @param[opt=nil] step The step size to snap the slider to. If nil, the slider will 49 | -- slide smoothly. 50 | -- @param[opt=false] vertical If true, the slider will be vertical. 51 | function Slider:_init(x, y, width, height, min, max, step, vertical) 52 | Component._init(self) 53 | self.x = x 54 | self.y = y 55 | self.width = width 56 | self.height = height 57 | self.mouseOver = false 58 | self.minValue = min or 0 59 | self.maxValue = max or 1 60 | self.valueStep = step 61 | self.vertical = vertical 62 | self.value = self.minValue 63 | self:addListener( 64 | "maxValue", 65 | function(t, k, old, new) 66 | if t.value > new then 67 | t.value = new 68 | end 69 | end 70 | ) 71 | end 72 | 73 | --- @section end 74 | 75 | function Slider:update(dt) 76 | if self.dragging then 77 | if not GUI.mouseState[1] then 78 | self.dragging = false 79 | else 80 | local mousePos = GUI.mousePosition 81 | local lineSize = self.lineSize 82 | local min = self.minValue 83 | local max = self.maxValue 84 | local len = max - min 85 | local step = self.valueStep 86 | local sliderValue 87 | if self.vertical then 88 | sliderValue = (mousePos[2] - self.dragOffset 89 | - (self.y + self.offset[2] + lineSize) 90 | ) / (self.height - lineSize * 2 - self.handleSize) * len 91 | else 92 | sliderValue = (mousePos[1] - self.dragOffset 93 | - (self.x + self.offset[1] + lineSize) 94 | ) / (self.width - lineSize * 2 - self.handleSize) * len 95 | end 96 | if sliderValue ~= sliderValue then -- sliderValue is NaN 97 | sliderValue = 0 98 | end 99 | sliderValue = math.max(sliderValue, 0) 100 | sliderValue = math.min(sliderValue, len) 101 | if step then 102 | local stepFreq = 1 / step 103 | sliderValue = math.floor(sliderValue * stepFreq + 0.5) / stepFreq 104 | end 105 | sliderValue = sliderValue + min 106 | self.value = sliderValue 107 | end 108 | end 109 | if self.moving ~= nil then 110 | if not GUI.mouseState[1] then 111 | self.moving = nil 112 | else 113 | local step = self.valueStep 114 | local direction = self.moving 115 | local max = self.maxValue 116 | local min = self.minValue 117 | local len = max - min 118 | if not step then 119 | step = len / 100 120 | end 121 | local value = self.value 122 | if direction then 123 | self.value = math.min(value + step, max) 124 | else 125 | self.value = math.max(value - step, min) 126 | end 127 | end 128 | end 129 | end 130 | 131 | function Slider:draw(dt) 132 | local startX = self.x + self.offset[1] 133 | local startY = self.y + self.offset[2] 134 | local w = self.width 135 | local h = self.height 136 | 137 | local lineSize = self.lineSize 138 | local lineColor = self.lineColor 139 | local percentage = self:getPercentage() 140 | local handleBorderSize = self.handleBorderSize 141 | local handleSize = self.handleSize 142 | local slidableLength 143 | local handleBorderRect 144 | local handleRect 145 | if self.vertical then 146 | PtUtil.drawLine({startX + w / 2, startY}, 147 | {startX + w / 2, startY + h}, lineColor, lineSize) 148 | PtUtil.drawLine({startX, startY + lineSize / 2} 149 | , {startX + w, startY + lineSize / 2}, lineColor, lineSize) 150 | PtUtil.drawLine({startX, startY + h - lineSize / 2} 151 | , {startX + w, startY + h - lineSize / 2}, lineColor, lineSize) 152 | 153 | slidableLength = h - lineSize * 2 - handleSize 154 | local sliderY = startY + lineSize + percentage * slidableLength 155 | handleBorderRect = {startX, sliderY, startX + w, sliderY + handleSize} 156 | handleRect = {startX + handleBorderSize, sliderY + handleBorderSize 157 | , startX + w - handleBorderSize 158 | , sliderY + handleSize - handleBorderSize} 159 | else 160 | PtUtil.drawLine({startX, startY + h / 2}, 161 | {startX + w, startY + h / 2}, self.lineColor, self.lineSize) 162 | PtUtil.drawLine({startX + lineSize / 2, startY}, 163 | {startX + lineSize / 2, startY + h}, lineColor, lineSize) 164 | PtUtil.drawLine({startX + w - lineSize / 2, startY}, 165 | {startX + w - lineSize / 2, startY + h}, lineColor, lineSize) 166 | 167 | slidableLength = w - lineSize * 2 - handleSize 168 | local sliderX = startX + lineSize + percentage * slidableLength 169 | handleBorderRect = {sliderX, startY, sliderX + handleSize, startY + h} 170 | handleRect = {sliderX + handleBorderSize, startY + handleBorderSize 171 | , sliderX + handleSize - handleBorderSize 172 | , startY + h - handleBorderSize} 173 | end 174 | PtUtil.drawRect(handleBorderRect, self.handleBorderColor, handleBorderSize) 175 | local handleColor 176 | if self.dragging then 177 | handleColor = self.handlePressedColor 178 | elseif self.mouseOver then 179 | handleColor = self.handleHoverColor 180 | else 181 | handleColor = self.handleColor 182 | end 183 | PtUtil.fillRect(handleRect, handleColor) 184 | end 185 | 186 | --- Get the percentage of max that this slider's value is at. 187 | -- @return A percentage, from 0 to 1 inclusive. 188 | function Slider:getPercentage() 189 | local min = self.minValue 190 | local max = self.maxValue 191 | local len = max - min 192 | if len == 0 then 193 | return 0 194 | else 195 | return (self.value - min) / len 196 | end 197 | end 198 | 199 | function Slider:clickEvent(position, button, pressed) 200 | if button == 1 then -- Only react to LMB 201 | if pressed then 202 | local startX = self.x + self.offset[1] 203 | local startY = self.y + self.offset[2] 204 | local w = self.width 205 | local h = self.height 206 | 207 | local lineSize = self.lineSize 208 | local handleSize = self.handleSize 209 | local percentage = self:getPercentage() 210 | local handleX 211 | local handleY 212 | local handleWidth 213 | local handleHeight 214 | if self.vertical then 215 | local slidableLength = h - lineSize * 2 - handleSize 216 | handleX = startX 217 | handleY = startY + lineSize + percentage * slidableLength 218 | handleWidth = w 219 | handleHeight = handleSize 220 | else 221 | local slidableLength = w - lineSize * 2 - handleSize 222 | handleX = startX + lineSize + percentage * slidableLength 223 | handleY = startY 224 | handleWidth = handleSize 225 | handleHeight = h 226 | end 227 | if position[1] >= handleX and position[1] <= handleX + handleWidth 228 | and position[2] >= handleY and position[2] <= handleY + handleHeight 229 | then 230 | -- Drag handle 231 | local dragOffset 232 | if self.vertical then 233 | dragOffset = position[2] - handleY 234 | else 235 | dragOffset = position[1] - handleX 236 | end 237 | self.dragOffset = dragOffset 238 | self.dragging = true 239 | else 240 | -- Move handle 241 | if self.vertical then 242 | if position[2] < handleY then 243 | self.moving = false 244 | else 245 | self.moving = true 246 | end 247 | else 248 | if position[1] < handleX then 249 | self.moving = false 250 | else 251 | self.moving = true 252 | end 253 | end 254 | end 255 | end 256 | return true 257 | end 258 | end 259 | -------------------------------------------------------------------------------- /penguingui/TextButton.lua: -------------------------------------------------------------------------------- 1 | --- A button that has a text label. 2 | -- @classmod TextButton 3 | -- @usage 4 | -- -- Create a text button with the text "Hello" 5 | -- local button = TextButton(0, 0, 100, 16, "Hello") 6 | TextButton = class(Button) 7 | --- The text of the button. 8 | TextButton.text = nil 9 | --- The padding between the text and the button edge. 10 | TextButton.textPadding = 2 11 | 12 | --- Constructor 13 | -- @section 14 | 15 | --- Constructs a button with a text label. 16 | -- 17 | -- @param x The x coordinate of the new component, relative to its parent. 18 | -- @param y The y coordinate of the new component, relative to its parent. 19 | -- @param width The width of the new component. 20 | -- @param height The height of the new component. 21 | -- @param text The text string to display on the button. The button's size will 22 | -- be based on this string. 23 | -- @param fontColor (optional) The color of the text to display, default white. 24 | function TextButton:_init(x, y, width, height, text, fontColor) 25 | Button._init(self, x, y, width, height) 26 | local padding = self.textPadding 27 | local fontSize = height - padding * 2 28 | local label = Label(0, padding, text, fontSize, fontColor) 29 | self.text = text 30 | 31 | self.label = label 32 | self:add(label) 33 | 34 | self:addListener( 35 | "text", 36 | function(t, k, old, new) 37 | t.label.text = new 38 | t:repositionLabel() 39 | end 40 | ) 41 | 42 | self:repositionLabel() 43 | end 44 | 45 | --- @section end 46 | 47 | -- Centers the text label 48 | function TextButton:repositionLabel() 49 | local label = self.label 50 | local text = label.text 51 | local padding = self.textPadding 52 | local maxHeight = self.height - padding * 2 53 | local maxWidth = self.width - padding * 2 54 | if label.height < maxHeight then 55 | label.fontSize = maxHeight 56 | label:recalculateBounds() 57 | end 58 | while label.width > maxWidth do 59 | label.fontSize = label.fontSize - 1 60 | label:recalculateBounds() 61 | end 62 | label.x = (self.width - label.width) / 2 63 | label.y = (self.height - label.height) / 2 64 | end 65 | 66 | --- Called when this button is clicked. 67 | -- @function onClick 68 | -- 69 | -- @param button The mouse button that was used. 70 | -------------------------------------------------------------------------------- /penguingui/TextField.lua: -------------------------------------------------------------------------------- 1 | --- An editable text field. 2 | -- @classmod TextField 3 | -- @usage 4 | -- -- Create a text field that prints whatever is in it when enter is pressed. 5 | -- local textfield = TextField(0, 0, 100, 16, "default text") 6 | -- textfield.onEnter = function(textfield) 7 | -- print(textfield.text) 8 | -- end 9 | TextField = class(Component) 10 | TextField.vPadding = 3 11 | TextField.hPadding = 4 12 | --- The color of this text field's border. 13 | TextField.borderColor = {84, 84, 84} 14 | --- The background color of this text field. 15 | TextField.backgroundColor = {0, 0, 0} 16 | --- The color of this text field's text. 17 | TextField.textColor = {255, 255, 255} 18 | --- The color of this text field's text when the mouse is over it. 19 | TextField.textHoverColor = {153, 153, 153} 20 | --- The color of this text field's default text. 21 | TextField.defaultTextColor = {51, 51, 51} 22 | --- The color of this text field's default text when the mouse is over it. 23 | TextField.defaultTextHoverColor = {119, 119, 199} 24 | --- The color of the text cursor. 25 | TextField.cursorColor = {255, 255, 255} 26 | --- The period of the cursor's blinking. 27 | TextField.cursorRate = 1 28 | --- The filter pattern to restrict this TextField's text to, or nil if none. 29 | TextField.filter = nil 30 | 31 | --- Delay before a key starts repeating. 32 | TextField.repeatDelay = 0.5 33 | --- Interval between key repeats. 34 | TextField.repeatInterval = 0.05 35 | 36 | --- Constructor 37 | -- @section 38 | 39 | --- Constructs a new TextField. 40 | -- 41 | -- @param x The x coordinate of the new component, relative to its parent. 42 | -- @param y The y coordinate of the new component, relative to its parent. 43 | -- @param width The width of the new component. 44 | -- @param height The height of the new component. 45 | -- @param defaultText The text to display when nothing has been entered. 46 | function TextField:_init(x, y, width, height, defaultText) 47 | Component._init(self) 48 | self.x = x 49 | self.y = y 50 | self.width = width 51 | self.height = height 52 | self.fontSize = height - self.vPadding * 2 53 | self.cursorPosition = 0 54 | self.cursorX = 0 55 | self.cursorTimer = self.cursorRate 56 | self.text = "" 57 | self.defaultText = defaultText 58 | self.textOffset = 0 59 | self.textClip = nil 60 | self.mouseOver = false 61 | self.keyTimes = {} 62 | end 63 | 64 | --- @section end 65 | 66 | function TextField:update(dt) 67 | if self.hasFocus then 68 | -- Key repeat 69 | local keyTimes = self.keyTimes 70 | for key,dur in pairs(keyTimes) do 71 | local time = dur + dt 72 | keyTimes[key] = time 73 | if time > self.repeatDelay + self.repeatInterval then 74 | self:keyEvent(key, true) 75 | keyTimes[key] = self.repeatDelay 76 | end 77 | end 78 | 79 | -- Cursor blink 80 | local timer = self.cursorTimer 81 | local rate = self.cursorRate 82 | timer = timer - dt 83 | if timer < 0 then 84 | timer = rate 85 | end 86 | self.cursorTimer = timer 87 | end 88 | end 89 | 90 | function TextField:draw(dt) 91 | local startX = self.x + self.offset[1] 92 | local startY = self.y + self.offset[2] 93 | local w = self.width 94 | local h = self.height 95 | 96 | -- Draw border and background 97 | local borderRect = { 98 | startX, startY, startX + w, startY + h 99 | } 100 | local backgroundRect = { 101 | startX + 1, startY + 1, startX + w - 1, startY + h - 1 102 | } 103 | PtUtil.fillRect(borderRect, self.borderColor) 104 | PtUtil.fillRect(backgroundRect, self.backgroundColor) 105 | 106 | local text = self.text 107 | -- Decide if the default text should be displayed 108 | local default = (text == "") and (self.defaultText ~= nil) 109 | 110 | local textColor 111 | -- Choose the color to draw the text with 112 | if self.mouseOver then 113 | textColor = default and self.defaultTextHoverColor or self.textHoverColor 114 | else 115 | textColor = default and self.defaultTextColor or self.textColor 116 | end 117 | 118 | local cursorPosition = self.cursorPosition 119 | text = default and self.defaultText 120 | or text:sub(self.textOffset + 1, self.textOffset 121 | + (self.textClip or #text)) 122 | 123 | -- Draw the text 124 | PtUtil.drawText(text, { 125 | position = { 126 | startX + self.hPadding, 127 | startY + self.vPadding 128 | }, 129 | verticalAnchor = "bottom" 130 | }, self.fontSize, textColor) 131 | 132 | -- Text cursor 133 | if self.hasFocus then 134 | local timer = self.cursorTimer 135 | local rate = self.cursorRate 136 | 137 | if timer > rate / 2 then -- Draw cursor 138 | local cursorX = startX + self.cursorX + self.hPadding 139 | local cursorY = startY + self.vPadding 140 | PtUtil.drawLine({cursorX, cursorY}, 141 | {cursorX, cursorY + h - self.vPadding * 2}, 142 | self.cursorColor, 143 | 1) 144 | end 145 | end 146 | end 147 | 148 | --- Set the character position of the text cursor. 149 | -- 150 | -- @param pos The new position for the cursor, where 0 is the beginning of the 151 | -- field. 152 | function TextField:setCursorPosition(pos) 153 | if pos > #self.text then 154 | pos = #self.text 155 | end 156 | self.cursorPosition = pos 157 | 158 | if pos < self.textOffset then 159 | self.textOffset = pos 160 | end 161 | self:calculateTextClip() 162 | local textClip = self.textClip 163 | while (textClip) and (pos > self.textOffset + textClip) do 164 | self.textOffset = self.textOffset + 1 165 | self:calculateTextClip() 166 | textClip = self.textClip 167 | end 168 | while self.textOffset > 0 and not textClip do 169 | self.textOffset = self.textOffset - 1 170 | self:calculateTextClip() 171 | textClip = self.textClip 172 | if textClip then 173 | self.textOffset = self.textOffset + 1 174 | self:calculateTextClip() 175 | end 176 | end 177 | -- world.logInfo("cursor: %s, textOffset: %s, textClip: %s", pos, self.textOffset, self.textClip) 178 | 179 | local text = self.text 180 | local cursorX = 0 181 | for i=self.textOffset + 1,pos,1 do 182 | local charWidth = PtUtil.getStringWidth(text:sub(i, i), self.fontSize) 183 | cursorX = cursorX + charWidth 184 | end 185 | self.cursorX = cursorX 186 | self.cursorTimer = self.cursorRate 187 | end 188 | 189 | -- Calculates the text clip, i.e. how many characters to display. 190 | function TextField:calculateTextClip() 191 | local maxX = self.width - self.hPadding * 2 192 | local text = self.text 193 | local totalWidth = 0 194 | local startI = self.textOffset + 1 195 | for i=startI,#text,1 do 196 | totalWidth = totalWidth 197 | + PtUtil.getStringWidth(text:sub(i, i), self.fontSize) 198 | if totalWidth > maxX then 199 | self.textClip = i - startI 200 | return 201 | end 202 | end 203 | self.textClip = nil 204 | end 205 | 206 | function TextField:clickEvent(position, button, pressed) 207 | if button <= 3 then 208 | local xPos = position[1] - self.x - self.offset[1] - self.hPadding 209 | 210 | local text = self.text 211 | local totalWidth = 0 212 | for i=self.textOffset + 1,#text,1 do 213 | local charWidth = PtUtil.getStringWidth(text:sub(i, i), self.fontSize) 214 | if xPos < (totalWidth + charWidth * 0.6) then 215 | self:setCursorPosition(i - 1) 216 | return true 217 | end 218 | totalWidth = totalWidth + charWidth 219 | end 220 | self:setCursorPosition(#text) 221 | 222 | return true 223 | end 224 | end 225 | 226 | function TextField:keyEvent(keyCode, pressed) 227 | -- Update key timings 228 | if pressed then 229 | self.keyTimes[keyCode] = self.keyTimes[keyCode] or 0 230 | else 231 | self.keyTimes[keyCode] = nil 232 | end 233 | 234 | -- Ignore key releases and any keys pressed while ctrl or alt is held 235 | local keyState = GUI.keyState 236 | if not pressed 237 | or keyState[305] or keyState[306] 238 | or keyState[307] or keyState[308] 239 | then 240 | return 241 | end 242 | 243 | local shift = keyState[303] or keyState[304] 244 | local caps = keyState[301] 245 | local key = PtUtil.getKey(keyCode, shift, caps) 246 | 247 | local text = self.text 248 | local cursorPos = self.cursorPosition 249 | 250 | local filter = self.filter 251 | if #key == 1 then -- Type a character 252 | text = text:sub(1, cursorPos) .. key .. text:sub(cursorPos + 1) 253 | if filter then 254 | if not text:match(filter) then 255 | return true 256 | end 257 | end 258 | self.text = text 259 | self:setCursorPosition(cursorPos + 1) 260 | else -- Special character 261 | if key == "backspace" then 262 | if cursorPos > 0 then 263 | text = text:sub(1, cursorPos - 1) .. text:sub(cursorPos + 1) 264 | if filter then 265 | if not text:match(filter) then 266 | return true 267 | end 268 | end 269 | self.text = text 270 | self:setCursorPosition(cursorPos - 1) 271 | end 272 | elseif key == "enter" then 273 | if self.onEnter then 274 | self:onEnter() 275 | end 276 | elseif key == "delete" then 277 | if cursorPos < #text then 278 | text = text:sub(1, cursorPos) .. text:sub(cursorPos + 2) 279 | if filter then 280 | if not text:match(filter) then 281 | return true 282 | end 283 | end 284 | self.text = text 285 | end 286 | elseif key == "right" then 287 | self:setCursorPosition(math.min(cursorPos + 1, #text)) 288 | elseif key == "left" then 289 | self:setCursorPosition(math.max(0, cursorPos - 1)) 290 | elseif key == "home" then 291 | self:setCursorPosition(0) 292 | elseif key == "end" then 293 | self:setCursorPosition(#text) 294 | end 295 | end 296 | return true 297 | end 298 | 299 | --- Called when the user presses enter in this text field. 300 | -- @function onEnter 301 | -------------------------------------------------------------------------------- /penguingui/TextRadioButton.lua: -------------------------------------------------------------------------------- 1 | --- A radio button with text. 2 | -- Extends @{RadioButton}, so all fields of @{RadioButton} are inherited. 3 | -- @classmod TextRadioButton 4 | -- @usage 5 | -- -- Create a group of buttons that print when they are selected. 6 | -- local group = Panel(0, 0) 7 | -- local numButtons = 10 8 | -- for i = 1, numButtons, 1 do 9 | -- local button = TextRadioButton(0, i * 20, 60, 16, "Button " .. i) 10 | -- button:addListener("selected", function(t, k, old, new) 11 | -- if new then 12 | -- print(t.text .. " was selected") 13 | -- end 14 | -- end) 15 | -- group:add(button) 16 | -- end 17 | TextRadioButton = class(RadioButton) 18 | TextRadioButton.hoverColor = {31, 31, 31} 19 | TextRadioButton.pressedColor = {69, 69, 69} 20 | TextRadioButton.checkColor = {52, 52, 52} 21 | --- The text of the button. 22 | TextRadioButton.text = nil 23 | --- The padding between the text and the button edge. 24 | TextRadioButton.textPadding = 2 25 | 26 | --- Constructor 27 | -- @section 28 | 29 | --- Constructs a new TextRadioButton. 30 | -- @param x The x coordinate of the new component, relative to its parent. 31 | -- @param y The y coordinate of the new component, relative to its parent. 32 | -- @param width The width of the new component. 33 | -- @param height The height of the new component. 34 | -- @param text The text of this radio button. 35 | function TextRadioButton:_init(x, y, width, height, text) 36 | RadioButton._init(self, x, y, 0) 37 | self.width = width 38 | self.height = height 39 | 40 | local padding = self.textPadding 41 | local fontSize = height - padding * 2 42 | local label = Label(0, padding, text, fontSize, fontColor) 43 | 44 | self.label = label 45 | self:add(label) 46 | 47 | self.text = text 48 | self:addListener( 49 | "text", 50 | function(t, k, old, new) 51 | t.label.text = new 52 | t:repositionLabel() 53 | end 54 | ) 55 | self:repositionLabel() 56 | end 57 | 58 | --- @section end 59 | 60 | function TextRadioButton:drawCheck(dt) 61 | local startX = self.x + self.offset[1] 62 | local startY = self.y + self.offset[2] 63 | local w = self.width 64 | local h = self.height 65 | local checkRect = {startX + 1, startY + 1, 66 | startX + w - 1, startY + h - 1} 67 | PtUtil.fillRect(checkRect, self.checkColor) 68 | end 69 | 70 | TextRadioButton.repositionLabel = TextButton.repositionLabel 71 | -------------------------------------------------------------------------------- /penguingui/Util.lua: -------------------------------------------------------------------------------- 1 | --- Utility functions. 2 | -- @module PtUtil 3 | PtUtil = {} 4 | -- Pixel widths of the first 255 characters. This was generated in Java. 5 | PtUtil.charWidths = {6, 6, 6, 6, 6, 6, 6, 6, 0, 0, 6, 6, 0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 4, 8, 12, 10, 12, 12, 4, 6, 6, 8, 8, 6, 8, 4, 12, 10, 6, 10, 10, 10, 10, 10, 10, 10, 10, 4, 4, 8, 8, 8, 10, 12, 10, 10, 8, 10, 8, 8, 10, 10, 8, 10, 10, 8, 12, 10, 10, 10, 10, 10, 10, 8, 10, 10, 12, 10, 10, 8, 6, 12, 6, 8, 10, 6, 10, 10, 9, 10, 10, 8, 10, 10, 4, 6, 9, 4, 12, 10, 10, 10, 10, 8, 10, 8, 10, 10, 12, 8, 10, 10, 8, 4, 8, 10, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 6, 10, 10, 15, 10, 5, 13, 7, 14, 15, 15, 10, 6, 14, 12, 16, 14, 7, 7, 6, 11, 12, 8, 7, 6, 16, 16, 15, 15, 15, 10, 10, 10, 10, 10, 10, 10, 14, 10, 8, 8, 8, 8, 8, 8, 8, 8, 13, 10, 10, 10, 10, 10, 10, 10, 13, 10, 10, 10, 10, 10, 14, 11, 10, 10, 10, 10, 10, 10, 15, 9, 10, 10, 10, 10, 8, 8, 8, 8, 12, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 15, 10} 6 | 7 | --- Get a list of all lua scripts in the PenguinGUI library. 8 | -- 9 | -- @return A list of strings containing the paths to the PenguinGUI scripts. 10 | function PtUtil.library() 11 | return { 12 | "/penguingui/Util.lua", 13 | "/penguingui/Binding.lua", 14 | "/penguingui/BindingFunctions.lua", 15 | "/penguingui/GUI.lua", 16 | "/penguingui/Component.lua", 17 | "/penguingui/Line.lua", 18 | "/penguingui/Rectangle.lua", 19 | "/penguingui/Align.lua", 20 | "/penguingui/HorizontalLayout.lua", 21 | "/penguingui/VerticalLayout.lua", 22 | "/penguingui/Panel.lua", 23 | "/penguingui/Frame.lua", 24 | "/penguingui/Button.lua", 25 | "/penguingui/Label.lua", 26 | "/penguingui/TextButton.lua", 27 | "/penguingui/TextField.lua", 28 | "/penguingui/Image.lua", 29 | "/penguingui/CheckBox.lua", 30 | "/penguingui/RadioButton.lua", 31 | "/penguingui/TextRadioButton.lua", 32 | "/penguingui/Slider.lua", 33 | "/penguingui/List.lua", 34 | "/lib/profilerapi.lua", 35 | "/lib/inspect.lua" 36 | } 37 | end 38 | 39 | --- Draw the text string, offsetting the string to account for leading whitespace. 40 | -- 41 | -- All parameters are identical to those of console.canvasDrawText 42 | function PtUtil.drawText(text, options, fontSize, color) 43 | -- Convert to pixels 44 | fontSize = fontSize or 16 45 | if text:byte() == 32 then -- If it starts with a space, offset the string 46 | local xOffset = PtUtil.getStringWidth(" ", fontSize) 47 | local oldX = options.position[1] 48 | options.position[1] = oldX + xOffset 49 | console.canvasDrawText(text, options, fontSize, color) 50 | options.position[1] = oldX 51 | else 52 | console.canvasDrawText(text, options, fontSize, color) 53 | end 54 | end 55 | 56 | --- Get the approximate pixel width of a string. 57 | -- 58 | -- @param text The string to get the width of. 59 | -- @param fontSize The size of the font to get the width from. 60 | function PtUtil.getStringWidth(text, fontSize) 61 | local widths = PtUtil.charWidths 62 | local scale = PtUtil.getFontScale(fontSize) 63 | local out = 0 64 | local len = #text 65 | for i=1,len,1 do 66 | out = out + widths[string.byte(text, i)] 67 | end 68 | return out * scale 69 | end 70 | 71 | --- Gets the scale of the specified font size. 72 | -- 73 | -- @param size The font size to get the scale for. 74 | function PtUtil.getFontScale(size) 75 | return size / 16 76 | end 77 | 78 | PtUtil.specialKeyMap = { 79 | [8] = "backspace", 80 | [13] = "enter", 81 | [127] = "delete", 82 | [275] = "right", 83 | [276] = "left", 84 | [278] = "home", 85 | [279] = "end", 86 | [301] = "capslock", 87 | [303] = "shift", 88 | [304] = "shift" 89 | } 90 | 91 | PtUtil.shiftKeyMap = { 92 | [39] = "\"", 93 | [44] = "<", 94 | [45] = "_", 95 | [46] = ">", 96 | [47] = "?", 97 | [48] = ")", 98 | [49] = "!", 99 | [50] = "@", 100 | [51] = "#", 101 | [52] = "$", 102 | [53] = "%", 103 | [54] = "^", 104 | [55] = "&", 105 | [56] = "*", 106 | [57] = "(", 107 | [59] = ":", 108 | [61] = "+", 109 | [91] = "{", 110 | [92] = "|", 111 | [93] = "}", 112 | [96] = "~" 113 | } 114 | 115 | --- Gets a string representation of the keycode. 116 | -- 117 | -- @param key The keycode of the key. 118 | -- @param shift Boolean representing whether or not shift is pressed. 119 | -- @param capslock Boolean representing whether or not capslock is on. 120 | function PtUtil.getKey(key, shift, capslock) 121 | if (capslock and not shift) or (shift and not capslock) then 122 | if key >= 97 and key <= 122 then 123 | return string.upper(string.char(key)) 124 | end 125 | end 126 | if shift and PtUtil.shiftKeyMap[key] then 127 | return PtUtil.shiftKeyMap[key] 128 | else 129 | if key >= 32 and key <= 122 then 130 | return string.char(key) 131 | elseif PtUtil.specialKeyMap[key] then 132 | return PtUtil.specialKeyMap[key] 133 | else 134 | return "unknown" 135 | end 136 | end 137 | end 138 | 139 | --- Fills a rectangle. 140 | -- 141 | -- All parameters are identical to those of console.canvasDrawRect 142 | function PtUtil.fillRect(rect, color) 143 | console.canvasDrawRect(rect, color) 144 | end 145 | 146 | -- TODO - There is no way to fill a polygon yet. 147 | -- Fills a polygon. 148 | function PtUtil.fillPoly(poly, color) 149 | console.logInfo("fillPoly is not functional yet") 150 | -- console.canvasDrawPoly(poly, color) 151 | end 152 | 153 | --- Draws a line. 154 | -- 155 | -- All parameters are identical to those of console.canvasDrawLine 156 | function PtUtil.drawLine(p1, p2, color, width) 157 | console.canvasDrawLine(p1, p2, color, width * 2) 158 | end 159 | 160 | --- Draws a rectangle. 161 | -- 162 | -- All parameters are identical to those of console.canvasDrawRect 163 | function PtUtil.drawRect(rect, color, width) 164 | local minX = rect[1] + width / 2 165 | local minY = math.floor((rect[2] + width / 2) * 2) / 2 166 | local maxX = rect[3] - width / 2 167 | local maxY = math.floor((rect[4] - width / 2) * 2) / 2 168 | PtUtil.drawLine( 169 | {minX - width / 2, minY}, 170 | {maxX + width / 2, minY}, 171 | color, width 172 | ) 173 | PtUtil.drawLine( 174 | {maxX, minY}, 175 | {maxX, maxY}, 176 | color, width 177 | ) 178 | PtUtil.drawLine( 179 | {minX - width / 2, maxY}, 180 | {maxX + width / 2, maxY}, 181 | color, width 182 | ) 183 | PtUtil.drawLine( 184 | {minX, minY}, 185 | {minX, maxY}, 186 | color, width 187 | ) 188 | end 189 | 190 | -- Draws a polygon. 191 | -- 192 | -- All parameters are identical to those of console.canvasDrawPoly 193 | function PtUtil.drawPoly(poly, color, width) 194 | -- Draw lines 195 | for i=1,#poly - 1,1 do 196 | PtUtil.drawLine(poly[i], poly[i + 1], color, width) 197 | end 198 | PtUtil.drawLine(poly[#poly], poly[1], color, width) 199 | end 200 | 201 | -- Draws an image. 202 | -- 203 | -- All parameters are identical to those of console.canvasDrawImage 204 | function PtUtil.drawImage(image, position, scale) 205 | console.canvasDrawImage(image, position, scale) 206 | end 207 | 208 | --- Does the same thing as ipairs, except backwards 209 | -- 210 | -- @param t The table to iterate backwards over 211 | function ripairs(t) 212 | local function ripairs_it(t,i) 213 | i=i-1 214 | local v=t[i] 215 | if v==nil then return v end 216 | return i,v 217 | end 218 | return ripairs_it, t, #t+1 219 | end 220 | 221 | --- Removes the first occurence of an object from the given table. 222 | -- 223 | -- @param t The table to remove from. 224 | -- @param o The object to remove. 225 | -- @return The index of the removed object, or -1 if the object was not found. 226 | function PtUtil.removeObject(t, o) 227 | for i,obj in ipairs(t) do 228 | if obj == o then 229 | table.remove(t, i) 230 | return i 231 | end 232 | end 233 | return -1 234 | end 235 | 236 | --- Creates a new class with the specified superclass(es). 237 | -- @param ... The new class's superclass(es). 238 | function class(...) 239 | -- "cls" is the new class 240 | local cls, bases = {}, {...} 241 | 242 | -- copy base class contents into the new class 243 | for i, base in ipairs(bases) do 244 | for k, v in pairs(base) do 245 | cls[k] = v 246 | end 247 | end 248 | 249 | -- set the class's __index, and start filling an "is_a" table that contains this class and all of its bases 250 | -- so you can do an "instance of" check using my_instance.is_a[MyClass] 251 | cls.__index, cls.is_a = cls, {[cls] = true} 252 | for i, base in ipairs(bases) do 253 | for c in pairs(base.is_a) do 254 | cls.is_a[c] = true 255 | end 256 | cls.is_a[base] = true 257 | end 258 | -- the class's __call metamethod 259 | setmetatable( 260 | cls, 261 | { 262 | __call = function (c, ...) 263 | local instance = setmetatable({}, c) 264 | instance = Binding.proxy(instance) 265 | 266 | -- run the init method if it's there 267 | local init = instance._init 268 | if init then init(instance, ...) end 269 | return instance 270 | end 271 | } 272 | ) 273 | -- return the new class table, that's ready to fill with methods 274 | return cls 275 | end 276 | 277 | -- Dumps value as a string closely resemling Lua code that could be used to 278 | -- recreate it (with the exception of functions, threads and recursive tables). 279 | -- Credit to MrMagical. 280 | -- 281 | -- Basic usage: dump(value) 282 | -- 283 | -- @param value The value to be dumped. 284 | -- @param indent (optional) String used for indenting the dumped value. 285 | -- @param seen (optional) Table of already processed tables which will be 286 | -- dumped as "{...}" to prevent infinite recursion. 287 | function dump(value, indent, seen) 288 | if type(value) ~= "table" then 289 | if type(value) == "string" then 290 | return string.format('%q', value) 291 | else 292 | return tostring(value) 293 | end 294 | else 295 | if type(seen) ~= "table" then 296 | seen = {} 297 | elseif seen[value] then 298 | return "{...}" 299 | end 300 | seen[value] = true 301 | indent = indent or "" 302 | if next(value) == nil then 303 | return "{}" 304 | end 305 | local str = "{" 306 | local first = true 307 | for k,v in pairs(value) do 308 | if first then 309 | first = false 310 | else 311 | str = str.."," 312 | end 313 | str = str.."\n"..indent.." ".."["..dump(k, "", seen) 314 | .."] = "..dump(v, indent.." ", seen) 315 | end 316 | str = str.."\n"..indent.."}" 317 | return str 318 | end 319 | end 320 | -------------------------------------------------------------------------------- /penguingui/VerticalLayout.lua: -------------------------------------------------------------------------------- 1 | --- Lays out components vertically. 2 | -- @classmod VerticalLayout 3 | -- @usage 4 | -- -- Create a vertical layout manager with padding of 2. 5 | -- local layout = VerticalLayout(2) 6 | VerticalLayout = class() 7 | 8 | --- Padding between contained components. 9 | VerticalLayout.padding = nil 10 | --- Vertical alignment of contained components. 11 | -- Default top. 12 | VerticalLayout.vAlignment = Align.TOP 13 | --- Horizontal alignment of contained components. 14 | -- Default center. 15 | VerticalLayout.hAlignment = Align.CENTER 16 | 17 | --- Constructor 18 | -- @section 19 | 20 | --- Constructs a VerticalLayout. 21 | -- 22 | -- @param[opt=0] padding The padding between components within this layout. 23 | -- @param[opt=Align.TOP] vAlign The vertical alignment of the components. 24 | -- @param[opt=Align.CENTER] hAlign The horizontal alignment of the components. 25 | function VerticalLayout:_init(padding, vAlign, hAlign) 26 | self.padding = padding or 0 27 | if hAlign then 28 | self.hAlignment = hAlign 29 | end 30 | if vAlign then 31 | self.vAlignment = vAlign 32 | end 33 | end 34 | 35 | --- @section end 36 | 37 | function VerticalLayout:layout() 38 | local vAlign = self.vAlignment 39 | local hAlign = self.hAlignment 40 | local padding = self.padding 41 | 42 | local container = self.container 43 | local components = container.children 44 | local totalHeight = 0 45 | for _,component in ipairs(components) do 46 | totalHeight = totalHeight + component.height 47 | end 48 | totalHeight = totalHeight + (#components - 1) * padding 49 | 50 | local startY 51 | if vAlign == Align.TOP then 52 | startY = container.height 53 | elseif vAlign == Align.CENTER then 54 | startY = container.height - (container.height - totalHeight) / 2 55 | else -- ALIGN_BOTTOM 56 | startY = totalHeight 57 | end 58 | 59 | for _,component in ipairs(components) do 60 | component.y = startY - component.height 61 | if hAlign == Align.LEFT then 62 | component.x = 0 63 | elseif hAlign == Align.CENTER then 64 | component.x = (container.width - component.width) / 2 65 | else -- ALIGN_RIGHT 66 | component.x = container.width - component.width 67 | end 68 | startY = startY - (component.height + padding) 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /penguingui/testobject/ptguitestconsole.lua: -------------------------------------------------------------------------------- 1 | function init() 2 | storage = console.configParameter("scriptStorage") 3 | local tests = { 4 | "textTest", 5 | "windowTest", 6 | "listTest", 7 | "labelTest" 8 | } 9 | local y = 190 10 | local pad = 10 11 | local x = 10 12 | for _,test in ipairs(tests) do 13 | local button = TextRadioButton(x, y, 70, 12, test) 14 | x = x + button.width + pad 15 | GUI.add(button) 16 | local panel = Panel(0, 0) 17 | GUI.add(panel) 18 | panel:bind("visible", Binding(button, "selected")) 19 | _ENV[test](panel) 20 | end 21 | end 22 | 23 | function labelTest(panel) 24 | local x = 10 25 | local y = 10 26 | local text = "ABCDEabcdegjp^|" 27 | for i=5,25,1 do 28 | local label = Label(x, y, text, i) 29 | local rect = Rectangle(x, y, label.width, label.height, "red") 30 | panel:add(rect) 31 | panel:add(label) 32 | y = y + label.height + 1 33 | end 34 | end 35 | 36 | function textTest(panel) 37 | local y = 10 38 | local x = 10 39 | local button = TextButton(x, y, 70, 12, "text") 40 | local field = TextField(x, y + button.height + 10, 150, 12, "Set text") 41 | field.onEnter = function() 42 | button.text = field.text 43 | end 44 | panel:add(button) 45 | panel:add(field) 46 | end 47 | 48 | function windowTest(panel) 49 | local testbutton = TextButton(10, 10, 100, 16, "Make window") 50 | testbutton.onClick = testButtonClick 51 | panel:add(testbutton) 52 | 53 | local slider = Slider(10, 40, 100, 10, 50, 100, 1) 54 | local label = Label(120, 40, "", 10) 55 | label:bind("text", Binding.concat( 56 | "Sample Slider: ", Binding(slider, "value"))) 57 | panel:add(slider) 58 | panel:add(label) 59 | end 60 | 61 | function listTest(panel) 62 | -- List test 63 | local list = List(30, 30, 100, 100, 12) 64 | local selected = Binding.proxy({item = nil}) 65 | panel:add(list) 66 | for i=1,20,1 do 67 | local item = list:emplaceItem("Item " .. i) 68 | if i == 1 then 69 | selected.item = item 70 | end 71 | local listener = 72 | function(t, k, old, new) 73 | if new then 74 | selected.item = t 75 | end 76 | end 77 | item:addListener("selected", listener) 78 | end 79 | 80 | local filter = TextField(list.x, list.y + list.height + 5, list.width, 15, "Filter") 81 | panel:add(filter) 82 | filter:addListener( 83 | "text", 84 | function(t, k, old, new) 85 | list:filter( 86 | function(item) 87 | if item.text:find(new) then 88 | return true 89 | end 90 | end 91 | ) 92 | end 93 | ) 94 | 95 | local removeButton = TextButton(150, 60, 100, 12, "Remove") 96 | removeButton:bind("text", Binding.concat("Remove ", 97 | Binding(selected, {"item", "text"}))) 98 | removeButton.onClick = function() 99 | list:removeItem(selected.item) 100 | end 101 | panel:add(removeButton) 102 | end 103 | 104 | function testButtonClick(button, mouseButton) 105 | local padding = 20 106 | 107 | local frame = Frame(100, 50) 108 | GUI.add(frame) 109 | 110 | local closeButton = TextButton(100 + 10 + padding, 111 | padding, 100, 16, "close") 112 | closeButton.onClick = function(button) 113 | GUI.remove(frame) 114 | end 115 | frame:add(closeButton) 116 | 117 | local xLabel = Label(padding, padding, "") 118 | xLabel:bind("text", Binding.concat("x: ", Binding(frame, "x"):tostring())) 119 | frame:add(xLabel) 120 | 121 | local yLabel = Label(padding, padding + xLabel.height + 10, "") 122 | yLabel:bind("text", Binding.concat("y: ", Binding(frame, "y"):tostring())) 123 | frame:add(yLabel) 124 | 125 | frame:pack(padding) 126 | end 127 | 128 | function syncStorage() 129 | world.callScriptedEntity(console.sourceEntity(), "onConsoleStorageRecieve", storage) 130 | end 131 | 132 | function update(dt) 133 | GUI.step(dt) 134 | end 135 | 136 | function canvasClickEvent(position, button, pressed) 137 | -- world.logInfo("ClickEvent detected at %s with button %s %s", position, button, pressed) 138 | GUI.clickEvent(position, button, pressed) 139 | end 140 | 141 | function canvasKeyEvent(key, isKeyDown) 142 | -- world.logInfo("Key %s was %s", key, isKeyDown and "pressed" or "released") 143 | GUI.keyEvent(key, isKeyDown) 144 | end 145 | -------------------------------------------------------------------------------- /penguingui/testobject/ptguitestobject.frames: -------------------------------------------------------------------------------- 1 | { 2 | "frameGrid" : { 3 | "size" : [16, 8], 4 | "dimensions" : [1, 2], 5 | "names" : [ 6 | [ "default.off" ], 7 | [ "default.on" ] 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /penguingui/testobject/ptguitestobject.lua: -------------------------------------------------------------------------------- 1 | function init(virtual) 2 | if not virtual then 3 | storage.consoleStorage = storage.consoleStorage or {} 4 | entity.setInteractive(true) 5 | end 6 | end 7 | 8 | function onConsoleStorageRecieve(consoleStorage) 9 | storage.consoleStorage = consoleStorage 10 | end 11 | 12 | function onInteraction(args) 13 | local interactionConfig = entity.configParameter("interactionConfig") 14 | 15 | local development = true 16 | if development then 17 | local consoleScripts = PtUtil.library() 18 | for _,script in ipairs(interactionConfig.scripts) do 19 | table.insert(consoleScripts, script) 20 | end 21 | interactionConfig.scripts = consoleScripts 22 | else 23 | table.insert(interactionConfig.scripts, 1, "/penguingui.lua") 24 | end 25 | 26 | interactionConfig.scriptStorage = storage.consoleStorage 27 | 28 | return {"ScriptConsole", interactionConfig} 29 | end 30 | -------------------------------------------------------------------------------- /penguingui/testobject/ptguitestobject.object: -------------------------------------------------------------------------------- 1 | { 2 | "objectName" : "ptguitestobject", 3 | "rarity" : "Rare", 4 | "description" : "Testing object for the PenguinGUI library", 5 | "shortdescription" : "Penguin GUI Test", 6 | "race" : "generic", 7 | 8 | "category" : "wire", 9 | "price" : 1, 10 | "printable" : false, 11 | 12 | "inventoryIcon" : "ptguitestobjecticon.png", 13 | "orientations" : [ 14 | { 15 | "image" : "ptguitestobject.png:default.off", 16 | "imagePosition" : [0, 0], 17 | 18 | "spaceScan" : 0.1, 19 | "direction" : "right" 20 | } 21 | ], 22 | 23 | "scripts" : ["/lib/inspect.lua", "/penguingui/Util.lua", "ptguitestobject.lua"], 24 | "scriptDelta" : 15, 25 | 26 | "animation" : "/objects/wired/switch/switchtoggle.animation", 27 | 28 | "animationParts" : { 29 | "switch" : "ptguitestobject.png" 30 | }, 31 | "animationPosition" : [0, 0], 32 | 33 | "interactionConfig" : { 34 | "gui" : { 35 | "background" : { 36 | "zlevel" : 0, 37 | "type" : "background", 38 | "fileHeader" : "/testconsole/consoleheader.png", 39 | "fileBody" : "/testconsole/consolebody.png" 40 | }, 41 | "scriptCanvas" : { 42 | "zlevel" : 1, 43 | "type" : "canvas", 44 | "rect" : [40, 45, 434, 254], 45 | "captureMouseEvents" : true, 46 | "captureKeyboardEvents" : true 47 | }, 48 | "close" : { 49 | "zlevel" : 2, 50 | "type" : "button", 51 | "base" : "/interface/cockpit/xup.png", 52 | "hover" : "/interface/cockpit/xdown.png", 53 | "pressed" : "/interface/cockpit/xdown.png", 54 | "callback" : "close", 55 | "position" : [419, 263], 56 | "pressedOffset" : [0, -1] 57 | } 58 | }, 59 | "scripts" : ["/penguingui/testobject/ptguitestconsole.lua"], 60 | "scriptDelta" : 1, 61 | "scriptCanvas" : "scriptCanvas" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /penguingui/testobject/ptguitestobject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PenguinToast/PenguinGUI/91be30d5445a3bc611ae305bffeba9600f254f85/penguingui/testobject/ptguitestobject.png -------------------------------------------------------------------------------- /penguingui/testobject/ptguitestobjecticon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PenguinToast/PenguinGUI/91be30d5445a3bc611ae305bffeba9600f254f85/penguingui/testobject/ptguitestobjecticon.png -------------------------------------------------------------------------------- /testconsole/consolebody.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PenguinToast/PenguinGUI/91be30d5445a3bc611ae305bffeba9600f254f85/testconsole/consolebody.png -------------------------------------------------------------------------------- /testconsole/consoleheader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PenguinToast/PenguinGUI/91be30d5445a3bc611ae305bffeba9600f254f85/testconsole/consoleheader.png --------------------------------------------------------------------------------