├── .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
--------------------------------------------------------------------------------