├── imgs ├── logo.png ├── beGUI1.png ├── beGUI2.png ├── grids.png ├── docking_absolutely.png └── docking_relatively.png ├── src ├── disk.png ├── imgs │ ├── slide.png │ ├── button.png │ ├── checkbox.png │ ├── radiobox.png │ ├── window.png │ ├── button_up.png │ ├── panel_gray.png │ ├── button_close.png │ ├── button_down.png │ ├── button_left.png │ ├── button_right.png │ ├── panel_white.png │ └── progressbar.png ├── fonts │ └── ascii 8x8.png ├── info.json ├── config.json ├── libs │ └── beGUI │ │ ├── LICENSE_beGUI.txt │ │ ├── beGUI_Structures.lua │ │ ├── beClass.lua │ │ ├── beGUI_Custom.lua │ │ ├── beStack.lua │ │ ├── beGUI.lua │ │ ├── beTheme.lua │ │ ├── beGUI_Popups.lua │ │ ├── beTween.lua │ │ ├── beGUI_Utils.lua │ │ ├── beGUI_Widget.lua │ │ └── beGUI_Containers.lua ├── keycode.lua └── main.lua ├── docs ├── bitty.data ├── bitty.wasm └── index.html ├── LICENSE └── README.md /imgs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paladin-t/begui/HEAD/imgs/logo.png -------------------------------------------------------------------------------- /src/disk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paladin-t/begui/HEAD/src/disk.png -------------------------------------------------------------------------------- /docs/bitty.data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paladin-t/begui/HEAD/docs/bitty.data -------------------------------------------------------------------------------- /docs/bitty.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paladin-t/begui/HEAD/docs/bitty.wasm -------------------------------------------------------------------------------- /imgs/beGUI1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paladin-t/begui/HEAD/imgs/beGUI1.png -------------------------------------------------------------------------------- /imgs/beGUI2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paladin-t/begui/HEAD/imgs/beGUI2.png -------------------------------------------------------------------------------- /imgs/grids.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paladin-t/begui/HEAD/imgs/grids.png -------------------------------------------------------------------------------- /src/imgs/slide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paladin-t/begui/HEAD/src/imgs/slide.png -------------------------------------------------------------------------------- /src/imgs/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paladin-t/begui/HEAD/src/imgs/button.png -------------------------------------------------------------------------------- /src/imgs/checkbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paladin-t/begui/HEAD/src/imgs/checkbox.png -------------------------------------------------------------------------------- /src/imgs/radiobox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paladin-t/begui/HEAD/src/imgs/radiobox.png -------------------------------------------------------------------------------- /src/imgs/window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paladin-t/begui/HEAD/src/imgs/window.png -------------------------------------------------------------------------------- /src/fonts/ascii 8x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paladin-t/begui/HEAD/src/fonts/ascii 8x8.png -------------------------------------------------------------------------------- /src/imgs/button_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paladin-t/begui/HEAD/src/imgs/button_up.png -------------------------------------------------------------------------------- /src/imgs/panel_gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paladin-t/begui/HEAD/src/imgs/panel_gray.png -------------------------------------------------------------------------------- /src/imgs/button_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paladin-t/begui/HEAD/src/imgs/button_close.png -------------------------------------------------------------------------------- /src/imgs/button_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paladin-t/begui/HEAD/src/imgs/button_down.png -------------------------------------------------------------------------------- /src/imgs/button_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paladin-t/begui/HEAD/src/imgs/button_left.png -------------------------------------------------------------------------------- /src/imgs/button_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paladin-t/begui/HEAD/src/imgs/button_right.png -------------------------------------------------------------------------------- /src/imgs/panel_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paladin-t/begui/HEAD/src/imgs/panel_white.png -------------------------------------------------------------------------------- /src/imgs/progressbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paladin-t/begui/HEAD/src/imgs/progressbar.png -------------------------------------------------------------------------------- /imgs/docking_absolutely.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paladin-t/begui/HEAD/imgs/docking_absolutely.png -------------------------------------------------------------------------------- /imgs/docking_relatively.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paladin-t/begui/HEAD/imgs/docking_relatively.png -------------------------------------------------------------------------------- /src/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 0, 3 | "title": "beGUI | Bitty Engine", 4 | "description": "Minimal customizable GUI system for Bitty Engine", 5 | "author": "Tony", 6 | "version": "1.5.9", 7 | "genre": "LIB", 8 | "url": "https://github.com/paladin-t/begui" 9 | } -------------------------------------------------------------------------------- /src/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "application": { 3 | "_window": { 4 | "display_index": 0, 5 | "fullscreen": false, 6 | "maximized": false, 7 | "size": [ 8 | 480, 9 | 320 10 | ] 11 | }, 12 | "pause_on_focus_lost": false 13 | }, 14 | "canvas": { 15 | "state": 0, 16 | "fix_ratio": true 17 | }, 18 | "input": { 19 | "onscreen_gamepad": { 20 | "enabled": false, 21 | "swap_ab": false, 22 | "scale": 1.0, 23 | "padding": [ 24 | 8.0, 25 | 12.0 26 | ] 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (C) 2021 - 2022 Tony Wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/libs/beGUI/LICENSE_beGUI.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (C) 2021 - 2022 Tony Wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/libs/beGUI/beGUI_Structures.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | The MIT License 3 | 4 | Copyright (C) 2021 - 2022 Tony Wang 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | ]] 23 | 24 | local beClass = require 'libs/beGUI/beClass' 25 | 26 | --[[ 27 | Helper structures. 28 | ]] 29 | 30 | local Percent = beClass.class({ 31 | amount = 0, 32 | 33 | -- Constructs a Percent structure. 34 | -- `amount`: typically [0, 100] 35 | ctor = function (self, amount) 36 | self.amount = amount / 100 37 | end, 38 | 39 | __mul = function (self, num) 40 | return self.amount * num 41 | end 42 | }) 43 | 44 | local function percent(amount) 45 | return Percent.new(amount) 46 | end 47 | 48 | --[[ 49 | Exporting. 50 | ]] 51 | 52 | return { 53 | Percent = Percent, 54 | percent = percent 55 | } 56 | -------------------------------------------------------------------------------- /src/libs/beGUI/beClass.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | The MIT License 3 | 4 | Copyright (C) 2021 - 2022 Tony Wang 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | ]] 23 | 24 | local function class(kls, base) 25 | if not base then 26 | base = { } 27 | end 28 | 29 | kls.new = function (...) 30 | local obj = { } 31 | setmetatable(obj, kls) 32 | if obj.ctor then 33 | obj:ctor(...) 34 | end 35 | 36 | return obj 37 | end 38 | 39 | kls.__index = kls 40 | 41 | setmetatable(kls, base) 42 | 43 | return kls 44 | end 45 | 46 | local function is(obj, kls) 47 | if kls == nil then 48 | error('Invalid class.') 49 | end 50 | 51 | repeat 52 | if obj == kls then 53 | return true 54 | end 55 | obj = getmetatable(obj) 56 | until not obj 57 | 58 | return false 59 | end 60 | 61 | local beClass = { 62 | class = class, 63 | is = is 64 | } 65 | 66 | return beClass 67 | -------------------------------------------------------------------------------- /src/libs/beGUI/beGUI_Custom.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | The MIT License 3 | 4 | Copyright (C) 2021 - 2022 Tony Wang 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | ]] 23 | 24 | local beClass = require 'libs/beGUI/beClass' 25 | local beWidget = require 'libs/beGUI/beGUI_Widget' 26 | 27 | --[[ 28 | Widgets. 29 | ]] 30 | 31 | local Custom = beClass.class({ 32 | _name = 'Custom', 33 | 34 | -- Constructs a Custom Widget. 35 | ctor = function (self, name) 36 | beWidget.Widget.ctor(self) 37 | 38 | if name then 39 | self._name = name 40 | end 41 | end, 42 | 43 | __tostring = function (self) 44 | return self.name 45 | end, 46 | 47 | navigatable = function (self) 48 | return 'children' 49 | end, 50 | 51 | name = function (self) 52 | return self._name 53 | end, 54 | setName = function (self, val) 55 | self._name = val 56 | 57 | return self 58 | end, 59 | 60 | _update = function (self, theme, delta, dx, dy, event) 61 | if not self.visibility then 62 | return 63 | end 64 | 65 | local ox, oy = self:offset() 66 | local px, py = self:position() 67 | local x, y = dx + px + ox, dy + py + oy 68 | local w, h = self:size() 69 | self:_trigger('updated', self, x, y, w, h, delta, event) 70 | 71 | beWidget.Widget._update(self, theme, delta, dx, dy, event) 72 | end 73 | }, beWidget.Widget) 74 | 75 | --[[ 76 | Exporting. 77 | ]] 78 | 79 | return { 80 | Custom = Custom 81 | } 82 | -------------------------------------------------------------------------------- /src/keycode.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Example for the Bitty Engine 3 | 4 | Copyright (C) 2020 - 2022 Tony Wang, all rights reserved 5 | 6 | Homepage: https://paladin-t.github.io/bitty/ 7 | ]] 8 | 9 | local function asc(s) 10 | return string.byte(s, 1) 11 | end 12 | 13 | local function toKeycode(k) 14 | return (1 << 30) | k 15 | end 16 | 17 | KeyCode = { 18 | Return = 13, 19 | Esc = 27, 20 | Backspace = 8, 21 | Tab = 9, 22 | Space = asc(' '), 23 | 24 | Exclaim = asc('!'), 25 | DoubleQuote = 34, 26 | Hash = asc('#'), 27 | Percent = asc('%'), 28 | Dollar = asc('$'), 29 | Ampersand = asc('&'), 30 | Quote = asc('\''), 31 | LeftParenthesis = asc('('), 32 | RightParenthesis = asc(')'), 33 | Asterisk = asc('*'), 34 | Plus = asc('+'), 35 | Comma = asc(','), 36 | Minus = asc('-'), 37 | Period = asc('.'), 38 | Slash = asc('/'), 39 | 40 | Num0 = asc('0'), 41 | Num1 = asc('1'), 42 | Num2 = asc('2'), 43 | Num3 = asc('3'), 44 | Num4 = asc('4'), 45 | Num5 = asc('5'), 46 | Num6 = asc('6'), 47 | Num7 = asc('7'), 48 | Num8 = asc('8'), 49 | Num9 = asc('9'), 50 | 51 | Colon = asc(':'), 52 | Semicolon = asc(';'), 53 | Less = asc('<'), 54 | Equals = asc('='), 55 | Greater = asc('>'), 56 | Question = asc('?'), 57 | At = asc('@'), 58 | LeftBracket = asc('['), 59 | Backslash = asc('\\'), 60 | RightBracket = asc(']'), 61 | Caret = asc('^'), 62 | Underscore = asc('_'), 63 | Backquote = asc('`'), 64 | 65 | A = asc('a'), 66 | B = asc('b'), 67 | C = asc('c'), 68 | D = asc('d'), 69 | E = asc('e'), 70 | F = asc('f'), 71 | G = asc('g'), 72 | H = asc('h'), 73 | I = asc('i'), 74 | J = asc('j'), 75 | K = asc('k'), 76 | L = asc('l'), 77 | M = asc('m'), 78 | N = asc('n'), 79 | O = asc('o'), 80 | P = asc('p'), 81 | Q = asc('q'), 82 | R = asc('r'), 83 | S = asc('s'), 84 | T = asc('t'), 85 | U = asc('u'), 86 | V = asc('v'), 87 | W = asc('w'), 88 | X = asc('x'), 89 | Y = asc('y'), 90 | Z = asc('z'), 91 | 92 | F1 = toKeycode(58), 93 | F2 = toKeycode(59), 94 | F3 = toKeycode(60), 95 | F4 = toKeycode(61), 96 | F5 = toKeycode(62), 97 | F6 = toKeycode(63), 98 | F7 = toKeycode(64), 99 | F8 = toKeycode(65), 100 | F9 = toKeycode(66), 101 | F10 = toKeycode(67), 102 | F11 = toKeycode(68), 103 | F12 = toKeycode(69), 104 | 105 | Insert = toKeycode(73), 106 | Home = toKeycode(74), 107 | PageUp = toKeycode(75), 108 | Delete = 127, 109 | End = toKeycode(77), 110 | PageDown = toKeycode(78), 111 | Right = toKeycode(79), 112 | Left = toKeycode(80), 113 | Down = toKeycode(81), 114 | Up = toKeycode(82), 115 | 116 | NumLockClear = toKeycode(83), 117 | KeypadDivide = toKeycode(84), 118 | KeypadMultiply = toKeycode(85), 119 | KeypadMinus = toKeycode(86), 120 | KeypadPlus = toKeycode(87), 121 | KeypadEnter = toKeycode(88), 122 | Keypad1 = toKeycode(89), 123 | Keypad2 = toKeycode(90), 124 | Keypad3 = toKeycode(91), 125 | Keypad4 = toKeycode(92), 126 | Keypad5 = toKeycode(93), 127 | Keypad6 = toKeycode(94), 128 | Keypad7 = toKeycode(95), 129 | Keypad8 = toKeycode(96), 130 | Keypad9 = toKeycode(97), 131 | Keypad0 = toKeycode(98), 132 | KeypadPeriod = toKeycode(99), 133 | 134 | LCtrl = toKeycode(224), 135 | LShift = toKeycode(225), 136 | LAlt = toKeycode(226), 137 | LGui = toKeycode(227), 138 | RCtrl = toKeycode(228), 139 | RShift = toKeycode(229), 140 | RAlt = toKeycode(230), 141 | RGui = toKeycode(231) 142 | } 143 | -------------------------------------------------------------------------------- /src/libs/beGUI/beStack.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | The MIT License 3 | 4 | Copyright (C) 2021 - 2022 Tony Wang 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | ]] 23 | 24 | local beClass = require 'libs/beGUI/beClass' 25 | 26 | --[[ 27 | Stack. 28 | ]] 29 | 30 | local NonShrinkStack = beClass.class({ 31 | _threshold = nil, 32 | _stack = nil, 33 | _count = 0, 34 | 35 | ctor = function (self, threshold) 36 | self._threshold = threshold or 5 37 | self._stack = { } 38 | self._count = 0 39 | end, 40 | 41 | __tostring = function (self) 42 | return 'NonShrinkStack' 43 | end, 44 | 45 | __len = function (self) 46 | return self._count 47 | end, 48 | 49 | push = function (self, arg1, arg2) 50 | if self._count >= self._threshold then 51 | error('Stack overflow.') 52 | end 53 | if self._count >= #self._stack then 54 | table.insert(self._stack, { arg1, arg2 }) 55 | self._count = self._count + 1 56 | else 57 | self._count = self._count + 1 58 | local obj = self._stack[self._count] 59 | obj[1], obj[2] = arg1, arg2 60 | end 61 | 62 | return self 63 | end, 64 | pop = function (self) 65 | if self._count == 0 then 66 | error('Pop from empty stack.') 67 | end 68 | local result = self._stack[self._count] 69 | self._count = self._count - 1 70 | local ret1, ret2 = result[1], result[2] 71 | result[1], result[2] = false, false 72 | 73 | return ret1, ret2 74 | end, 75 | top = function (self) 76 | if self._count == 0 then 77 | return nil 78 | end 79 | local result = self._stack[self._count] 80 | 81 | return result[1], result[2] 82 | end, 83 | count = function (self) 84 | return self._count 85 | end, 86 | empty = function (self) 87 | return self._count == 0 88 | end, 89 | clear = function (self) 90 | while not self:empty() do 91 | self:pop() 92 | end 93 | 94 | return self 95 | end, 96 | get = function (self, index) 97 | if index < 1 or index > self._count then 98 | return nil 99 | end 100 | 101 | return self._stack[index] 102 | end, 103 | set = function (self, index, data) 104 | if index < 1 or index > self._count then 105 | return self 106 | end 107 | 108 | self._stack[index] = data 109 | 110 | return self 111 | end 112 | }) 113 | 114 | --[[ 115 | Exporting. 116 | ]] 117 | 118 | return { 119 | NonShrinkStack = NonShrinkStack 120 | } 121 | -------------------------------------------------------------------------------- /src/libs/beGUI/beGUI.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | The MIT License 3 | 4 | Copyright (C) 2021 - 2022 Tony Wang 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | ]] 23 | 24 | local beUtils = require 'libs/beGUI/beGUI_Utils' 25 | local beStructures = require 'libs/beGUI/beGUI_Structures' 26 | local beWidget = require 'libs/beGUI/beGUI_Widget' 27 | local beBasics = require 'libs/beGUI/beGUI_Basics' 28 | local beContainers = require 'libs/beGUI/beGUI_Containers' 29 | local bePopups = require 'libs/beGUI/beGUI_Popups' 30 | local beCustom = require 'libs/beGUI/beGUI_Custom' 31 | local beTween = require 'libs/beGUI/beTween' 32 | 33 | --[[ 34 | Exporting. 35 | ]] 36 | 37 | if not beGUI then 38 | beGUI = { } 39 | end 40 | beGUI = beUtils.merge( 41 | beGUI, 42 | { 43 | version = '1.5.9', 44 | 45 | -- Data structure to represent relative number. 46 | percent = beStructures.percent, 47 | 48 | -- Widget class, base for other widgets, also could be used as container of other widgets. 49 | Widget = beWidget.Widget, 50 | -- Label widget. 51 | Label = beBasics.Label, 52 | -- MultilineLabel widget. 53 | MultilineLabel = beBasics.MultilineLabel, 54 | -- Url widget. 55 | -- Events: 56 | -- 'clicked': function (sender) end 57 | Url = beBasics.Url, 58 | -- InputBox widget. 59 | -- Events: 60 | -- 'changed': function (sender, value) end 61 | InputBox = beBasics.InputBox, 62 | -- Picture widget. 63 | Picture = beBasics.Picture, 64 | -- Button widget. 65 | -- Events: 66 | -- 'clicked': function (sender) end 67 | Button = beBasics.Button, 68 | -- PictureButton widget. 69 | -- Events: 70 | -- 'clicked': function (sender) end 71 | PictureButton = beBasics.PictureButton, 72 | -- CheckBox widget. 73 | -- Events: 74 | -- 'changed': function (sender, value) end 75 | CheckBox = beBasics.CheckBox, 76 | -- RadioBox widget. 77 | -- Events: 78 | -- 'changed': function (sender, value) end 79 | RadioBox = beBasics.RadioBox, 80 | -- ComboBox widget. 81 | -- Events: 82 | -- 'changed': function (sender, value) end 83 | ComboBox = beBasics.ComboBox, 84 | -- NumberBox widget. 85 | -- Events: 86 | -- 'changed': function (sender, value) end 87 | NumberBox = beBasics.NumberBox, 88 | -- ProgressBar widget. 89 | -- Events: 90 | -- 'changed': function (sender, value, maxValue, shadowValue) end 91 | ProgressBar = beBasics.ProgressBar, 92 | -- Slide widget. 93 | -- Events: 94 | -- 'changed': function (sender, value) end 95 | Slide = beBasics.Slide, 96 | -- Group widget. 97 | Group = beContainers.Group, 98 | -- List widget. 99 | List = beContainers.List, 100 | -- Draggable widget. 101 | Draggable = beContainers.Draggable, 102 | -- Droppable widget. 103 | -- Events: 104 | -- 'entered': function (sender, draggable) end 105 | -- 'left': function (sender, draggable) end 106 | -- 'dropping': function (sender, draggable) return droppable end 107 | -- 'dropped': function (sender, draggable) end 108 | -- 'clicked': function (sender) end 109 | Droppable = beContainers.Droppable, 110 | -- Tab widget. 111 | -- Events: 112 | -- 'changed': function (sender, value) end 113 | Tab = beContainers.Tab, 114 | -- Popup widget. 115 | Popup = bePopups.Popup, 116 | -- MessageBox widget. 117 | -- Events: 118 | -- 'canceled': function (sender) end 119 | -- 'confirmed': function (sender) end 120 | MessageBox = bePopups.MessageBox, 121 | -- QuestionBox widget. 122 | -- Events: 123 | -- 'canceled': function (sender) end 124 | -- 'confirmed': function (sender) end 125 | -- 'denied': function (sender) end 126 | QuestionBox = bePopups.QuestionBox, 127 | -- Custom widget. 128 | -- Events: 129 | -- 'updated': function (sender, x, y, w, h, delta, event) end 130 | Custom = beCustom.Custom, 131 | -- Tweening helper. 132 | Tween = beTween.Tween 133 | } 134 | ) 135 | -------------------------------------------------------------------------------- /src/libs/beGUI/beTheme.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | The MIT License 3 | 4 | Copyright (C) 2021 - 2022 Tony Wang 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | ]] 23 | 24 | local function default() 25 | local font_ = Font.new('fonts/ascii 8x8.png', Vec2.new(8, 8)) 26 | 27 | return { 28 | ['font'] = { 29 | resource = font_, 30 | color = Color.new(0, 0, 0) 31 | }, 32 | ['font_white'] = { 33 | resource = font_, 34 | color = Color.new(255, 255, 255) 35 | }, 36 | ['font_placeholder'] = { 37 | resource = font_, 38 | color = Color.new(138, 138, 138) 39 | }, 40 | ['font_title'] = { 41 | resource = font_, 42 | color = Color.new(255, 255, 255) 43 | }, 44 | ['font_url'] = { 45 | resource = font_, 46 | color = Color.new(0, 102, 255) 47 | }, 48 | ['font_url_hover'] = { 49 | resource = font_, 50 | color = Color.new(0, 162, 255) 51 | }, 52 | 53 | ['label'] = { 54 | content_offset = nil 55 | }, 56 | ['label_shadow'] = { 57 | content_offset = { 1, 1 } 58 | }, 59 | ['multilinelabel'] = { 60 | content_offset = nil 61 | }, 62 | 63 | ['url'] = { 64 | content_offset = nil 65 | }, 66 | ['url_down'] = { 67 | content_offset = nil 68 | }, 69 | 70 | ['inputbox'] = { 71 | resource = Resources.load('imgs/panel_white.png'), 72 | area = { 0, 0, 17, 17 }, 73 | content_offset = { 2, 0, 2 } 74 | }, 75 | 76 | ['button'] = { 77 | resource = Resources.load('imgs/button.png'), 78 | area = { 0, 0, 23, 23 }, 79 | content_offset = nil 80 | }, 81 | ['button_down'] = { 82 | resource = Resources.load('imgs/button.png'), 83 | area = { 0, 23, 23, 23 }, 84 | content_offset = { 0, 1 } 85 | }, 86 | ['button_disabled'] = { 87 | resource = Resources.load('imgs/button.png'), 88 | area = { 0, 46, 23, 23 }, 89 | content_offset = nil 90 | }, 91 | 92 | ['button_close'] = { 93 | resource = Resources.load('imgs/button_close.png'), 94 | area = { 0, 0, 19, 19 }, 95 | content_offset = nil 96 | }, 97 | ['button_close_down'] = { 98 | resource = Resources.load('imgs/button_close.png'), 99 | area = { 0, 19, 19, 19 }, 100 | content_offset = nil 101 | }, 102 | 103 | ['checkbox'] = { 104 | resource = Resources.load('imgs/checkbox.png'), 105 | area = { 0, 0, 13, 13 }, 106 | content_offset = { 16, 1 } 107 | }, 108 | ['checkbox_selected'] = { 109 | resource = Resources.load('imgs/checkbox.png'), 110 | area = { 0, 13, 13, 13 }, 111 | content_offset = { 16, 1 } 112 | }, 113 | ['checkbox_disabled'] = { 114 | resource = Resources.load('imgs/checkbox.png'), 115 | area = { 13, 0, 13, 13 }, 116 | content_offset = { 16, 1 } 117 | }, 118 | ['checkbox_selected_disabled'] = { 119 | resource = Resources.load('imgs/checkbox.png'), 120 | area = { 13, 13, 13, 13 }, 121 | content_offset = { 16, 1 } 122 | }, 123 | 124 | ['radiobox'] = { 125 | resource = Resources.load('imgs/radiobox.png'), 126 | area = { 0, 0, 13, 13 }, 127 | content_offset = { 16, 1 } 128 | }, 129 | ['radiobox_selected'] = { 130 | resource = Resources.load('imgs/radiobox.png'), 131 | area = { 0, 13, 13, 13 }, 132 | content_offset = { 16, 1 } 133 | }, 134 | 135 | ['combobox'] = { 136 | resource = Resources.load('imgs/panel_gray.png'), 137 | area = { 0, 0, 17, 17 }, 138 | content_offset = { 1, 1 } 139 | }, 140 | ['combobox_button_left'] = { 141 | resource = Resources.load('imgs/button_left.png'), 142 | area = { 0, 0, 17, 17 }, 143 | content_offset = nil 144 | }, 145 | ['combobox_button_left_down'] = { 146 | resource = Resources.load('imgs/button_left.png'), 147 | area = { 0, 17, 17, 17 }, 148 | content_offset = nil 149 | }, 150 | ['combobox_button_right'] = { 151 | resource = Resources.load('imgs/button_right.png'), 152 | area = { 0, 0, 17, 17 }, 153 | content_offset = nil 154 | }, 155 | ['combobox_button_right_down'] = { 156 | resource = Resources.load('imgs/button_right.png'), 157 | area = { 0, 17, 17, 17 }, 158 | content_offset = nil 159 | }, 160 | 161 | ['numberbox'] = { 162 | resource = Resources.load('imgs/panel_gray.png'), 163 | area = { 0, 0, 17, 17 }, 164 | content_offset = { 1, 1 } 165 | }, 166 | ['numberbox_button_up'] = { 167 | resource = Resources.load('imgs/button_up.png'), 168 | area = { 0, 0, 17, 17 }, 169 | content_offset = nil 170 | }, 171 | ['numberbox_button_up_down'] = { 172 | resource = Resources.load('imgs/button_up.png'), 173 | area = { 0, 17, 17, 17 }, 174 | content_offset = nil 175 | }, 176 | ['numberbox_button_down'] = { 177 | resource = Resources.load('imgs/button_down.png'), 178 | area = { 0, 0, 17, 17 }, 179 | content_offset = nil 180 | }, 181 | ['numberbox_button_down_down'] = { 182 | resource = Resources.load('imgs/button_down.png'), 183 | area = { 0, 17, 17, 17 }, 184 | content_offset = nil 185 | }, 186 | 187 | ['progressbar'] = { 188 | resource = Resources.load('imgs/progressbar.png'), 189 | area = { 0, 0, 17, 17 }, 190 | content_offset = { 2, 2 } 191 | }, 192 | 193 | ['slide'] = { 194 | resource = Resources.load('imgs/slide.png'), 195 | color = Color.new(0, 0, 0), 196 | area = { 0, 0, 13, 17 }, 197 | content_offset = nil 198 | }, 199 | 200 | ['group'] = { 201 | resource = nil, 202 | color = Color.new(0, 0, 0), 203 | area = nil, 204 | content_offset = { 8, 0 } 205 | }, 206 | ['group_title'] = { 207 | content_offset = nil 208 | }, 209 | 210 | ['list'] = { 211 | resource = Resources.load('imgs/panel_white.png'), 212 | color = Color.new(127, 127, 127, 128), 213 | area = { 0, 0, 17, 17 }, 214 | content_offset = nil 215 | }, 216 | 217 | ['tab'] = { 218 | resource = nil, 219 | color = Color.new(0, 0, 0), 220 | area = nil, 221 | content_offset = { 3, 3 } 222 | }, 223 | ['tab_title'] = { 224 | content_offset = nil 225 | }, 226 | 227 | ['window'] = { 228 | resource = Resources.load('imgs/window.png') 229 | } 230 | } 231 | end 232 | 233 | beTheme = { 234 | default = default 235 | } 236 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Bitty Engine 17 | 18 | 19 | 119 | 120 | 121 |
122 |
123 | 124 |
125 |
126 |
127 |
128 |
129 |
130 | 135 |

136 | beGUI live demo, source code 137 |

138 |

139 | Powered by Bitty Engine 140 |

141 |

142 |
143 |
144 |

145 |
146 | 218 | 233 | 234 | 235 | 236 | -------------------------------------------------------------------------------- /src/libs/beGUI/beGUI_Popups.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | The MIT License 3 | 4 | Copyright (C) 2021 - 2022 Tony Wang 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | ]] 23 | 24 | local beClass = require 'libs/beGUI/beClass' 25 | local beStructures = require 'libs/beGUI/beGUI_Structures' 26 | local beWidget = require 'libs/beGUI/beGUI_Widget' 27 | 28 | --[[ 29 | Widgets. 30 | ]] 31 | 32 | local Popup = beClass.class({ 33 | _scheduled = nil, 34 | 35 | -- Constructs a Popup. 36 | ctor = function (self) 37 | beWidget.Widget.ctor(self) 38 | end, 39 | 40 | __tostring = function (self) 41 | return 'Popup' 42 | end, 43 | 44 | navigatable = function (self) 45 | return 'children' 46 | end, 47 | 48 | schedule = function (self, func) 49 | self._scheduled = func 50 | end, 51 | 52 | _update = function (self, theme, delta, dx, dy, event) 53 | if not self.visibility then 54 | return 55 | end 56 | 57 | local ox, oy = self:offset() 58 | local px, py = self:position() 59 | local x, y = dx + px + ox, dy + py + oy 60 | local w, h = self:size() 61 | 62 | if self.transparency then 63 | local col = Color.new(80, 80, 80, 120 * (self.transparency / 255)) 64 | rect(x, y, x + w, y + h, true, col) 65 | else 66 | rect(x, y, x + w, y + h, true, Color.new(80, 80, 80, 120)) 67 | end 68 | 69 | beWidget.Widget._update(self, theme, delta, dx, dy, event) 70 | 71 | if self._scheduled then 72 | self._scheduled() 73 | self._scheduled = nil 74 | end 75 | end 76 | }, beWidget.Widget) 77 | 78 | local MessageBox = beClass.class({ 79 | _closable = true, 80 | _title = nil, 81 | _message = nil, 82 | _confirm = 'OK', 83 | _initialized = false, 84 | 85 | -- Constructs a MessageBox. 86 | -- `closable`: `true` to enable the close button, `false` to disable 87 | -- `title`: the title text 88 | -- `message`: the message text 89 | -- `confirm`: the text for the confirm button 90 | ctor = function (self, closable, title, message, confirm) 91 | Popup.ctor(self) 92 | 93 | self._closable = closable 94 | self._title = title or 'Bitty Engine' 95 | self._message = message 96 | if confirm then 97 | self._confirm = confirm 98 | end 99 | 100 | local P = beStructures.percent 101 | self 102 | :setId('popup') 103 | :anchor(0, 0) 104 | :put(0, 0) 105 | :resize(P(100), P(100)) 106 | end, 107 | 108 | __tostring = function (self) 109 | return 'MessageBox' 110 | end, 111 | 112 | _update = function (self, theme, delta, dx, dy, event) 113 | if not self._initialized then 114 | self._initialized = true 115 | 116 | local width = 256 117 | local P = beStructures.percent 118 | local child = beGUI.Picture.new(theme['window'], true) 119 | :setId('picture') 120 | :anchor(0.5, 0.5) 121 | :put(P(50), P(50)) 122 | :resize(width, 128) 123 | :addChild( 124 | beGUI.Label.new(self._title, 'center', true, 'font_title') 125 | :setId('label_title') 126 | :anchor(0.5, 0) 127 | :put(P(50), 3) 128 | :resize(P(100), 23) 129 | ) 130 | :addChild( 131 | beGUI.Label.new(self._message, 'center', true) 132 | :setId('label_message') 133 | :anchor(0.5, 1) 134 | :put(P(50), P(50)) 135 | :resize(P(100), 23) 136 | ) 137 | :addChild( 138 | beGUI.Button.new(self._confirm) 139 | :setId('button_confirm') 140 | :anchor(0.5, 1) 141 | :put(P(50), P(90)) 142 | :resize(P(24), 23) 143 | :on('clicked', function (sender) 144 | self:_trigger('confirmed', self) 145 | end) 146 | ) 147 | if self._closable then 148 | child 149 | :addChild( 150 | beGUI.PictureButton.new('', false, { normal = 'button_close', pressed = 'button_close_down' }) 151 | :setId('button_close') 152 | :anchor(1, 0) 153 | :put(width - 6, 5) 154 | :resize(19, 19) 155 | :on('clicked', function (sender) 156 | self:_trigger('canceled', self) 157 | end) 158 | ) 159 | end 160 | self:addChild(child) 161 | 162 | local w, h = self:size() 163 | child:_updateLayout(w, h) 164 | end 165 | 166 | Popup._update(self, theme, delta, dx, dy, event) 167 | end, 168 | 169 | _cancelNavigation = function (self) 170 | if self._closable then 171 | self:_trigger('canceled', self) 172 | end 173 | 174 | self.context.focus = nil 175 | self.context.navigated = false 176 | end 177 | }, Popup) 178 | 179 | local QuestionBox = beClass.class({ 180 | _closable = true, 181 | _title = nil, 182 | _message = nil, 183 | _confirm = 'Yes', 184 | _deny = 'No', 185 | _initialized = false, 186 | 187 | -- Constructs a QuestionBox. 188 | -- `closable`: `true` to enable the close button, `false` to disable 189 | -- `title`: the title text 190 | -- `message`: the message text 191 | -- `confirm`: the text for the confirm button 192 | -- `deny`: the text for the deny button 193 | ctor = function (self, closable, title, message, confirm, deny) 194 | Popup.ctor(self) 195 | 196 | self._closable = closable 197 | self._title = title or 'Bitty Engine' 198 | self._message = message 199 | if confirm then 200 | self._confirm = confirm 201 | end 202 | if deny then 203 | self._deny = deny 204 | end 205 | 206 | local P = beStructures.percent 207 | self 208 | :setId('popup') 209 | :anchor(0, 0) 210 | :put(0, 0) 211 | :resize(P(100), P(100)) 212 | end, 213 | 214 | __tostring = function (self) 215 | return 'QuestionBox' 216 | end, 217 | 218 | _update = function (self, theme, delta, dx, dy, event) 219 | if not self._initialized then 220 | self._initialized = true 221 | 222 | local width = 256 223 | local P = beStructures.percent 224 | local child = beGUI.Picture.new(theme['window'], true) 225 | :setId('picture') 226 | :anchor(0.5, 0.5) 227 | :put(P(50), P(50)) 228 | :resize(width, 128) 229 | :addChild( 230 | beGUI.Label.new(self._title, 'center', true, 'font_title') 231 | :setId('label_title') 232 | :anchor(0.5, 0) 233 | :put(P(50), 3) 234 | :resize(P(100), 23) 235 | ) 236 | :addChild( 237 | beGUI.Label.new(self._message, 'center', true) 238 | :setId('label_message') 239 | :anchor(0.5, 1) 240 | :put(P(50), P(50)) 241 | :resize(P(100), 23) 242 | ) 243 | :addChild( 244 | beGUI.Button.new(self._confirm) 245 | :setId('button_confirm') 246 | :anchor(1.1, 1) 247 | :put(P(50), P(90)) 248 | :resize(P(24), 23) 249 | :on('clicked', function (sender) 250 | self:_trigger('confirmed', self) 251 | end) 252 | ) 253 | :addChild( 254 | beGUI.Button.new(self._deny) 255 | :setId('button_deny') 256 | :anchor(-0.1, 1) 257 | :put(P(50), P(90)) 258 | :resize(P(24), 23) 259 | :on('clicked', function (sender) 260 | self:_trigger('denied', self) 261 | end) 262 | ) 263 | if self._closable then 264 | child 265 | :addChild( 266 | beGUI.PictureButton.new('', false, { normal = 'button_close', pressed = 'button_close_down' }) 267 | :setId('button_close') 268 | :anchor(1, 0) 269 | :put(width - 6, 5) 270 | :resize(19, 19) 271 | :on('clicked', function (sender) 272 | self:_trigger('canceled', self) 273 | end) 274 | ) 275 | end 276 | self:addChild(child) 277 | 278 | local w, h = self:size() 279 | child:_updateLayout(w, h) 280 | end 281 | 282 | Popup._update(self, theme, delta, dx, dy, event) 283 | end, 284 | 285 | _cancelNavigation = function (self) 286 | if self._closable then 287 | self:_trigger('canceled', self) 288 | end 289 | 290 | self.context.focus = nil 291 | self.context.navigated = false 292 | end 293 | }, Popup) 294 | 295 | --[[ 296 | Exporting. 297 | ]] 298 | 299 | return { 300 | Popup = Popup, 301 | MessageBox = MessageBox, 302 | QuestionBox = QuestionBox 303 | } 304 | -------------------------------------------------------------------------------- /src/libs/beGUI/beTween.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | MIT LICENSE 3 | 4 | Copyright (c) 2014 Enrique García Cota, Yuichi Tateno, Emmanuel Oga 5 | Adapted by Tony for Bitty Engine 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a 8 | copy of this software and associated documentation files (the 9 | "Software"), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included 16 | in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 19 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | ]] 26 | 27 | local tween = { 28 | _VERSION = 'Tween 2.1.1', 29 | _DESCRIPTION = 'Tweening for Lua', 30 | _URL = 'https://github.com/kikito/tween.lua' 31 | } 32 | 33 | -- Easing. 34 | 35 | -- Adapted from https://github.com/EmmanuelOga/easing. 36 | -- For all easing functions: 37 | -- t = time == how much time has to pass for the tweening to complete 38 | -- b = begin == starting property value 39 | -- c = change == ending - beginning 40 | -- d = duration == running time. How much time has passed *right now* 41 | 42 | local pow, sin, cos, pi, sqrt, abs, asin = function (a, b) return a ^ b end, math.sin, math.cos, math.pi, math.sqrt, math.abs, math.asin 43 | 44 | -- Linear. 45 | local function linear(t, b, c, d) 46 | return c * t / d + b 47 | end 48 | 49 | -- Quad. 50 | local function inQuad(t, b, c, d) 51 | return c * pow(t / d, 2) + b 52 | end 53 | local function outQuad(t, b, c, d) 54 | t = t / d 55 | 56 | return -c * t * (t - 2) + b 57 | end 58 | local function inOutQuad(t, b, c, d) 59 | t = t / d * 2 60 | if t < 1 then 61 | return c / 2 * pow(t, 2) + b 62 | end 63 | 64 | return -c / 2 * ((t - 1) * (t - 3) - 1) + b 65 | end 66 | local function outInQuad(t, b, c, d) 67 | if t < d / 2 then 68 | return outQuad(t * 2, b, c / 2, d) 69 | end 70 | 71 | return inQuad((t * 2) - d, b + c / 2, c / 2, d) 72 | end 73 | 74 | -- Cubic. 75 | local function inCubic (t, b, c, d) 76 | return c * pow(t / d, 3) + b 77 | end 78 | local function outCubic(t, b, c, d) 79 | return c * (pow(t / d - 1, 3) + 1) + b 80 | end 81 | local function inOutCubic(t, b, c, d) 82 | t = t / d * 2 83 | if t < 1 then 84 | return c / 2 * t * t * t + b 85 | end 86 | t = t - 2 87 | 88 | return c / 2 * (t * t * t + 2) + b 89 | end 90 | local function outInCubic(t, b, c, d) 91 | if t < d / 2 then 92 | return outCubic(t * 2, b, c / 2, d) 93 | end 94 | 95 | return inCubic((t * 2) - d, b + c / 2, c / 2, d) 96 | end 97 | 98 | -- Quart. 99 | local function inQuart(t, b, c, d) 100 | return c * pow(t / d, 4) + b 101 | end 102 | local function outQuart(t, b, c, d) 103 | return -c * (pow(t / d - 1, 4) - 1) + b 104 | end 105 | local function inOutQuart(t, b, c, d) 106 | t = t / d * 2 107 | if t < 1 then 108 | return c / 2 * pow(t, 4) + b 109 | end 110 | 111 | return -c / 2 * (pow(t - 2, 4) - 2) + b 112 | end 113 | local function outInQuart(t, b, c, d) 114 | if t < d / 2 then 115 | return outQuart(t * 2, b, c / 2, d) 116 | end 117 | 118 | return inQuart((t * 2) - d, b + c / 2, c / 2, d) 119 | end 120 | 121 | -- Quint. 122 | local function inQuint(t, b, c, d) 123 | return c * pow(t / d, 5) + b 124 | end 125 | local function outQuint(t, b, c, d) 126 | return c * (pow(t / d - 1, 5) + 1) + b 127 | end 128 | local function inOutQuint(t, b, c, d) 129 | t = t / d * 2 130 | if t < 1 then 131 | return c / 2 * pow(t, 5) + b 132 | end 133 | 134 | return c / 2 * (pow(t - 2, 5) + 2) + b 135 | end 136 | local function outInQuint(t, b, c, d) 137 | if t < d / 2 then 138 | return outQuint(t * 2, b, c / 2, d) 139 | end 140 | 141 | return inQuint((t * 2) - d, b + c / 2, c / 2, d) 142 | end 143 | 144 | -- Sine. 145 | local function inSine(t, b, c, d) 146 | return -c * cos(t / d * (pi / 2)) + c + b 147 | end 148 | local function outSine(t, b, c, d) 149 | return c * sin(t / d * (pi / 2)) + b 150 | end 151 | local function inOutSine(t, b, c, d) 152 | return -c / 2 * (cos(pi * t / d) - 1) + b 153 | end 154 | local function outInSine(t, b, c, d) 155 | if t < d / 2 then 156 | return outSine(t * 2, b, c / 2, d) 157 | end 158 | 159 | return inSine((t * 2) -d, b + c / 2, c / 2, d) 160 | end 161 | 162 | -- Expo. 163 | local function inExpo(t, b, c, d) 164 | if t == 0 then 165 | return b 166 | end 167 | 168 | return c * pow(2, 10 * (t / d - 1)) + b - c * 0.001 169 | end 170 | local function outExpo(t, b, c, d) 171 | if t == d then 172 | return b + c 173 | end 174 | 175 | return c * 1.001 * (-pow(2, -10 * t / d) + 1) + b 176 | end 177 | local function inOutExpo(t, b, c, d) 178 | if t == 0 then 179 | return b 180 | end 181 | if t == d then 182 | return b + c 183 | end 184 | t = t / d * 2 185 | if t < 1 then 186 | return c / 2 * pow(2, 10 * (t - 1)) + b - c * 0.0005 187 | end 188 | 189 | return c / 2 * 1.0005 * (-pow(2, -10 * (t - 1)) + 2) + b 190 | end 191 | local function outInExpo(t, b, c, d) 192 | if t < d / 2 then 193 | return outExpo(t * 2, b, c / 2, d) 194 | end 195 | 196 | return inExpo((t * 2) - d, b + c / 2, c / 2, d) 197 | end 198 | 199 | -- Circ. 200 | local function inCirc(t, b, c, d) 201 | return (-c * (sqrt(1 - pow(t / d, 2)) - 1) + b) 202 | end 203 | local function outCirc(t, b, c, d) 204 | return (c * sqrt(1 - pow(t / d - 1, 2)) + b) 205 | end 206 | local function inOutCirc(t, b, c, d) 207 | t = t / d * 2 208 | if t < 1 then 209 | return -c / 2 * (sqrt(1 - t * t) - 1) + b 210 | end 211 | t = t - 2 212 | 213 | return c / 2 * (sqrt(1 - t * t) + 1) + b 214 | end 215 | local function outInCirc(t, b, c, d) 216 | if t < d / 2 then 217 | return outCirc(t * 2, b, c / 2, d) 218 | end 219 | 220 | return inCirc((t * 2) - d, b + c / 2, c / 2, d) 221 | end 222 | 223 | -- Elastic. 224 | local function calculatePAS(p, a, c, d) 225 | p, a = p or d * 0.3, a or 0 226 | if a < abs(c) then 227 | return p, c, p / 4 228 | end -- p, a, s. 229 | 230 | return p, a, p / (2 * pi) * asin(c / a) -- p, a, s. 231 | end 232 | local function inElastic(t, b, c, d, a, p) 233 | local s 234 | if t == 0 then 235 | return b 236 | end 237 | t = t / d 238 | if t == 1 then 239 | return b + c 240 | end 241 | p, a, s = calculatePAS(p, a, c, d) 242 | t = t - 1 243 | 244 | return -(a * pow(2, 10 * t) * sin((t * d - s) * (2 * pi) / p)) + b 245 | end 246 | local function outElastic(t, b, c, d, a, p) 247 | local s 248 | if t == 0 then 249 | return b 250 | end 251 | t = t / d 252 | if t == 1 then 253 | return b + c 254 | end 255 | p, a, s = calculatePAS(p, a, c, d) 256 | 257 | return a * pow(2, -10 * t) * sin((t * d - s) * (2 * pi) / p) + c + b 258 | end 259 | local function inOutElastic(t, b, c, d, a, p) 260 | local s 261 | if t == 0 then 262 | return b 263 | end 264 | t = t / d * 2 265 | if t == 2 then 266 | return b + c 267 | end 268 | p, a, s = calculatePAS(p, a, c, d) 269 | t = t - 1 270 | if t < 0 then 271 | return -0.5 * (a * pow(2, 10 * t) * sin((t * d - s) * (2 * pi) / p)) + b 272 | end 273 | 274 | return a * pow(2, -10 * t) * sin((t * d - s) * (2 * pi) / p) * 0.5 + c + b 275 | end 276 | local function outInElastic(t, b, c, d, a, p) 277 | if t < d / 2 then 278 | return outElastic(t * 2, b, c / 2, d, a, p) 279 | end 280 | 281 | return inElastic((t * 2) - d, b + c / 2, c / 2, d, a, p) 282 | end 283 | 284 | -- Back. 285 | local function inBack(t, b, c, d, s) 286 | s = s or 1.70158 287 | t = t / d 288 | 289 | return c * t * t * ((s + 1) * t - s) + b 290 | end 291 | local function outBack(t, b, c, d, s) 292 | s = s or 1.70158 293 | t = t / d - 1 294 | 295 | return c * (t * t * ((s + 1) * t + s) + 1) + b 296 | end 297 | local function inOutBack(t, b, c, d, s) 298 | s = (s or 1.70158) * 1.525 299 | t = t / d * 2 300 | if t < 1 then 301 | return c / 2 * (t * t * ((s + 1) * t - s)) + b 302 | end 303 | t = t - 2 304 | 305 | return c / 2 * (t * t * ((s + 1) * t + s) + 2) + b 306 | end 307 | local function outInBack(t, b, c, d, s) 308 | if t < d / 2 then 309 | return outBack(t * 2, b, c / 2, d, s) 310 | end 311 | 312 | return inBack((t * 2) - d, b + c / 2, c / 2, d, s) 313 | end 314 | 315 | -- Bounce. 316 | local function outBounce(t, b, c, d) 317 | t = t / d 318 | if t < 1 / 2.75 then 319 | return c * (7.5625 * t * t) + b 320 | end 321 | if t < 2 / 2.75 then 322 | t = t - (1.5 / 2.75) 323 | 324 | return c * (7.5625 * t * t + 0.75) + b 325 | elseif t < 2.5 / 2.75 then 326 | t = t - (2.25 / 2.75) 327 | 328 | return c * (7.5625 * t * t + 0.9375) + b 329 | end 330 | t = t - (2.625 / 2.75) 331 | 332 | return c * (7.5625 * t * t + 0.984375) + b 333 | end 334 | local function inBounce(t, b, c, d) 335 | return c - outBounce(d - t, 0, c, d) + b 336 | end 337 | local function inOutBounce(t, b, c, d) 338 | if t < d / 2 then 339 | return inBounce(t * 2, 0, c, d) * 0.5 + b 340 | end 341 | 342 | return outBounce(t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b 343 | end 344 | local function outInBounce(t, b, c, d) 345 | if t < d / 2 then 346 | return outBounce(t * 2, b, c / 2, d) 347 | end 348 | 349 | return inBounce((t * 2) - d, b + c / 2, c / 2, d) 350 | end 351 | 352 | tween.easing = { 353 | linear = linear, 354 | inQuad = inQuad, outQuad = outQuad, inOutQuad = inOutQuad, outInQuad = outInQuad, 355 | inCubic = inCubic, outCubic = outCubic, inOutCubic = inOutCubic, outInCubic = outInCubic, 356 | inQuart = inQuart, outQuart = outQuart, inOutQuart = inOutQuart, outInQuart = outInQuart, 357 | inQuint = inQuint, outQuint = outQuint, inOutQuint = inOutQuint, outInQuint = outInQuint, 358 | inSine = inSine, outSine = outSine, inOutSine = inOutSine, outInSine = outInSine, 359 | inExpo = inExpo, outExpo = outExpo, inOutExpo = inOutExpo, outInExpo = outInExpo, 360 | inCirc = inCirc, outCirc = outCirc, inOutCirc = inOutCirc, outInCirc = outInCirc, 361 | inElastic = inElastic, outElastic = outElastic, inOutElastic = inOutElastic, outInElastic = outInElastic, 362 | inBack = inBack, outBack = outBack, inOutBack = inOutBack, outInBack = outInBack, 363 | inBounce = inBounce, outBounce = outBounce, inOutBounce = inOutBounce, outInBounce = outInBounce 364 | } 365 | 366 | -- Private stuff. 367 | 368 | local function copyTables(destination, keysTable, valuesTable) 369 | valuesTable = valuesTable or keysTable 370 | local mt = getmetatable(keysTable) 371 | if mt and getmetatable(destination) == nil then 372 | setmetatable(destination, mt) 373 | end 374 | for k, v in pairs(keysTable) do 375 | if type(v) == 'table' then 376 | destination[k] = copyTables({ }, v, valuesTable[k]) 377 | else 378 | destination[k] = valuesTable[k] 379 | end 380 | end 381 | 382 | return destination 383 | end 384 | 385 | local function checkSubjectAndTargetRecursively(subject, target, path) 386 | path = path or { } 387 | local targetType, newPath 388 | for k, targetValue in pairs(target) do 389 | targetType, newPath = type(targetValue), copyTables({ }, path) 390 | table.insert(newPath, tostring(k)) 391 | if targetType == 'number' then 392 | assert(type(subject[k]) == 'number', "Parameter '" .. table.concat(newPath,'/') .. "' is missing from subject or isn't a number") 393 | elseif targetType == 'table' then 394 | checkSubjectAndTargetRecursively(subject[k], targetValue, newPath) 395 | else 396 | assert(targetType == 'number', "Parameter '" .. table.concat(newPath,'/') .. "' must be a number or table of numbers") 397 | end 398 | end 399 | end 400 | 401 | local function checkNewParams(duration, subject, target, easing) 402 | assert(type(duration) == 'number' and duration > 0, "duration must be a positive number. Was " .. tostring(duration)) 403 | local tsubject = type(subject) 404 | assert(tsubject == 'table' or tsubject == 'userdata', "subject must be a table or userdata. Was " .. tostring(subject)) 405 | assert(type(target) == 'table', "target must be a table. Was " .. tostring(target)) 406 | assert(type(easing) =='function', "easing must be a function. Was " .. tostring(easing)) 407 | checkSubjectAndTargetRecursively(subject, target) 408 | end 409 | 410 | local function getEasingFunction(easing) 411 | easing = easing or "linear" 412 | if type(easing) == 'string' then 413 | local name = easing 414 | easing = tween.easing[name] 415 | if type(easing) ~= 'function' then 416 | error("The easing function name '" .. name .. "' is invalid") 417 | end 418 | end 419 | 420 | return easing 421 | end 422 | 423 | local function performEasingOnSubject(subject, target, initial, clock, duration, easing) 424 | local t, b, c, d 425 | for k, v in pairs(target) do 426 | if type(v) == 'table' then 427 | performEasingOnSubject(subject[k], v, initial[k], clock, duration, easing) 428 | else 429 | t, b, c, d = clock, initial[k], v - initial[k], duration 430 | subject[k] = easing(t, b, c, d) 431 | end 432 | end 433 | end 434 | 435 | -- Tween methods. 436 | 437 | local Tween_idx = { } 438 | local Tween_mt = { __index = Tween_idx } 439 | 440 | function Tween_idx:reset() 441 | self.initial = self.initial or copyTables({ }, self.target, self.subject) 442 | self.clock = 0 443 | copyTables(self.subject, self.initial) 444 | self.finished = false 445 | 446 | return self 447 | end 448 | 449 | function Tween_idx:set(clock) 450 | assert(type(clock) == 'number', "clock must be a positive number or 0") 451 | 452 | if self.clock == clock then 453 | return self.clock >= self.duration 454 | end 455 | 456 | self.initial = self.initial or copyTables({ }, self.target, self.subject) 457 | self.clock = clock 458 | 459 | if self.clock <= 0 then 460 | self.clock = 0 461 | copyTables(self.subject, self.initial) 462 | elseif self.clock >= self.duration then -- The tween has expired. 463 | self.clock = self.duration 464 | copyTables(self.subject, self.target) 465 | else 466 | performEasingOnSubject(self.subject, self.target, self.initial, self.clock, self.duration, self.easing) 467 | end 468 | 469 | self:trigger('changed', self.subject) 470 | 471 | if self.clock < self.duration then 472 | return false 473 | end 474 | if self.loop then 475 | self:trigger('completed', self.subject) 476 | self.clock = 0 477 | 478 | return false 479 | end 480 | if not self.finished then 481 | self:trigger('completed', self.subject) 482 | self.finished = true 483 | end 484 | 485 | return true 486 | end 487 | 488 | function Tween_idx:update(delta) 489 | assert(type(delta) == 'number', "delta must be a number") 490 | 491 | return self:set(self.clock + delta) 492 | end 493 | 494 | function Tween_idx:on(event, handler) 495 | if not event then 496 | return self 497 | end 498 | if self.events == nil then 499 | self.events = { } 500 | end 501 | self.events[event] = handler 502 | 503 | return self 504 | end 505 | 506 | function Tween_idx:off(event) 507 | if not event then 508 | return self 509 | end 510 | if self.events == nil then 511 | return self 512 | end 513 | if self.events[event] == nil then 514 | return self 515 | end 516 | self.events[event] = nil 517 | 518 | return self 519 | end 520 | 521 | function Tween_idx:trigger(event, ...) 522 | if not event then 523 | return nil 524 | end 525 | if self.events == nil then 526 | return nil 527 | end 528 | if not self.events[event] then 529 | return nil 530 | end 531 | local ret = self.events[event](self, ...) 532 | 533 | return ret 534 | end 535 | 536 | -- Public interface. 537 | 538 | function tween.new(duration, subject, target, easing, loop) 539 | easing = getEasingFunction(easing) 540 | checkNewParams(duration, subject, target, easing) 541 | 542 | return setmetatable({ 543 | duration = duration, 544 | subject = subject, 545 | target = target, 546 | easing = easing, 547 | clock = 0, 548 | finished = false, 549 | loop = loop 550 | }, Tween_mt) 551 | end 552 | 553 | --[[ 554 | Exporting. 555 | ]] 556 | 557 | return { 558 | Tween = tween 559 | } 560 | -------------------------------------------------------------------------------- /src/libs/beGUI/beGUI_Utils.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | The MIT License 3 | 4 | Copyright (C) 2021 - 2022 Tony Wang 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | ]] 23 | 24 | --[[ 25 | Constants. 26 | ]] 27 | 28 | local function toKeycode(k) 29 | return (1 << 30) | k 30 | end 31 | 32 | local KeyCodeLShift = toKeycode(225) 33 | local KeyCodeRShift = toKeycode(229) 34 | 35 | --[[ 36 | Helper functions. 37 | ]] 38 | 39 | --[[ Number. ]] 40 | 41 | local function NaN(val) 42 | return 0 / 0 43 | end 44 | 45 | local function isNaN(val) 46 | return val ~= val 47 | end 48 | 49 | local function round(val) 50 | if val >= 0 then 51 | return math.floor(val + 0.5) 52 | else 53 | return math.ceil(val - 0.5) 54 | end 55 | end 56 | 57 | local function clamp(x, min, max) 58 | return math.max(math.min(x, max), min) 59 | end 60 | 61 | --[[ Math. ]] 62 | 63 | local function intersected(rect1, rect2) 64 | if not rect1 then 65 | return nil 66 | end 67 | if not rect2 then 68 | return rect1 69 | end 70 | 71 | local x1, y1, x2, y2 = 72 | math.max(rect1:xMin(), rect2:xMin()), 73 | math.max(rect1:yMin(), rect2:yMin()), 74 | math.min(rect1:xMax(), rect2:xMax()), 75 | math.min(rect1:yMax(), rect2:yMax()) 76 | if x1 >= x2 or y1 >= y2 then 77 | return nil 78 | end 79 | 80 | return Rect.new(x1, y1, x2, y2) 81 | end 82 | 83 | --[[ String. ]] 84 | 85 | local function startsWith(txt, part) 86 | return part == '' or string.sub(txt, 1, #part) == part 87 | end 88 | 89 | local function endsWith(str, part) 90 | return part == '' or string.sub(str, -#part) == part 91 | end 92 | 93 | local function split(txt, sep, pattern, translate) 94 | local result = { } 95 | for line in string.gmatch(txt, '[^\n]*') do 96 | if sep == nil then 97 | local i = 1 98 | for str in string.gmatch(line, pattern or '[\33-\127\192-\255]+[\128-\191]*') do 99 | if translate then 100 | str = translate(str, i) 101 | end 102 | local codes = { } 103 | for _, v in utf8.codes(str) do 104 | table.insert(codes, v) 105 | end 106 | local j = nil 107 | for i = #codes, 1, -1 do 108 | if codes[i] <= 255 then 109 | break 110 | end 111 | j = i 112 | end 113 | if j == nil then 114 | table.insert(result, str) 115 | else 116 | local first, second = '', '' 117 | for k = 1, j - 1, 1 do 118 | first = first .. utf8.char(codes[k]) 119 | end 120 | for k = j, #codes, 1 do 121 | second = second .. utf8.char(codes[k]) 122 | end 123 | if first ~= '' then 124 | table.insert(result, first) 125 | end 126 | if second ~= '' then 127 | table.insert(result, second) 128 | end 129 | end 130 | i = i + 1 131 | end 132 | else 133 | local i = 1 134 | for str in string.gmatch(line, '([^' .. sep .. ']+)') do 135 | if translate then 136 | str = translate(str, i) 137 | end 138 | table.insert(result, str) 139 | i = i + 1 140 | end 141 | end 142 | table.insert(result, '\n') 143 | end 144 | table.remove(result) 145 | 146 | return result 147 | end 148 | 149 | local function escape(txt, defaultColor, tokenize, pattern, translate) 150 | local fill = function (result, txt, col) 151 | if tokenize then 152 | local chars = split(txt, nil, pattern, translate) 153 | for i, c in ipairs(chars) do 154 | local code = utf8.codepoint(c) 155 | if code <= 255 then 156 | if c ~= '\n' then 157 | c = c .. ' ' 158 | end 159 | else 160 | local nextCode = i < #chars and utf8.codepoint(chars[i + 1]) or 256 161 | if nextCode <= 255 then 162 | if c ~= '\n' then 163 | c = c .. ' ' 164 | end 165 | end 166 | end 167 | table.insert( 168 | result, 169 | { 170 | text = c, 171 | color = col 172 | } 173 | ) 174 | end 175 | else 176 | table.insert( 177 | result, 178 | { 179 | text = txt, 180 | color = col 181 | } 182 | ) 183 | end 184 | end 185 | 186 | local result = { } 187 | while #txt > 0 do 188 | local i = string.find(txt, '%[col=') 189 | if i == nil then 190 | fill(result, txt, defaultColor) 191 | txt = '' 192 | else 193 | local head = string.sub(txt, 1, i - 1) 194 | fill(result, head, defaultColor) 195 | txt = string.sub(txt, i + 5) 196 | local j = string.find(txt, '%]') 197 | if j == nil then 198 | error('Invalid escape.') 199 | end 200 | local col = string.sub(txt, 1, j - 1) 201 | local rgba = tonumber(col) 202 | rgba = ((rgba >> 24) & 0xff) | ((rgba << 8) & 0xff0000) | ((rgba >> 8) & 0xff00) | ((rgba << 24) & 0xff000000) 203 | col = Color.new() 204 | col:fromRGBA(rgba) 205 | txt = string.sub(txt, j + 1) 206 | local k = string.find(txt, '%[/col%]') 207 | if k == nil then 208 | error('Invalid escape.') 209 | end 210 | local text = string.sub(txt, 1, k - 1) 211 | fill(result, text, col) 212 | txt = string.sub(txt, k + 6) 213 | end 214 | end 215 | 216 | return result 217 | end 218 | 219 | --[[ List. ]] 220 | 221 | local function car(lst) 222 | if not lst or #lst == 0 then 223 | return nil 224 | end 225 | 226 | return lst[1] 227 | end 228 | 229 | local function cdr(lst) 230 | if not lst or #lst == 0 then 231 | return { } 232 | end 233 | lst = table.pack(table.unpack(lst)) 234 | table.remove(lst, 1) 235 | 236 | return lst 237 | end 238 | 239 | local function exists(lst, elem) 240 | if not lst then 241 | return false 242 | end 243 | 244 | for _, v in pairs(lst) do 245 | if v == elem then 246 | return true 247 | end 248 | end 249 | 250 | return false 251 | end 252 | 253 | local function filter(lst, pred) 254 | if not lst then 255 | return nil 256 | end 257 | 258 | local result = { } 259 | for _, v in ipairs(lst) do 260 | if pred and pred(v) then 261 | table.insert(result, v) 262 | elseif not pred and not v then 263 | return { } 264 | end 265 | end 266 | 267 | return result 268 | end 269 | 270 | --[[ Dictionary. ]] 271 | 272 | local function merge(first, second) 273 | if first == nil and second == nil then 274 | return nil 275 | end 276 | local result = { } 277 | if first then 278 | for k, v in pairs(first) do 279 | result[k] = v 280 | end 281 | end 282 | if second then 283 | for k, v in pairs(second) do 284 | result[k] = v 285 | end 286 | end 287 | 288 | return result 289 | end 290 | 291 | --[[ Text drawing. ]] 292 | 293 | local function tex3Grid(elem, x, y, w, h, permeation, alpha, color_) 294 | local img = elem.resource 295 | local area = elem.area 296 | local srcx, srcy = 0, 0 297 | local srcw, srch = img.width, img.height 298 | if area then 299 | srcx, srcy = area[1], area[2] 300 | srcw, srch = area[3], area[4] 301 | end 302 | local col = nil 303 | if color_ or alpha then 304 | if color_ then 305 | if alpha then 306 | col = Color.new(color_.r, color_.g, color_.b, color_.a * (alpha / 255)) 307 | else 308 | col = color_ 309 | end 310 | else 311 | if alpha then 312 | col = Color.new(255, 255, 255, alpha) 313 | end 314 | end 315 | end 316 | if srcw == w and srch == h then 317 | if area then 318 | if col then 319 | tex(img, x, y, w, h, srcx, srcy, srcw, srch, 0, Vec2.new(0.5, 0.5), false, false, col) 320 | else 321 | tex(img, x, y, w, h, srcx, srcy, srcw, srch) 322 | end 323 | else 324 | if col then 325 | tex(img, x, y, w, h, x, y, w, h, 0, Vec2.new(0.5, 0.5), false, false, col) 326 | else 327 | tex(img, x, y, w, h) 328 | end 329 | end 330 | else 331 | if col then 332 | permeation = 0 333 | elseif not permeation then 334 | permeation = 1 335 | end 336 | x, y, w, h = math.floor(x), math.floor(y), math.floor(w), math.floor(h) 337 | local w_1_3 = math.floor(srcw * 1 / 3) 338 | local srcx1, srcx2, srcx3, srcx4 = srcx, srcx + w_1_3, srcx + srcw - w_1_3 - 1, srcx + srcw - 1 339 | local srcwBorder, srcwMiddle = srcx2 - srcx1 + 1, srcx3 - srcx2 + 1 340 | local dstx1, dstx2, dstx3, dstx4 = x, x + srcwBorder, x + w - srcwBorder, x + w - 1 341 | if col then 342 | tex(img, dstx2 - permeation, y, dstx3 - dstx2 + permeation * 2, h, srcx2, srcy, srcwMiddle, srch, 0, Vec2.new(0.5, 0.5), false, false, col) -- Middle. 343 | tex(img, dstx1, y, srcwBorder, h, srcx1, srcy, srcwBorder, srch, 0, Vec2.new(0.5, 0.5), false, false, col) -- Left. 344 | tex(img, dstx3, y, srcwBorder, h, srcx3, srcy, srcwBorder, srch, 0, Vec2.new(0.5, 0.5), false, false, col) -- Right. 345 | else 346 | tex(img, dstx2 - permeation, y, dstx3 - dstx2 + permeation * 2, h, srcx2, srcy, srcwMiddle, srch) -- Middle. 347 | tex(img, dstx1, y, srcwBorder, h, srcx1, srcy, srcwBorder, srch) -- Left. 348 | tex(img, dstx3, y, srcwBorder, h, srcx3, srcy, srcwBorder, srch) -- Right. 349 | end 350 | end 351 | end 352 | 353 | local function tex9Grid(elem, x, y, w, h, permeation, alpha, color_) 354 | local img = elem.resource 355 | local area = elem.area 356 | local srcx, srcy = 0, 0 357 | local srcw, srch = img.width, img.height 358 | if area then 359 | srcx, srcy = area[1], area[2] 360 | srcw, srch = area[3], area[4] 361 | end 362 | local col = nil 363 | if color_ or alpha then 364 | if color_ then 365 | if alpha then 366 | col = Color.new(color_.r, color_.g, color_.b, color_.a * (alpha / 255)) 367 | else 368 | col = color_ 369 | end 370 | else 371 | if alpha then 372 | col = Color.new(255, 255, 255, alpha) 373 | end 374 | end 375 | end 376 | if srcw == w and srch == h then 377 | if area then 378 | if col then 379 | tex(img, x, y, w, h, srcx, srcy, srcw, srch, 0, Vec2.new(0.5, 0.5), false, false, col) 380 | else 381 | tex(img, x, y, w, h, srcx, srcy, srcw, srch) 382 | end 383 | else 384 | if col then 385 | tex(img, x, y, w, h, x, y, w, h, 0, Vec2.new(0.5, 0.5), false, false, col) 386 | else 387 | tex(img, x, y, w, h) 388 | end 389 | end 390 | else 391 | if col then 392 | permeation = 0 393 | elseif not permeation then 394 | permeation = 1 395 | end 396 | x, y, w, h = math.floor(x), math.floor(y), math.floor(w), math.floor(h) 397 | local w_1_3, h_1_3 = math.floor(srcw * 1 / 3), math.floor(srch * 1 / 3) 398 | local srcx1, srcx2, srcx3, srcx4 = srcx, srcx + w_1_3, srcx + srcw - w_1_3 - 1, srcx + srcw - 1 399 | local srcy1, srcy2, srcy3, srcy4 = srcy, srcy + h_1_3, srcy + srch - h_1_3 - 1, srcy + srch - 1 400 | local srcwBorder, srcwMiddle = srcx2 - srcx1 + 1, srcx3 - srcx2 + 1 401 | local srchBorder, srchMiddle = srcy2 - srcy1 + 1, srcy3 - srcy2 + 1 402 | local dstx1, dstx2, dstx3, dstx4 = x, x + srcwBorder, x + w - srcwBorder, x + w - 1 403 | local dsty1, dsty2, dsty3, dsty4 = y, y + srchBorder, y + h - srchBorder, y + h - 1 404 | if col then 405 | tex(img, dstx2 - permeation, dsty2 - permeation, dstx3 - dstx2 + permeation * 2, dsty3 - dsty2 + permeation * 2, srcx2, srcy2, srcwMiddle, srchMiddle, 0, Vec2.new(0.5, 0.5), false, false, col) -- Center. 406 | tex(img, dstx2 - permeation, dsty1, dstx3 - dstx2 + permeation * 2, srchBorder, srcx2, srcy1, srcwMiddle, srchBorder, 0, Vec2.new(0.5, 0.5), false, false, col) -- Middle top. 407 | tex(img, dstx1, dsty2 - permeation, srcwBorder, dsty3 - dsty2 + permeation * 2, srcx1, srcy2, srcwBorder, srchMiddle, 0, Vec2.new(0.5, 0.5), false, false, col) -- Left middle. 408 | tex(img, dstx3, dsty2 - permeation, srcwBorder, dsty3 - dsty2 + permeation * 2, srcx3, srcy2, srcwBorder, srchMiddle, 0, Vec2.new(0.5, 0.5), false, false, col) -- Right middle. 409 | tex(img, dstx2 - permeation, dsty3, dstx3 - dstx2 + permeation * 2, srchBorder, srcx2, srcy3, srcwMiddle, srchBorder, 0, Vec2.new(0.5, 0.5), false, false, col) -- Middle bottom. 410 | tex(img, dstx1, dsty1, srcwBorder, srchBorder, srcx1, srcy1, srcwBorder, srchBorder, 0, Vec2.new(0.5, 0.5), false, false, col) -- Left top. 411 | tex(img, dstx3, dsty1, srcwBorder, srchBorder, srcx3, srcy1, srcwBorder, srchBorder, 0, Vec2.new(0.5, 0.5), false, false, col) -- Right top. 412 | tex(img, dstx1, dsty3, srcwBorder, srchBorder, srcx1, srcy3, srcwBorder, srchBorder, 0, Vec2.new(0.5, 0.5), false, false, col) -- Left bottom. 413 | tex(img, dstx3, dsty3, srcwBorder, srchBorder, srcx3, srcy3, srcwBorder, srchBorder, 0, Vec2.new(0.5, 0.5), false, false, col) -- Right bottom. 414 | else 415 | tex(img, dstx2 - permeation, dsty2 - permeation, dstx3 - dstx2 + permeation * 2, dsty3 - dsty2 + permeation * 2, srcx2, srcy2, srcwMiddle, srchMiddle) -- Center. 416 | tex(img, dstx2 - permeation, dsty1, dstx3 - dstx2 + permeation * 2, srchBorder, srcx2, srcy1, srcwMiddle, srchBorder) -- Middle top. 417 | tex(img, dstx1, dsty2 - permeation, srcwBorder, dsty3 - dsty2 + permeation * 2, srcx1, srcy2, srcwBorder, srchMiddle) -- Left middle. 418 | tex(img, dstx3, dsty2 - permeation, srcwBorder, dsty3 - dsty2 + permeation * 2, srcx3, srcy2, srcwBorder, srchMiddle) -- Right middle. 419 | tex(img, dstx2 - permeation, dsty3, dstx3 - dstx2 + permeation * 2, srchBorder, srcx2, srcy3, srcwMiddle, srchBorder) -- Middle bottom. 420 | tex(img, dstx1, dsty1, srcwBorder, srchBorder, srcx1, srcy1, srcwBorder, srchBorder) -- Left top. 421 | tex(img, dstx3, dsty1, srcwBorder, srchBorder, srcx3, srcy1, srcwBorder, srchBorder) -- Right top. 422 | tex(img, dstx1, dsty3, srcwBorder, srchBorder, srcx1, srcy3, srcwBorder, srchBorder) -- Left bottom. 423 | tex(img, dstx3, dsty3, srcwBorder, srchBorder, srcx3, srcy3, srcwBorder, srchBorder) -- Right bottom. 424 | end 425 | end 426 | end 427 | 428 | local function textLeftSameLine(txt, font_, x, y, w, h, offset_, alpha) 429 | local dx = offset_ and offset_[1] or 0 430 | local dy = offset_ and offset_[2] or 0 431 | local margin, scale = font_.margin or 1, font_.scale or 1 432 | local textWidth, textHeight = measure(txt, font_.resource, margin, scale) 433 | local fx, fy = x + dx, y + dy 434 | local col = alpha and Color.new(font_.color.r, font_.color.g, font_.color.b, alpha) or font_.color 435 | text(txt, fx, fy, col, margin, scale) 436 | 437 | return fx, fy, textWidth, textHeight 438 | end 439 | 440 | local function textLeft(txt, font_, x, y, w, h, offset_, alpha) 441 | local dx = offset_ and offset_[1] or 0 442 | local dy = offset_ and offset_[2] or 0 443 | local margin, scale = font_.margin or 1, font_.scale or 1 444 | local textWidth, textHeight = measure(txt, font_.resource, margin, scale) 445 | local fx, fy = x + dx, y + (h - textHeight) * 0.5 + dy 446 | local col = alpha and Color.new(font_.color.r, font_.color.g, font_.color.b, alpha) or font_.color 447 | text(txt, fx, fy, col, margin, scale) 448 | 449 | return fx, fy, textWidth, textHeight 450 | end 451 | 452 | local function textCenter(txt, font_, x, y, w, h, offset_, alpha) 453 | local dx = offset_ and offset_[1] or 0 454 | local dy = offset_ and offset_[2] or 0 455 | local margin, scale = font_.margin or 1, font_.scale or 1 456 | local textWidth, textHeight = measure(txt, font_.resource, margin, scale) 457 | local fx, fy = x + (w - textWidth) * 0.5 + dx, y + (h - textHeight) * 0.5 + dy 458 | local col = alpha and Color.new(font_.color.r, font_.color.g, font_.color.b, alpha) or font_.color 459 | text(txt, fx, fy, col, margin, scale) 460 | 461 | return fx, fy, textWidth, textHeight 462 | end 463 | 464 | local function textRight(txt, font_, x, y, w, h, offset_, alpha) 465 | local dx = offset_ and offset_[1] or 0 466 | local dy = offset_ and offset_[2] or 0 467 | local margin, scale = font_.margin or 1, font_.scale or 1 468 | local textWidth, textHeight = measure(txt, font_.resource, margin, scale) 469 | local fx, fy = x + (w - textWidth) + dx, y + (h - textHeight) * 0.5 + dy 470 | local col = alpha and Color.new(font_.color.r, font_.color.g, font_.color.b, alpha) or font_.color 471 | text(txt, fx, fy, col, margin, scale) 472 | 473 | return fx, fy, textWidth, textHeight 474 | end 475 | 476 | --[[ 477 | Exporting. 478 | ]] 479 | 480 | return { 481 | KeyCodeLShift = KeyCodeLShift, 482 | KeyCodeRShift = KeyCodeRShift, 483 | NaN = NaN, 484 | isNaN = isNaN, 485 | round = round, 486 | clamp = clamp, 487 | intersected = intersected, 488 | startsWith = startsWith, 489 | endsWith = endsWith, 490 | split = split, 491 | escape = escape, 492 | car = car, 493 | cdr = cdr, 494 | exists = exists, 495 | filter = filter, 496 | merge = merge, 497 | tex3Grid = tex3Grid, 498 | tex9Grid = tex9Grid, 499 | textLeftSameLine = textLeftSameLine, 500 | textLeft = textLeft, 501 | textCenter = textCenter, 502 | textRight = textRight 503 | } 504 | -------------------------------------------------------------------------------- /src/main.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | The MIT License 3 | 4 | Copyright (C) 2021 - 2022 Tony Wang 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | ]] 23 | 24 | require 'libs/beGUI/beGUI' 25 | require 'libs/beGUI/beTheme' 26 | require 'keycode' 27 | 28 | local DEBUG = false 29 | 30 | local widgets = nil 31 | local theme = nil 32 | 33 | function setup() 34 | print('beGUI v' .. beGUI.version) 35 | 36 | Canvas.main:resize(0, 320) 37 | 38 | local P = beGUI.percent -- Alias of percent. 39 | local left = beGUI.Widget.new() 40 | :put(P(27), 15) -- X: 27%, Y: 15. 41 | :resize(P(45), 290) -- W: 45%, H: 290. 42 | :addChild( 43 | beGUI.Button.new('Button') 44 | :setId('button') 45 | :anchor(0, 0) -- X: left, Y: top. 46 | :put(P(-50), 0) -- X: -50%, Y: 0. 47 | :resize(P(48), 23) -- W: 48%, H: 23. 48 | :on('clicked', function (sender) 49 | local lbl = widgets:get(1, 'label') 50 | lbl:setValue('Clicked ' .. tostring(sender)) 51 | end) 52 | ) 53 | :addChild( 54 | beGUI.Button.new('Popup') 55 | :setId('button') 56 | :anchor(1, 0) -- X: right, Y: top. 57 | :put(P(50), 0) -- X: 50%, Y: 0. 58 | :resize(P(48), 23) -- W: 48%, H: 23. 59 | :on('clicked', function (sender) 60 | widgets:openPopup( -- Open popup. 61 | beGUI.QuestionBox.new(true, 'Message', 'Hi there!') 62 | :on('canceled', function (sender) 63 | widgets:closePopup() 64 | 65 | local lbl = widgets:get(1, 'label') 66 | lbl:setValue('Popup canceled') 67 | end) 68 | :on('confirmed', function (sender) 69 | widgets:closePopup() 70 | 71 | local lbl = widgets:get(1, 'label') 72 | lbl:setValue('Popup confirmed') 73 | end) 74 | :on('denied', function (sender) 75 | widgets:closePopup() 76 | 77 | local lbl = widgets:get(1, 'label') 78 | lbl:setValue('Popup denied') 79 | end) 80 | ) 81 | 82 | local lbl = widgets:get(1, 'label') 83 | lbl:setValue('Popup opened') 84 | end) 85 | ) 86 | :addChild( 87 | beGUI.Label.new('beGUI demo', 'left', true) 88 | :setId('label') 89 | :anchor(0.5, 0) 90 | :put(0, 20) 91 | :resize(P(100), 23) 92 | ) 93 | :addChild( 94 | beGUI.CheckBox.new('CheckBox') 95 | :setId('checkbox') 96 | :anchor(0.5, 0) 97 | :put(0, 40) 98 | :resize(P(100), 23) 99 | :on('changed', function (sender, value) 100 | local lbl = widgets:get(1, 'label') 101 | lbl:setValue('Changed ' .. tostring(sender) .. ', ' .. tostring(value)) 102 | end) 103 | ) 104 | :addChild( 105 | beGUI.RadioBox.new('RadioBox 1', true) 106 | :setId('radiobox1') 107 | :anchor(0.5, 0) 108 | :put(0, 60) 109 | :resize(P(100), 23) 110 | :on('changed', function (sender, value) 111 | if value then 112 | local lbl = widgets:get(1, 'label') 113 | lbl:setValue('Changed ' .. tostring(sender) .. '1, ' .. tostring(value)) 114 | 115 | local multilinelabel = widgets:find('multilinelabel') 116 | multilinelabel:setAlignment('left') 117 | end 118 | end) 119 | ) 120 | :addChild( 121 | beGUI.RadioBox.new('RadioBox 2') 122 | :setId('radiobox2') 123 | :anchor(0.5, 0) 124 | :put(0, 80) 125 | :resize(P(100), 23) 126 | :on('changed', function (sender, value) 127 | if value then 128 | local lbl = widgets:get(1, 'label') 129 | lbl:setValue('Changed ' .. tostring(sender) .. '2, ' .. tostring(value)) 130 | 131 | local multilinelabel = widgets:find('multilinelabel') 132 | multilinelabel:setAlignment('center') 133 | end 134 | end) 135 | ) 136 | :addChild( 137 | beGUI.RadioBox.new('RadioBox 3') 138 | :setId('radiobox3') 139 | :anchor(0.5, 0) 140 | :put(0, 100) 141 | :resize(P(100), 23) 142 | :on('changed', function (sender, value) 143 | if value then 144 | local lbl = widgets:get(1, 'label') 145 | lbl:setValue('Changed ' .. tostring(sender) .. '3, ' .. tostring(value)) 146 | 147 | local multilinelabel = widgets:find('multilinelabel') 148 | multilinelabel:setAlignment('right') 149 | end 150 | end) 151 | ) 152 | :addChild( 153 | beGUI.ComboBox.new({ 'Item 1', 'Item 2', 'Item 3', 'More Items...' }) 154 | :setId('combobox') 155 | :anchor(0.5, 0) 156 | :put(0, 120) 157 | :resize(P(100), 23) 158 | :on('changed', function (sender, value) 159 | local lbl = widgets:get(1, 'label') 160 | lbl:setValue('Changed ' .. tostring(sender) .. ', ' .. tostring(value)) 161 | end) 162 | ) 163 | :addChild( 164 | beGUI.List.new(true) 165 | :setId('list') 166 | :anchor(0.5, 0) 167 | :put(0, 150) 168 | :resize(P(100), 69) 169 | :addChild( -- List items are added as children. 170 | beGUI.Button.new('Increase') 171 | :setId('button') 172 | :put(0, 0) 173 | :resize(P(100), 23) 174 | :on('clicked', function (_) 175 | local lbl = widgets:get(1, 'list', 'label1') -- Get the following one label. 176 | local num = tonumber(lbl:getValue()) 177 | lbl:setValue(tostring(num + 1)) 178 | end) 179 | ) 180 | :addChild( 181 | beGUI.Label.new('1') 182 | :setId('label1') 183 | :put(P(1), 20) 184 | :resize(P(98), 23) 185 | ) 186 | :addChild( 187 | beGUI.Label.new('Hold and slide...') 188 | :setId('label2') 189 | :put(P(1), 40) 190 | :resize(P(98), 23) 191 | ) 192 | :addChild( 193 | beGUI.Label.new('Label 3') 194 | :setId('label3') 195 | :put(P(1), 60) 196 | :resize(P(98), 23) 197 | ) 198 | :addChild( 199 | beGUI.Label.new('Item in a List') 200 | :setId('label4') 201 | :put(P(1), 80) 202 | :resize(P(98), 23) 203 | ) 204 | ) 205 | :addChild( 206 | beGUI.Group.new('Group') 207 | :setId('group') 208 | :anchor(0, 0) 209 | :put(P(-50), 226) 210 | :resize(P(100), 60) 211 | :addChild( 212 | beGUI.Picture.new({ resource = Resources.load('disk.png') }) 213 | :setId('picture') 214 | :anchor(0, 0) 215 | :put(P(1), 10) 216 | :resize(48, 48) 217 | ) 218 | :addChild( 219 | beGUI.NumberBox.new(10, 1, 0, 100) 220 | :setId('numberbox1') 221 | :anchor(1, 0) 222 | :put(P(99), 10) 223 | :resize(P(50), 23) 224 | :on('changed', function (sender, value) 225 | local lbl = widgets:get(1, 'label') 226 | lbl:setValue('Changed ' .. tostring(sender) .. ', ' .. tostring(value)) 227 | end) 228 | ) 229 | :addChild( 230 | beGUI.NumberBox.new(5, 0.1, 0, 10, function (val) 231 | return math.floor(val * 10 + 0.5) / 10 232 | end) 233 | :setId('numberbox2') 234 | :anchor(1, 1) 235 | :put(P(99), 55) 236 | :resize(P(50), 23) 237 | :on('changed', function (sender, value) 238 | local lbl = widgets:get(1, 'label') 239 | lbl:setValue('Changed ' .. tostring(sender) .. ', ' .. tostring(value)) 240 | end) 241 | ) 242 | ) 243 | local right = beGUI.Widget.new() 244 | :put(P(73), 15) -- X: 73%, Y: 15. 245 | :resize(P(45), 290) -- W: 45%, H: 290. 246 | :addChild( 247 | beGUI.InputBox.new('', 'Input something...') 248 | :setId('inputbox') 249 | :anchor(0.5, 0) -- X: middle, Y: top. 250 | :put(0, 0) -- X: 0, Y: 0. 251 | :resize(P(100), 23) -- W: 100%, H: 23. 252 | :on('changed', function (sender, value) 253 | local lbl = widgets:get(2, 'label') 254 | lbl:setValue('Changed ' .. tostring(sender) .. ', ' .. tostring(value)) 255 | end) 256 | ) 257 | :addChild( 258 | beGUI.Label.new('Click above') 259 | :setId('label') 260 | :anchor(0.5, 0) 261 | :put(0, 20) 262 | :resize(P(100), 23) 263 | ) 264 | :addChild( 265 | beGUI.Droppable.new() 266 | :setId('droppable1') 267 | :anchor(0, 0) 268 | :put(P(-50), 40) 269 | :resize(P(40), 17) 270 | :addChild( 271 | beGUI.Custom.new() 272 | :setId('custom') 273 | :anchor(0, 0) 274 | :put(0, 0) 275 | :resize(P(100), P(100)) 276 | :on('updated', function (sender, x, y, w, h, delta, event) 277 | rect(x, y, x + w - 1, y + h - 1, false, sender.color or Color.new(0, 0, 0)) 278 | end) 279 | ) 280 | :on('entered', function (sender, draggable) 281 | local custom = widgets:get(2, 'droppable1', 'custom') 282 | custom.color = Color.new(0, 255, 0) 283 | 284 | local lbl = widgets:get(2, 'label') 285 | lbl:setValue('Entered ' .. sender.id .. ', ' .. draggable.id) 286 | end) 287 | :on('left', function (sender, draggable) 288 | local custom = widgets:get(2, 'droppable1', 'custom') 289 | custom.color = nil 290 | 291 | local lbl = widgets:get(2, 'label') 292 | lbl:setValue('Left ' .. sender.id .. ', ' .. draggable.id) 293 | end) 294 | :on('dropping', function (sender, draggable) 295 | return true 296 | end) 297 | :on('dropped', function (sender, draggable) 298 | local custom = widgets:get(2, 'droppable1', 'custom') 299 | custom.color = nil 300 | 301 | local lbl = widgets:get(2, 'label') 302 | lbl:setValue('Dropped ' .. sender.id .. ', ' .. draggable.id) 303 | end) 304 | :addChild( 305 | beGUI.Draggable.new() 306 | :setId('draggable1') 307 | :anchor(0, 0) 308 | :put(0, 0) 309 | :resize(P(100), P(100)) 310 | :addChild( 311 | beGUI.Button.new('Drag me') 312 | :setId('button_draggable') 313 | :anchor(0, 0) 314 | :put(P(1), P(6)) 315 | :resize(P(99), P(94)) 316 | :on('clicked', function (sender) 317 | local lbl = widgets:get(2, 'label') 318 | lbl:setValue('Clicked ' .. sender.id) 319 | end) 320 | ) 321 | ) 322 | ) 323 | :addChild( 324 | beGUI.Droppable.new() 325 | :setId('droppable2') 326 | :anchor(1, 0) 327 | :put(P(50), 40) 328 | :resize(P(40), 17) 329 | :addChild( 330 | beGUI.Custom.new() 331 | :setId('custom') 332 | :anchor(0, 0) 333 | :put(0, 0) 334 | :resize(P(100), P(100)) 335 | :on('updated', function (sender, x, y, w, h, delta, event) 336 | rect(x, y, x + w - 1, y + h - 1, false, sender.color or Color.new(0, 0, 0)) 337 | end) 338 | ) 339 | :on('entered', function (sender, draggable) 340 | local custom = widgets:get(2, 'droppable2', 'custom') 341 | custom.color = Color.new(0, 255, 0) 342 | 343 | local lbl = widgets:get(2, 'label') 344 | lbl:setValue('Entered ' .. sender.id .. ', ' .. draggable.id) 345 | end) 346 | :on('left', function (sender, draggable) 347 | local custom = widgets:get(2, 'droppable2', 'custom') 348 | custom.color = nil 349 | 350 | local lbl = widgets:get(2, 'label') 351 | lbl:setValue('Left ' .. sender.id .. ', ' .. draggable.id) 352 | end) 353 | :on('dropping', function (sender, draggable) 354 | return true 355 | end) 356 | :on('dropped', function (sender, draggable) 357 | local custom = widgets:get(2, 'droppable2', 'custom') 358 | custom.color = nil 359 | 360 | local lbl = widgets:get(2, 'label') 361 | lbl:setValue('Dropped ' .. sender.id .. ', ' .. draggable.id) 362 | end) 363 | ) 364 | :addChild( 365 | beGUI.Droppable.new() 366 | :setId('droppable3') 367 | :anchor(0, 0) 368 | :put(P(-50), 63) 369 | :resize(P(40), 17) 370 | :addChild( 371 | beGUI.Custom.new() 372 | :setId('custom') 373 | :anchor(0, 0) 374 | :put(0, 0) 375 | :resize(P(100), P(100)) 376 | :on('updated', function (sender, x, y, w, h, delta, event) 377 | rect(x, y, x + w - 1, y + h - 1, false, sender.color or Color.new(0, 0, 0)) 378 | end) 379 | ) 380 | :on('entered', function (sender, draggable) 381 | local custom = widgets:get(2, 'droppable3', 'custom') 382 | custom.color = Color.new(0, 255, 0) 383 | 384 | local lbl = widgets:get(2, 'label') 385 | lbl:setValue('Entered ' .. sender.id .. ', ' .. draggable.id) 386 | end) 387 | :on('left', function (sender, draggable) 388 | local custom = widgets:get(2, 'droppable3', 'custom') 389 | custom.color = nil 390 | 391 | local lbl = widgets:get(2, 'label') 392 | lbl:setValue('Left ' .. sender.id .. ', ' .. draggable.id) 393 | end) 394 | :on('dropping', function (sender, draggable) 395 | return true 396 | end) 397 | :on('dropped', function (sender, draggable) 398 | local custom = widgets:get(2, 'droppable3', 'custom') 399 | custom.color = nil 400 | 401 | local lbl = widgets:get(2, 'label') 402 | lbl:setValue('Dropped ' .. sender.id .. ', ' .. draggable.id) 403 | end) 404 | :addChild( 405 | beGUI.Draggable.new() 406 | :setId('draggable2') 407 | :anchor(0, 0) 408 | :put(0, 0) 409 | :resize(P(100), P(100)) 410 | :addChild( 411 | beGUI.Label.new('Drag me 2') 412 | :setId('label') 413 | :anchor(0, 0) 414 | :put(2, 0) 415 | :resize(P(100), P(100)) 416 | ) 417 | ) 418 | ) 419 | :addChild( 420 | beGUI.Droppable.new() 421 | :setId('droppable4') 422 | :anchor(1, 0) 423 | :put(P(50), 63) 424 | :resize(P(40), 17) 425 | :addChild( 426 | beGUI.Custom.new() 427 | :setId('custom') 428 | :anchor(0, 0) 429 | :put(0, 0) 430 | :resize(P(100), P(100)) 431 | :on('updated', function (sender, x, y, w, h, delta, event) 432 | rect(x, y, x + w - 1, y + h - 1, false, sender.color or Color.new(0, 0, 0)) 433 | end) 434 | ) 435 | :on('entered', function (sender, draggable) 436 | local custom = widgets:get(2, 'droppable4', 'custom') 437 | if draggable.id == 'draggable2' then 438 | custom.color = Color.new(0, 255, 0) 439 | else 440 | custom.color = Color.new(255, 0, 0) 441 | end 442 | 443 | local lbl = widgets:get(2, 'label') 444 | lbl:setValue('Entered ' .. sender.id .. ', ' .. draggable.id) 445 | end) 446 | :on('left', function (sender, draggable) 447 | local custom = widgets:get(2, 'droppable4', 'custom') 448 | custom.color = nil 449 | 450 | local lbl = widgets:get(2, 'label') 451 | lbl:setValue('Left ' .. sender.id .. ', ' .. draggable.id) 452 | end) 453 | :on('dropping', function (sender, draggable) 454 | return draggable.id == 'draggable2' 455 | end) 456 | :on('dropped', function (sender, draggable) 457 | local custom = widgets:get(2, 'droppable4', 'custom') 458 | custom.color = nil 459 | 460 | local lbl = widgets:get(2, 'label') 461 | lbl:setValue('Dropped ' .. sender.id .. ', ' .. draggable.id) 462 | end) 463 | ) 464 | :addChild( 465 | beGUI.List.new(true) 466 | :setId('list') 467 | :anchor(0.5, 0) 468 | :put(0, 90) 469 | :resize(P(100), 60) 470 | :addChild( 471 | beGUI.MultilineLabel.new('The Quick Brown Fox Jumps Over the Lazy Dog.\n[col=0xff0000ff]ABCDEFG[/col] [col=0x00ff00ff]HIJKLMN[/col] [col=0x0000ffff]OPQRST[/col] UVWXYZ abcdefg hijklmn opqrst uvwxyz 1234567890 ! @ # $ % ^ & * ( ) ` - = [ ] \\ ; \' , . / ~ _ + { } | : " < > ?\n') 472 | :setId('multilinelabel') 473 | :put(P(1), 2) 474 | :resize(P(98), 0) -- Height is automatically calculated. 475 | ) 476 | ) 477 | :addChild( 478 | beGUI.ProgressBar.new(0, Color.new(0, 0, 128)) 479 | :setId('progressbar') 480 | :anchor(0.5, 0) 481 | :put(0, 160) 482 | :resize(P(100), 17) 483 | :addChild( 484 | beGUI.Label.new('3/5', 'center', false, 'font_white') 485 | :setId('label_numbers') 486 | :anchor(0.5, 1) 487 | :put(P(50), P(100)) 488 | :resize(P(100), P(100)) 489 | ) 490 | :on('changed', function (sender, value, maxValue, shadowValue) 491 | local lblNums = sender:find('label_numbers') 492 | lblNums:setValue(tostring(value) .. '/' .. tostring(maxValue)) 493 | end) 494 | :setMaxValue(5) 495 | :setValue(3) 496 | ) 497 | :addChild( 498 | beGUI.Slide.new(3, 0, 5) 499 | :setId('slide') 500 | :anchor(0.5, 0) 501 | :put(0, 184) 502 | :resize(P(100), 17) 503 | :on('changed', function (sender, value) 504 | local prog = widgets:get(2, 'progressbar') 505 | prog:setValue(value) 506 | end) 507 | ) 508 | :addChild( 509 | beGUI.Url.new('Bitty Engine | beGUI') 510 | :setId('url') 511 | :anchor(0.5, 0) 512 | :put(0, 210) 513 | :resize(-1, 12) 514 | :on('clicked', function (sender) 515 | Platform.surf('https://github.com/paladin-t/begui') 516 | end) 517 | ) 518 | :addChild( 519 | beGUI.Tab.new() 520 | :setId('tab') 521 | :anchor(0.5, 0) 522 | :put(0, 230) 523 | :resize(P(100), 57) 524 | :add('Tab 1') 525 | :add('Tab 2') 526 | :add('Tab 3') 527 | :setValue(1) 528 | :addChild( 529 | beGUI.Button.new('Button') 530 | :setId('button') 531 | :anchor(1, 0) 532 | :put(P(98), 23) 533 | :resize(60, 23) 534 | :on('clicked', function (sender) 535 | local lbl = sender.parent:get('label') 536 | lbl:setValue('Clicked ' .. tostring(sender)) 537 | end) 538 | ) 539 | :addChild( 540 | beGUI.Label.new('Hello world!', 'left', true) 541 | :setId('label') 542 | :anchor(0, 0) 543 | :put(4, 23) 544 | :resize(P(100), 23) 545 | ) 546 | :setValue(2) 547 | :addChild( 548 | beGUI.Label.new('Greetings from tab 2.', 'left', true) 549 | :setId('label') 550 | :anchor(0, 0) 551 | :put(4, 23) 552 | :resize(P(100), 23) 553 | ) 554 | :setValue(3) 555 | :addChild( 556 | beGUI.Label.new('Greetings from tab 3.', 'left', true) 557 | :setId('label') 558 | :anchor(0, 0) 559 | :put(4, 23) 560 | :resize(P(100), 23) 561 | ) 562 | :setValue(1) 563 | :on('changed', function (sender, value) 564 | local lbl = widgets:get(2, 'label') 565 | lbl:setValue('Changed ' .. tostring(sender) .. ', ' .. tostring(value)) 566 | end) 567 | ) 568 | widgets = beGUI.Widget.new() 569 | :put(0, 0) 570 | :resize(P(100), P(100)) 571 | :addChild( 572 | left 573 | ) 574 | :addChild( 575 | right 576 | ) 577 | theme = beTheme.default() 578 | end 579 | 580 | function update(delta) 581 | cls(Color.new(255, 255, 255)) 582 | 583 | if keyp(KeyCode.Up) then 584 | widgets:navigate('prev') 585 | elseif keyp(KeyCode.Down) then 586 | widgets:navigate('next') 587 | elseif keyp(KeyCode.Left) then 588 | widgets:navigate('dec') 589 | elseif keyp(KeyCode.Right) then 590 | widgets:navigate('inc') 591 | elseif keyp(KeyCode.Return) then 592 | widgets:navigate('press') 593 | elseif keyp(KeyCode.Esc) then 594 | widgets:navigate('cancel') 595 | end 596 | 597 | font(theme['font'].resource) 598 | widgets:update(theme, delta) 599 | font(nil) 600 | 601 | if DEBUG then 602 | local x, y, lmb = mouse() 603 | circ(x, y, 3, lmb, Color.new(255, 0, 0)) 604 | end 605 | end 606 | -------------------------------------------------------------------------------- /src/libs/beGUI/beGUI_Widget.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | The MIT License 3 | 4 | Copyright (C) 2021 - 2022 Tony Wang 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | ]] 23 | 24 | local beClass = require 'libs/beGUI/beClass' 25 | local beUtils = require 'libs/beGUI/beGUI_Utils' 26 | local beStack = require 'libs/beGUI/beStack' 27 | local beStructures = require 'libs/beGUI/beGUI_Structures' 28 | 29 | --[[ 30 | Widgets. 31 | ]] 32 | 33 | local Widget = nil 34 | Widget = beClass.class({ 35 | DEBUG = false, 36 | 37 | id = nil, -- String. 38 | visibility = true, -- Boolean. 39 | capturability = false, 40 | queriablility = false, 41 | anchorX = 0, anchorY = 0, -- Number, typically [0.0, 1.0]. 42 | x = 0, y = 0, -- Number or Percent. 43 | worldX = 0, worldY = 0, -- Auto calculated. 44 | width = 0, height = 0, -- Number or Percent. 45 | parentWidth = nil, parentHeight = nil, -- Auto calculated. 46 | content = nil, 47 | transparency = nil, 48 | parent = nil, 49 | children = nil, 50 | 51 | popup = nil, 52 | hovering = false, 53 | focused = nil, -- Used to reserve focused Widget before popup. 54 | focusIfHovering = false, 55 | focusTicks = 0, 56 | clippingStack = nil, 57 | context = nil, 58 | tweens = nil, 59 | events = nil, 60 | 61 | ctor = function (self) 62 | -- Do nothing. 63 | end, 64 | 65 | __tostring = function (self) 66 | return 'Widget' 67 | end, 68 | 69 | -- Sets the ID of the Widget; an ID is used to identify a Widget from others for accessing. 70 | -- `id`: ID string 71 | setId = function (self, id) 72 | if id ~= nil and type(id) ~= 'string' then 73 | error('String or nil expected.') 74 | end 75 | 76 | self.id = id 77 | 78 | return self 79 | end, 80 | -- Gets a sub Widget with the specific ID sequence. 81 | -- `...`: ID sequence of the full hierarchy path 82 | get = function (self, ...) 83 | local full = { ... } 84 | if #full == 0 then 85 | return self 86 | end 87 | local car_ = beUtils.car(full) 88 | local cdr_ = beUtils.cdr(full) 89 | local c = self:getChild(car_) 90 | if not c then 91 | return nil 92 | end 93 | 94 | return c:get(table.unpack(cdr_)) 95 | end, 96 | -- Finds the first matched Widget with the specific ID. 97 | -- `id`: ID string at any level of the hierarchy path 98 | find = function (self, id) 99 | local find_ = nil 100 | find_ = function (widget, id) 101 | if widget.id == id then 102 | return widget 103 | end 104 | if widget.children then 105 | for _, c in ipairs(widget.children) do 106 | local ret = find_(c, id) 107 | if ret then 108 | return ret 109 | end 110 | end 111 | end 112 | 113 | return nil 114 | end 115 | 116 | return find_(self, id) 117 | end, 118 | 119 | -- Gets the visibility of the Widget. 120 | visible = function (self) 121 | return self.visibility 122 | end, 123 | -- Sets the visibility of the Widget. 124 | -- `val`: boolean 125 | setVisible = function (self, val) 126 | self.visibility = val 127 | 128 | return self 129 | end, 130 | 131 | -- Gets the capturability of the Widget. 132 | capturable = function (self) 133 | return self.capturability 134 | end, 135 | -- Sets the capturability of the Widget. 136 | -- `val`: `true`, `false` or 'children' 137 | setCapturable = function (self, val) 138 | self.capturability = val 139 | 140 | return self 141 | end, 142 | 143 | -- Sets the anchor of the Widget; anchor is used to calculate the offset when placing Widget. 144 | -- `x`: x position of the anchor in local space as number, typically [0.0, 1.0] for [left, right] 145 | -- `y`: y position of the anchor in local space as number, typically [0.0, 1.0] for [top, bottom] 146 | anchor = function (self, x, y) 147 | self.anchorX = x 148 | self.anchorY = y 149 | 150 | return self 151 | end, 152 | -- Gets the offset of the Widget. 153 | -- returns offset x, y in world space 154 | offset = function (self) 155 | local w, h = self:size() 156 | 157 | return -w * self.anchorX, -h * self.anchorY 158 | end, 159 | -- Sets the position of the Widget. 160 | -- `x`: number for absolute position, or Percent for relative position 161 | -- `y`: number for absolute position, or Percent for relative position 162 | put = function (self, x, y) 163 | self.x = x 164 | self.y = y 165 | 166 | return self 167 | end, 168 | -- Gets the position of the Widget. 169 | -- returns position x, y in local space 170 | position = function (self) 171 | if self.parentWidth == nil --[[ or self.parentHeight == nil ]] then 172 | self:_updateHierarchy() 173 | end 174 | local canvasWidth, canvasHeight = Canvas.main:size() 175 | local x, y = self.x, self.y 176 | if beClass.is(x, beStructures.Percent) then 177 | if self.parent then 178 | x = x * self.parentWidth 179 | else 180 | x = x * canvasWidth 181 | end 182 | end 183 | if beClass.is(y, beStructures.Percent) then 184 | if self.parent then 185 | y = y * self.parentHeight 186 | else 187 | y = y * canvasHeight 188 | end 189 | end 190 | 191 | return x, y 192 | end, 193 | -- Gets the position of the Widget in world space. 194 | -- returns position x, y in world space 195 | worldPosition = function (self) 196 | return self.worldX, self.worldY 197 | end, 198 | -- Sets the size of the Widget. 199 | -- `width`: number for absolute size, or Percent for relative size 200 | -- `height`: number for absolute size, or Percent for relative size 201 | resize = function (self, width, height) 202 | self.width = width 203 | self.height = height 204 | 205 | return self 206 | end, 207 | -- Gets the size of the Widget. 208 | size = function (self) 209 | if self.parentWidth == nil --[[ or self.parentHeight == nil ]] then 210 | self:_updateHierarchy() 211 | end 212 | local w, h = self.width, self.height 213 | if beClass.is(w, beStructures.Percent) then 214 | w = w * self.parentWidth 215 | end 216 | if beClass.is(h, beStructures.Percent) then 217 | h = h * self.parentHeight 218 | end 219 | 220 | return w, h 221 | end, 222 | 223 | -- Gets the alpha value of the Widget. 224 | alpha = function (self) 225 | if not self.transparency then 226 | return 255 227 | end 228 | 229 | return self.transparency 230 | end, 231 | -- Sets the alpha value of the Widget. 232 | -- `val`: number with range of value from 0 to 255, or nil for default (255) 233 | setAlpha = function (self, val) 234 | if val == 255 then 235 | val = nil 236 | end 237 | 238 | self.transparency = val 239 | 240 | if self.children ~= nil then 241 | for _, c in ipairs(self.children) do 242 | c:setAlpha(val) 243 | end 244 | end 245 | 246 | return self 247 | end, 248 | 249 | -- Gets the child with the specific ID or index. 250 | -- `idOrIndex`: ID string, or index number 251 | -- returns the found child or nil 252 | getChild = function (self, idOrIndex) 253 | if self.children == nil then 254 | return nil 255 | end 256 | if not idOrIndex then 257 | return nil 258 | end 259 | if type(idOrIndex) == 'string' then 260 | for _, v in ipairs(self.children) do 261 | if v.id == idOrIndex then 262 | return v 263 | end 264 | end 265 | elseif type(idOrIndex) == 'number' then 266 | if idOrIndex >= 1 and idOrIndex <= #self.children then 267 | return self.children[idOrIndex] 268 | end 269 | end 270 | 271 | return nil 272 | end, 273 | -- Inserts a child before the specific index. 274 | -- `child`: the child Widget to insert 275 | insertChild = function (self, child, index) 276 | if not child then 277 | return self 278 | end 279 | if child.parent then 280 | error('This widget is already a child of another one.') 281 | end 282 | if self.children == nil then 283 | self.children = { } 284 | end 285 | for _, v in ipairs(self.children) do 286 | if v == child then 287 | return self 288 | end 289 | end 290 | table.insert(self.children, index, child) 291 | child.parent = self 292 | 293 | return self 294 | end, 295 | -- Adds a child to the end of the children list. 296 | -- `child`: the child Widget to add 297 | addChild = function (self, child) 298 | return self:insertChild(child, self.children and #self.children + 1 or 1) 299 | end, 300 | -- Removes a child with the specific child, or its ID or index. 301 | -- `child`: child Widget, or its ID string, or index number 302 | removeChild = function (self, childOrIdOrIndex) 303 | if self.children == nil then 304 | return self 305 | end 306 | if not childOrIdOrIndex then 307 | return self 308 | end 309 | if type(childOrIdOrIndex) == 'string' then 310 | for i, v in ipairs(self.children) do 311 | if v.id == childOrIdOrIndex then 312 | local c = self.children[i] 313 | table.remove(self.children, i) 314 | c.parent = nil 315 | 316 | return self 317 | end 318 | end 319 | error('Child doesn\'t exist.') 320 | elseif type(childOrIdOrIndex) == 'number' then 321 | if childOrIdOrIndex >= 1 and childOrIdOrIndex <= #self.children then 322 | local c = self.children[childOrIdOrIndex] 323 | table.remove(self.children, childOrIdOrIndex) 324 | c.parent = nil 325 | else 326 | error('Index out of bounds.') 327 | end 328 | else 329 | if not childOrIdOrIndex.parent then 330 | error('This widget is not a child of any other one.') 331 | end 332 | for i, v in ipairs(self.children) do 333 | if v == childOrIdOrIndex then 334 | local c = self.children[i] 335 | table.remove(self.children, i) 336 | c.parent = nil 337 | 338 | return self 339 | end 340 | end 341 | end 342 | 343 | return self 344 | end, 345 | -- Iterates all children, and calls the specific handler. 346 | -- `handler`: the children handler in form of `function (child, index) end` 347 | foreachChild = function (self, handler) 348 | if self.children == nil then 349 | return self 350 | end 351 | for i, c in ipairs(self.children) do 352 | handler(c, i) 353 | end 354 | 355 | return self 356 | end, 357 | -- Sorts all children with the specific comparer. 358 | -- `comp`: the comparer in form of `function (left, right) end` 359 | sortChildren = function (self, comp) 360 | table.sort(self.children, comp) 361 | 362 | return self 363 | end, 364 | -- Gets the count of all children. 365 | getChildrenCount = function (self) 366 | if self.children == nil then 367 | return 0 368 | end 369 | 370 | return #self.children 371 | end, 372 | -- Clears all children. 373 | clearChildren = function (self) 374 | if self.children == nil then 375 | return self 376 | end 377 | for i, c in ipairs(self.children) do 378 | c.parent = nil 379 | end 380 | self.children = nil 381 | 382 | return self 383 | end, 384 | 385 | -- Opens a popup. 386 | -- `content`: the popup to open 387 | openPopup = function (self, content) 388 | if self.popup then 389 | return self 390 | end 391 | if not content then 392 | error('Widget expected.') 393 | end 394 | 395 | local focused = self.context.navigated ~= nil 396 | self.focused = self.context.focus 397 | self.context.focus = nil 398 | 399 | self.popup = content 400 | self:addChild(self.popup) 401 | 402 | local w, h = self:size() 403 | self.popup:_updateLayout(w, h) 404 | 405 | if focused then 406 | self.popup:schedule(function () 407 | if self.popup.context == nil then 408 | self.popup.context = self.context 409 | end 410 | self.popup:navigate('next') 411 | end) 412 | end 413 | 414 | return self 415 | end, 416 | -- Closes any popup. 417 | closePopup = function (self) 418 | if not self.popup then 419 | return self 420 | end 421 | self:removeChild(self.popup) 422 | self.popup = nil 423 | 424 | self.context.focus = self.focused 425 | self.focused = nil 426 | 427 | return self 428 | end, 429 | 430 | -- Updates the Widget and its children recursively. 431 | -- `theme`: the theme to draw with 432 | -- `delta`: elapsed time since previous update 433 | update = function (self, theme, delta, event) 434 | return self:_update(theme, delta, 0, 0, event) 435 | end, 436 | 437 | -- Registers the handler of the specific event. 438 | -- `event`: event name string 439 | -- `handler`: callback function 440 | on = function (self, event, handler) 441 | if not event then 442 | return self 443 | end 444 | if self.events == nil then 445 | self.events = { } 446 | end 447 | if self.events[event] == nil then 448 | self.events[event] = { } 449 | end 450 | if beUtils.exists(self.events[event], handler) then 451 | error('Event handler already exists.') 452 | end 453 | table.insert(self.events[event], handler) 454 | 455 | return self 456 | end, 457 | -- Unregisters the handlers of the specific event. 458 | -- `event`: event name string 459 | off = function (self, event) 460 | if not event then 461 | return self 462 | end 463 | if self.events == nil then 464 | return self 465 | end 466 | if self.events[event] == nil then 467 | return self 468 | end 469 | self.events[event] = nil 470 | if not next(self.events) then 471 | self.events = nil 472 | end 473 | 474 | return self 475 | end, 476 | 477 | -- Gets whether this Widget is navigatable. 478 | -- returns 'all' for fully navigatable, 479 | -- `nil` for non-navigatable, 480 | -- 'children' for children only, 481 | -- 'content' for content only 482 | navigatable = function (self) 483 | return 'children' 484 | end, 485 | -- Navigates through widgets, call this to perform key navigation, etc. 486 | -- `dir`: can be one in 'prev', 'next', 'press', 'cancel' 487 | navigate = function (self, dir) 488 | if dir ~= 'prev' and dir ~= 'next' and dir ~= 'dec' and dir ~= 'inc' and dir ~= 'press' and dir ~= 'cancel' then 489 | error('Unknown navigation: ' .. dir .. '.') 490 | end 491 | 492 | if self.context then 493 | self.context.navigated = dir 494 | end 495 | end, 496 | 497 | -- Gets whether this Widget is queriable. 498 | -- returns `true` for queriable, otherwise `false` 499 | queriable = function (self) 500 | return self.queriablility 501 | end, 502 | -- Sets whether this Widget is queriable. 503 | -- `val`: whether this Widget is queriable 504 | setQueriable = function (self, val) 505 | self.queriablility = val 506 | 507 | return self 508 | end, 509 | -- Queries a Widget at the specific position. 510 | -- `x`: the x position to query 511 | -- `y`: the y position to query 512 | query = function (self, x, y) 513 | if not x or not y or beUtils.isNaN(x) or beUtils.isNaN(y) then 514 | return nil 515 | end 516 | if not self:visible() then 517 | return nil 518 | end 519 | if not self:queriable() then 520 | return nil 521 | end 522 | if self.children ~= nil then 523 | for i = #self.children, 1, -1 do 524 | local c = self.children[i] 525 | local ret = c:query(x, y) 526 | if ret ~= nil then 527 | return ret 528 | end 529 | end 530 | end 531 | local x_, y_ = self.worldX, self.worldY 532 | local w_, h_ = self:size() 533 | if Math.intersects(Vec2.new(x, y), Rect.byXYWH(x_, y_, w_, h_)) then 534 | return self 535 | end 536 | 537 | return nil 538 | end, 539 | 540 | -- Gets whether this Widget has captured mouse event. 541 | captured = function (self) 542 | if not self.visibility then 543 | return false 544 | end 545 | if not self.capturability then 546 | return false 547 | end 548 | if self.popup then 549 | return true 550 | end 551 | if self.hovering then 552 | return true 553 | end 554 | if self.children ~= nil then 555 | for _, c in ipairs(self.children) do 556 | if c:captured() then 557 | return true 558 | end 559 | end 560 | end 561 | 562 | return false 563 | end, 564 | 565 | -- Schedules a tweening procedure. 566 | -- `t`: the tweening object 567 | tween = function (self, t) 568 | if self.tweens == nil then 569 | self.tweens = { } 570 | end 571 | table.insert(self.tweens, t) 572 | 573 | return self 574 | end, 575 | -- Clears all tweening procedures. 576 | clearTweenings = function (self) 577 | self.tweens = nil 578 | 579 | return self 580 | end, 581 | 582 | _updateHierarchy = function (self) 583 | if self.parent then 584 | local w, h = self.parent:size() 585 | self:_updateLayout(w, h) 586 | else 587 | local canvasWidth, canvasHeight = Canvas.main:size() 588 | self:_updateLayout(canvasWidth, canvasHeight) 589 | end 590 | end, 591 | _updateLayout = function (self, parentWidth, parentHeight) 592 | if self.parentWidth == parentWidth and self.parentHeight == parentHeight then 593 | return 594 | end 595 | self.parentWidth, self.parentHeight = parentWidth, parentHeight 596 | local w, h = self:size() 597 | 598 | if self.children == nil then 599 | return 600 | end 601 | for _, c in ipairs(self.children) do 602 | c:_updateLayout(w, h) 603 | end 604 | end, 605 | _update = function (self, theme, delta, dx, dy, event) 606 | if not self.visibility then 607 | return 608 | end 609 | 610 | local once = not event 611 | if once then 612 | self:_touchClip() 613 | if self.context == nil then 614 | self.context = { 615 | navigated = nil, 616 | focus = nil, 617 | active = nil, 618 | dragging = nil, 619 | popup = nil, 620 | clippingStack = self.clippingStack 621 | } 622 | end 623 | 624 | local canvasWidth, canvasHeight = Canvas.main:size() 625 | self:_updateLayout(canvasWidth, canvasHeight) 626 | 627 | local x, y, lmb, _4, _5, wheel = mouse() 628 | event = { 629 | mousePosition = Vec2.new(x, y), 630 | mouseDown = lmb, 631 | mouseWheel = wheel, 632 | canceled = false, 633 | context = self.context 634 | } 635 | 636 | if lmb or wheel ~= 0 then 637 | self.context.navigated = nil 638 | end 639 | 640 | if self.popup ~= nil then 641 | if self.popup.context == nil then 642 | self.popup.context = self.context 643 | end 644 | self.popup:_navigate() 645 | else 646 | self:_navigate() 647 | end 648 | else 649 | self.context = nil 650 | end 651 | 652 | if self.tweens then 653 | local dead = nil 654 | for _, t in ipairs(self.tweens) do 655 | if t:update(delta) then 656 | if dead == nil then 657 | dead = { } 658 | end 659 | table.insert(dead, t) 660 | end 661 | end 662 | if dead then 663 | self.tweens = beUtils.filter(self.tweens, function (t) 664 | return not beUtils.exists(dead, t) 665 | end) 666 | if #self.tweens == 0 then 667 | self.tweens = nil 668 | end 669 | end 670 | end 671 | 672 | local ox, oy = self:offset() 673 | local px, py = self:position() 674 | local x, y = dx + px + ox, dy + py + oy 675 | if beGUI.Widget.DEBUG then 676 | local w, h = self:size() 677 | rect(x, y, x + w - 1, y + h - 1, false, Color.new(255, 0, 0, 128)) 678 | end 679 | if self.capturability == true then 680 | local w, h = self:size() 681 | self.hovering = Math.intersects(event.mousePosition, Rect.byXYWH(x, y, w, h)) 682 | end 683 | self.worldX, self.worldY = x, y 684 | self:_updateChildren(theme, delta, x, y, event) 685 | 686 | if self.focusIfHovering then 687 | local w, h = self:size() 688 | if self.hovering or Math.intersects(event.mousePosition, Rect.byXYWH(x, y, w, h)) then 689 | if event.context then 690 | event.context.focus = self 691 | end 692 | end 693 | end 694 | if event.context and event.context.focus == self and (event.context.navigated ~= nil or self.focusIfHovering) then 695 | self:_updateFocus(delta, x, y) 696 | end 697 | 698 | if once then 699 | local dragging = self.context and self.context.dragging 700 | if dragging then 701 | dragging:updateDragging(theme, delta, event) 702 | end 703 | end 704 | end, 705 | _updateChildren = function (self, theme, delta, dx, dy, event) 706 | if self.children == nil then 707 | return 708 | end 709 | for _, c in ipairs(self.children) do 710 | if not self.popup or self.popup == c then 711 | c:_update(theme, delta, dx, dy, event) 712 | else 713 | c:_update( 714 | theme, delta, dx, dy, 715 | { 716 | mousePosition = nil, 717 | mouseDown = false, 718 | mouseWheel = 0, 719 | canceled = false, 720 | context = event.context 721 | } 722 | ) 723 | end 724 | end 725 | end, 726 | _updateFocus = function (self, delta, x, y) 727 | local HALF_DURATION = 1 728 | self.focusTicks = self.focusTicks + delta 729 | if self.focusTicks >= HALF_DURATION * 2 then 730 | self.focusTicks = self.focusTicks - HALF_DURATION * 2 731 | end 732 | local f = 0 733 | if self.focusTicks < HALF_DURATION then 734 | f = beUtils.clamp(self.focusTicks / HALF_DURATION, 0, 1) 735 | else 736 | f = beUtils.clamp(1 - (self.focusTicks - HALF_DURATION) / HALF_DURATION, 0, 1) 737 | end 738 | 739 | local w, h = self:size() 740 | local col = Color.new(55 + 200 * f, 55 + 200 * f, 55 + 200 * (1 - f)) 741 | rect(x, y, x + w - 1, y + h - 1, false, col) 742 | end, 743 | 744 | _touchClip = function (self) 745 | if self.clippingStack == nil then 746 | self.clippingStack = beStack.NonShrinkStack.new(5) -- Change this limit if you really need more than that. 747 | end 748 | 749 | return self 750 | end, 751 | _getClip = function (self) 752 | local clippingStack = self.clippingStack 753 | if not clippingStack then 754 | return nil, nil 755 | end 756 | if clippingStack:empty() then 757 | return nil, nil 758 | end 759 | 760 | local ret1, ret2 = clippingStack:top() 761 | 762 | return ret1, ret2 763 | end, 764 | _beginClip = function (self, event, x, y, w, h) 765 | local clippingStack = event.context and event.context.clippingStack or nil 766 | if not clippingStack then 767 | return false 768 | end 769 | 770 | if clippingStack:empty() then 771 | local canvasWidth, canvasHeight = Canvas.main:size() 772 | if canvasWidth <= 0 or canvasHeight <= 0 then 773 | return false 774 | end 775 | local rect0 = Rect.byXYWH(0, 0, canvasWidth, canvasHeight) 776 | local rect1 = Rect.byXYWH(x, y, w, h) 777 | local rect2 = beUtils.intersected(rect0, rect1) 778 | if not rect2 then 779 | return false 780 | end 781 | local x_, y_, w_, h_ = clip(rect2:xMin(), rect2:yMin(), rect2:width(), rect2:height()) 782 | clippingStack:push( 783 | x_ and Rect.byXYWH(x_, y_, w_, h_) or false, 784 | rect2 785 | ) 786 | 787 | return true 788 | 789 | -- It is not allowed to clip when an area's border is outside the drawing surface 790 | -- with some graphics backends. If beGUI would never run on those platforms, use 791 | -- the following code to simplify it. 792 | --[[ 793 | local x_, y_, w_, h_ = clip(x, y, w, h) 794 | clippingStack:push( 795 | x_ and Rect.byXYWH(x_, y_, w_, h_) or false, 796 | Rect.byXYWH(x, y, w, h) 797 | ) 798 | 799 | return true 800 | ]] 801 | else 802 | local _, rect0 = clippingStack:top() 803 | local rect1 = Rect.byXYWH(x, y, w, h) 804 | local rect2 = beUtils.intersected(rect0, rect1) 805 | if not rect2 then 806 | return false 807 | end 808 | local x_, y_, w_, h_ = clip(rect2:xMin(), rect2:yMin(), rect2:width(), rect2:height()) 809 | clippingStack:push( 810 | x_ and Rect.byXYWH(x_, y_, w_, h_) or false, 811 | rect2 812 | ) 813 | 814 | return true 815 | end 816 | end, 817 | _endClip = function (self, event) 818 | local clippingStack = event.context and event.context.clippingStack or nil 819 | if not clippingStack then 820 | return false 821 | end 822 | 823 | local rect0, _ = clippingStack:pop() 824 | if rect0 then 825 | clip(rect0:xMin(), rect0:yMin(), rect0:width(), rect0:height()) 826 | else 827 | clip() 828 | end 829 | 830 | return true 831 | end, 832 | 833 | _navigate = function (self) 834 | if not self.context or not self.context.navigated then 835 | return 836 | end 837 | 838 | if self.context.navigated == 'cancel' then 839 | self:_cancelNavigation() 840 | end 841 | 842 | local lst = { } 843 | self:_fillNavigation(lst) 844 | 845 | local first = #lst > 0 and lst[1] or nil 846 | local last = #lst > 0 and lst[#lst] or nil 847 | local prev = nil 848 | for i, c in ipairs(lst) do 849 | local next = i < #lst and lst[i + 1] or nil 850 | local nav = c:navigatable() 851 | if self.context.navigated == 'prev' then 852 | if self.context.focus == c then 853 | local scrolled = false 854 | if nav == 'content' then 855 | if c:_scroll(nil, -1) then 856 | scrolled = true 857 | end 858 | end 859 | if not scrolled then 860 | self.context.focus = prev or last 861 | end 862 | self.context.navigated = false 863 | end 864 | elseif self.context.navigated == 'next' then 865 | if self.context.focus == c then 866 | local scrolled = false 867 | if nav == 'content' then 868 | if c:_scroll(nil, 1) then 869 | scrolled = true 870 | end 871 | end 872 | if not scrolled then 873 | self.context.focus = next or first 874 | end 875 | self.context.navigated = false 876 | end 877 | end 878 | prev = c 879 | end 880 | if self.context.navigated == 'prev' then 881 | self.context.focus = last 882 | self.context.navigated = false 883 | elseif self.context.navigated == 'next' then 884 | self.context.focus = first 885 | self.context.navigated = false 886 | end 887 | end, 888 | _fillNavigation = function (self, lst) 889 | local nav = self:navigatable() 890 | if not nav then 891 | return 892 | end 893 | if not self:visible() then 894 | return 895 | end 896 | if (nav == 'all' or nav == 'children') and self.children then 897 | for _, c in ipairs(self.children) do 898 | c:_fillNavigation(lst) 899 | end 900 | end 901 | if nav == 'all' or nav == 'content' then 902 | table.insert(lst, self) 903 | end 904 | end, 905 | _cancelNavigation = function (self) 906 | self.context.focus = nil 907 | self.context.navigated = false 908 | end, 909 | 910 | _trigger = function (self, event, ...) 911 | if not event then 912 | return nil 913 | end 914 | if self.events == nil then 915 | return nil 916 | end 917 | if self.events[event] == nil then 918 | return nil 919 | end 920 | if #self.events[event] == 1 then 921 | local ret = self.events[event][1](...) 922 | 923 | return ret 924 | else 925 | local ret = { } 926 | for i, h in ipairs(self.events[event]) do 927 | table.insert(ret, h(...) or false) 928 | end 929 | 930 | return table.unpack(ret) 931 | end 932 | end 933 | }) 934 | 935 | --[[ 936 | Exporting. 937 | ]] 938 | 939 | return { 940 | Widget = Widget 941 | } 942 | -------------------------------------------------------------------------------- /src/libs/beGUI/beGUI_Containers.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | The MIT License 3 | 4 | Copyright (C) 2021 - 2022 Tony Wang 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | ]] 23 | 24 | local beClass = require 'libs/beGUI/beClass' 25 | local beUtils = require 'libs/beGUI/beGUI_Utils' 26 | local beWidget = require 'libs/beGUI/beGUI_Widget' 27 | 28 | --[[ 29 | Widgets. 30 | ]] 31 | 32 | local Group = beClass.class({ 33 | ctor = function (self, content) 34 | beWidget.Widget.ctor(self) 35 | 36 | self.content = content 37 | end, 38 | 39 | __tostring = function (self) 40 | return 'Group' 41 | end, 42 | 43 | -- Gets the group name. 44 | getValue = function (self) 45 | return self.content 46 | end, 47 | -- Sets the group name. 48 | setValue = function (self, val) 49 | self.content = val 50 | 51 | return self 52 | end, 53 | 54 | navigatable = function (self) 55 | return 'children' 56 | end, 57 | 58 | _update = function (self, theme, delta, dx, dy, event) 59 | if not self.visibility then 60 | return 61 | end 62 | 63 | local ox, oy = self:offset() 64 | local px, py = self:position() 65 | local x, y = dx + px + ox, dy + py + oy 66 | local w, h = self:size() 67 | 68 | local font_ = theme['font'] 69 | local elem = theme['group'] 70 | local x_ = x + elem.content_offset[1] 71 | local w_, h_ = measure(self.content, font_.resource, font_.margin or 1, font_.scale or 1) 72 | local black = Color.new(elem.color.r, elem.color.g, elem.color.b, self.transparency or 255) 73 | line(x, y + h_ * 0.5, x, y + h - 1, black) 74 | line(x, y + h - 1, x + w - 1, y + h - 1, black) 75 | line(x + w - 1, y + h_ * 0.5, x + w - 1, y + h - 1, black) 76 | line(x, y + h_ * 0.5, x_ - 2, y + h_ * 0.5, black) 77 | line(x_ + w_ + 2, y + h_ * 0.5, x + w - 1, y + h_ * 0.5, black) 78 | local elem_ = theme['group_title'] 79 | beUtils.textCenter(self.content, font_, x_, y, w_, h_, elem_.content_offset, self.transparency) 80 | 81 | beWidget.Widget._update(self, theme, delta, dx, dy, event) 82 | end 83 | }, beWidget.Widget) 84 | 85 | local List = beClass.class({ 86 | _withScrollBar = false, 87 | _scrolledTimestamp = nil, 88 | _pressed = false, 89 | _pressedPosition = nil, 90 | _pressingPosition = nil, 91 | _scrolling = nil, 92 | _scrollX = 0, _scrollY = 0, 93 | _scrollSpeed = 16, 94 | _maxX = 0, _maxY = 0, 95 | _scrollDirectionalTimestamp = nil, 96 | _scrollableVertically = true, 97 | _scrollableHorizontally = false, 98 | _inertance = nil, 99 | _inertanceSpeed = nil, 100 | _inertancePosition = nil, 101 | _inertanceDirection = nil, 102 | _childrenCount = 0, 103 | _theme = nil, 104 | _scrollable = true, 105 | 106 | -- Constructs a List. 107 | -- `withScrollBar`: whether to draw scroll bar(s) 108 | ctor = function (self, withScrollBar) 109 | beWidget.Widget.ctor(self) 110 | 111 | self._withScrollBar = withScrollBar 112 | end, 113 | 114 | __tostring = function (self) 115 | return 'List' 116 | end, 117 | 118 | -- Gets whether to allow scrolling vertically. 119 | scrollableVertically = function (self) 120 | return self._scrollableVertically 121 | end, 122 | -- Sets whether to allow scrolling vertically. 123 | setScrollableVertically = function (self, val) 124 | self._scrollableVertically = val 125 | 126 | return self 127 | end, 128 | -- Gets whether to allow scrolling horizontally. 129 | scrollableHorizontally = function (self) 130 | return self._scrollableHorizontally 131 | end, 132 | -- Sets whether to allow scrolling horizontally. 133 | setScrollableHorizontally = function (self, val) 134 | self._scrollableHorizontally = val 135 | 136 | return self 137 | end, 138 | 139 | -- Gets the scroll speed. 140 | scrollSpeed = function (self) 141 | return self._scrollSpeed 142 | end, 143 | -- Sets the scroll speed. 144 | setScrollSpeed = function (self, val) 145 | self._scrollSpeed = val 146 | 147 | return self 148 | end, 149 | 150 | setTheme = function (self, theme) 151 | self._theme = theme 152 | 153 | return self 154 | end, 155 | 156 | -- Gets whether can scroll the widget by mouse wheel. 157 | scrollable = function (self) 158 | return self._scrollable 159 | end, 160 | -- Sets whether can scroll the widget by mouse wheel. 161 | setScrollable = function (self, val) 162 | self._scrollable = val 163 | 164 | return self 165 | end, 166 | 167 | navigatable = function (self) 168 | return 'content' 169 | end, 170 | 171 | _update = function (self, theme, delta, dx, dy, event) 172 | if not self.visibility then 173 | return 174 | end 175 | 176 | local ox, oy = self:offset() 177 | local px, py = self:position() 178 | local x, y = dx + px + ox, dy + py + oy 179 | local w, h = self:size() 180 | local down = false 181 | local intersects = Math.intersects(event.mousePosition, Rect.byXYWH(x, y, w, h)) 182 | if event.canceled or event.context.dragging then 183 | self._pressed = false 184 | elseif self._pressed then 185 | down = event.mouseDown 186 | else 187 | down = event.mouseDown and intersects 188 | end 189 | local now = DateTime.ticks() 190 | if down and not self._pressed then 191 | if self._withScrollBar then 192 | self._scrolledTimestamp = now 193 | end 194 | self._pressed = true 195 | self._pressedPosition = event.mousePosition 196 | self._pressingPosition = event.mousePosition 197 | self._scrolling = nil 198 | self._scrollDirectionalTimestamp = now 199 | self._inertancePosition = event.mousePosition 200 | self._inertanceSpeed = 0 201 | elseif not down and self._pressed then 202 | if self._inertanceSpeed ~= nil then 203 | local speed = self._inertanceSpeed 204 | if speed > 5 then 205 | local force = beUtils.clamp(1 - speed / 40, 0, 6) * 20 + 4 206 | local dist = self._pressingPosition - self._pressedPosition 207 | if self._scrollableHorizontally and w < self._maxX then 208 | if math.abs(dist.x) < math.abs(dist.y) then 209 | self._inertance = dist.y * force 210 | self._inertanceDirection = 'y' 211 | else 212 | self._inertance = dist.x * force 213 | self._inertanceDirection = 'x' 214 | end 215 | elseif self._scrollableVertically then 216 | self._inertance = dist.y * force 217 | self._inertanceDirection = 'y' 218 | end 219 | end 220 | end 221 | self._pressed = false 222 | self._pressedPosition = nil 223 | self._pressingPosition = nil 224 | self._scrolling = nil 225 | self._scrollDirectionalTimestamp = nil 226 | self._inertancePosition = nil 227 | self._inertanceSpeed = nil 228 | elseif down and self._pressed then 229 | if self._withScrollBar then 230 | self._scrolledTimestamp = now 231 | end 232 | if self._scrollDirectionalTimestamp ~= nil then 233 | local diff = DateTime.toSeconds(now - self._scrollDirectionalTimestamp) 234 | if diff < 0.3 and self._pressedPosition ~= self._pressingPosition then 235 | local diff = self._pressedPosition - self._pressingPosition 236 | if self._scrollableHorizontally and w < self._maxX then 237 | if math.abs(diff.x) < math.abs(diff.y) then 238 | self._scrolling = 'y' 239 | else 240 | self._scrolling = 'x' 241 | end 242 | elseif self._scrollableVertically then 243 | if math.abs(diff.x) < math.abs(diff.y) then 244 | self._scrolling = 'y' 245 | end 246 | end 247 | self._scrollDirectionalTimestamp = nil 248 | end 249 | end 250 | if self._inertancePosition ~= event.mousePosition then 251 | if self._inertancePosition then 252 | local diff = event.mousePosition - self._inertancePosition 253 | self._inertanceSpeed = diff.length 254 | end 255 | self._inertancePosition = event.mousePosition 256 | end 257 | end 258 | 259 | local elem = theme[self._theme or 'list'] 260 | beUtils.tex9Grid(elem, x, y, w, h, nil, self.transparency, nil) 261 | 262 | local scrollBarTransparency = nil 263 | if self._scrolledTimestamp then 264 | local SCROLL_BAR_TIMEOUT_SECONDS = 1 265 | local diff = DateTime.toSeconds(now - self._scrolledTimestamp) 266 | local factor = diff / SCROLL_BAR_TIMEOUT_SECONDS 267 | scrollBarTransparency = factor < 0.7 and 1 or beUtils.clamp(1 - (factor - 0.7) / 0.3, 0, 1) 268 | if factor >= 1 then 269 | self._scrolledTimestamp = nil 270 | end 271 | end 272 | if self._maxX < w then 273 | self._maxX = w 274 | end 275 | if self._maxY < h then 276 | self._maxY = h 277 | end 278 | if self._pressingPosition then 279 | if self._scrolling == 'x' and event.mousePosition then 280 | local diff = event.mousePosition.x - self._pressingPosition.x 281 | if not beUtils.isNaN(diff) then 282 | self._scrollX = self._scrollX + diff 283 | self._scrollX = beUtils.clamp(self._scrollX, w - self._maxX, 0) 284 | end 285 | elseif self._scrolling == 'y' and event.mousePosition then 286 | local diff = event.mousePosition.y - self._pressingPosition.y 287 | if not beUtils.isNaN(diff) then 288 | self._scrollY = self._scrollY + diff 289 | self._scrollY = beUtils.clamp(self._scrollY, h - self._maxY, 0) 290 | end 291 | end 292 | self._pressingPosition = event.mousePosition 293 | elseif intersects and event.mouseWheel < 0 and self._scrollable then 294 | if self._withScrollBar then 295 | self._scrolledTimestamp = now 296 | end 297 | if self._scrollableVertically and not key(beUtils.KeyCodeLShift) and not key(beUtils.KeyCodeRShift) then 298 | self._scrollY = beUtils.clamp(self._scrollY - self._scrollSpeed, h - self._maxY, 0) 299 | elseif self._scrollableHorizontally then 300 | self._scrollX = beUtils.clamp(self._scrollX - self._scrollSpeed, w - self._maxX, 0) 301 | end 302 | elseif intersects and event.mouseWheel > 0 and self._scrollable then 303 | if self._withScrollBar then 304 | self._scrolledTimestamp = now 305 | end 306 | if self._scrollableVertically and not key(beUtils.KeyCodeLShift) and not key(beUtils.KeyCodeRShift) then 307 | self._scrollY = beUtils.clamp(self._scrollY + self._scrollSpeed, h - self._maxY, 0) 308 | elseif self._scrollableHorizontally then 309 | self._scrollX = beUtils.clamp(self._scrollX + self._scrollSpeed, w - self._maxX, 0) 310 | end 311 | end 312 | if not intersects then 313 | event = { 314 | mousePosition = event.mousePosition, 315 | mouseDown = false, 316 | mouseWheel = 0, 317 | canceled = false, 318 | context = event.context 319 | } 320 | elseif self._scrolling then 321 | event = { 322 | mousePosition = event.mousePosition, 323 | mouseDown = false, 324 | mouseWheel = 0, 325 | canceled = true, 326 | context = event.context 327 | } 328 | end 329 | if self._inertance then 330 | if self._withScrollBar then 331 | self._scrolledTimestamp = now 332 | end 333 | if self._inertanceDirection == 'y' then 334 | self._scrollY = beUtils.clamp(self._scrollY + self._inertance * delta, h - self._maxY, 0) 335 | else 336 | self._scrollX = beUtils.clamp(self._scrollX + self._inertance * delta, w - self._maxX, 0) 337 | end 338 | self._inertance = self._inertance * 0.9 339 | if math.abs(self._inertance) < 0.1 then 340 | self._inertance = nil 341 | self._inertanceSpeed = nil 342 | self._inertancePosition = nil 343 | self._inertanceDirection = nil 344 | end 345 | end 346 | if self._scrollX < 0 then 347 | self._scrollX = beUtils.clamp(self._scrollX, w - self._maxX, 0) 348 | end 349 | if self._scrollY < 0 then 350 | self._scrollY = beUtils.clamp(self._scrollY, h - self._maxY, 0) 351 | end 352 | 353 | local clipped = self:_beginClip(event, x + 1, y + 1, w - 2, h - 2) 354 | beWidget.Widget._update(self, theme, delta, dx + self._scrollX, dy + self._scrollY, event) 355 | local count = self:getChildrenCount() 356 | if count ~= self._childrenCount then 357 | self._childrenCount = count 358 | end 359 | if clipped then 360 | if elem.color and scrollBarTransparency then 361 | local col = Color.new(elem.color.r, elem.color.g, elem.color.b, elem.color.a * scrollBarTransparency) 362 | if self._scrollableVertically then 363 | local widgetPos = y + 1 364 | local widgetSize = h - 2 365 | local clientSize = widgetSize 366 | if self._scrollableHorizontally then 367 | clientSize = clientSize - 4 368 | end 369 | local contentSize = self._maxY 370 | local barSize = math.ceil(math.max(math.min((widgetSize / contentSize) * widgetSize, clientSize), 8)) 371 | local percent = beUtils.clamp(-self._scrollY / (contentSize - widgetSize), 0, 1) 372 | local slide = clientSize - barSize 373 | local offset = slide * percent 374 | local x_, y_, w_, h_ = 375 | math.floor(x + w - 4), beUtils.round(widgetPos + offset), 376 | math.floor(x + w - 1), math.min(beUtils.round(widgetPos + offset + barSize + 1), widgetPos + clientSize) 377 | rect(x_, y_, w_, h_, true, col) 378 | end 379 | if self._scrollableHorizontally then 380 | local widgetPos = x + 1 381 | local widgetSize = w - 2 382 | local clientSize = widgetSize 383 | if self._scrollableVertically then 384 | clientSize = clientSize - 4 385 | end 386 | local contentSize = self._maxX 387 | local barSize = math.ceil(math.max(math.min((widgetSize / contentSize) * widgetSize, clientSize), 8)) 388 | local percent = beUtils.clamp(-self._scrollX / (contentSize - widgetSize), 0, 1) 389 | local slide = clientSize - barSize 390 | local offset = slide * percent 391 | local x_, y_, w_, h_ = 392 | beUtils.round(widgetPos + offset), math.floor(y + h - 4), 393 | math.min(beUtils.round(widgetPos + offset + barSize + 1), widgetPos + clientSize), math.floor(y + h - 1) 394 | rect(x_, y_, w_, h_, true, col) 395 | end 396 | end 397 | end 398 | if clipped then 399 | self:_endClip(event) 400 | end 401 | end, 402 | _updateChildren = function (self, theme, delta, dx, dy, event) 403 | if self.children == nil then 404 | return 405 | end 406 | self._maxX, self._maxY = 0, 0 407 | for _, c in ipairs(self.children) do 408 | c:_update(theme, delta, dx, dy, event) 409 | local px, py = c:position() 410 | local w, h = c:size() 411 | local x, y = px + w, py + h 412 | if x > self._maxX then 413 | self._maxX = x 414 | end 415 | if y > self._maxY then 416 | self._maxY = y 417 | end 418 | end 419 | end, 420 | _updateFocus = function (self, delta, x, y) 421 | local HALF_DURATION = 1 422 | self.focusTicks = self.focusTicks + delta 423 | if self.focusTicks >= HALF_DURATION * 2 then 424 | self.focusTicks = self.focusTicks - HALF_DURATION * 2 425 | end 426 | local f = 0 427 | if self.focusTicks < HALF_DURATION then 428 | f = beUtils.clamp(self.focusTicks / HALF_DURATION, 0, 1) 429 | else 430 | f = beUtils.clamp(1 - (self.focusTicks - HALF_DURATION) / HALF_DURATION, 0, 1) 431 | end 432 | 433 | local w, h = self:size() 434 | local col = Color.new(55 + 200 * f, 55 + 200 * f, 55 + 200 * (1 - f)) 435 | rect(x - self._scrollX + 1, y - self._scrollY + 1, x + w - 1 - self._scrollX - 2, y + h - 1 - self._scrollY - 1, false, col) 436 | end, 437 | 438 | _scroll = function (self, dirX, dirY) 439 | local w, h = self:size() 440 | if dirX then 441 | if dirX < 0 then 442 | if self._scrollX < 0 then 443 | self._scrollX = beUtils.clamp(self._scrollX + self._scrollSpeed, w - self._maxX, 0) 444 | 445 | return true 446 | end 447 | elseif dirX > 0 then 448 | if self._scrollX > w - self._maxX then 449 | self._scrollX = beUtils.clamp(self._scrollX - self._scrollSpeed, w - self._maxX, 0) 450 | 451 | return true 452 | end 453 | end 454 | end 455 | if dirY then 456 | if dirY < 0 then 457 | if self._scrollY < 0 then 458 | self._scrollY = beUtils.clamp(self._scrollY + self._scrollSpeed, h - self._maxY, 0) 459 | 460 | return true 461 | end 462 | elseif dirY > 0 then 463 | if self._scrollY > h - self._maxY then 464 | self._scrollY = beUtils.clamp(self._scrollY - self._scrollSpeed, h - self._maxY, 0) 465 | 466 | return true 467 | end 468 | end 469 | end 470 | 471 | return false 472 | end 473 | }, beWidget.Widget) 474 | 475 | local Draggable = beClass.class({ 476 | _draggedTimestamp = nil, 477 | _draggedPosition = nil, 478 | _draggingPosition = nil, 479 | _dragging = false, 480 | _dragOffset = nil, 481 | _draggingX = 0, _draggingY = 0, 482 | _draggingEvent = nil, 483 | _draggingTimeout = false, 484 | _dropping = false, 485 | 486 | -- Constructs a Draggable. 487 | ctor = function (self) 488 | beWidget.Widget.ctor(self) 489 | 490 | self._dragOffset = Vec2.new(0, 0) 491 | end, 492 | 493 | __tostring = function (self) 494 | return 'Draggable' 495 | end, 496 | 497 | navigatable = function (self) 498 | return 'children' 499 | end, 500 | 501 | updateDragging = function (self, theme, delta, event) 502 | beWidget.Widget._update(self, theme, delta, self._draggingX, self._draggingY, self._draggingEvent or event) 503 | end, 504 | 505 | _update = function (self, theme, delta, dx, dy, event) 506 | if not self.visibility then 507 | return 508 | end 509 | 510 | local ox, oy = self:offset() 511 | local px, py = self:position() 512 | local x, y = dx + px + ox, dy + py + oy 513 | local w, h = self:size() 514 | local down = false 515 | local intersects = Math.intersects(event.mousePosition, Rect.byXYWH(x, y, w, h)) 516 | if event.context.dragging then 517 | down = event.mouseDown 518 | else 519 | down = event.mouseDown and intersects 520 | end 521 | local picking = false 522 | local dropping = false 523 | if self._draggingTimeout then 524 | if not down then 525 | self._draggingTimeout = false 526 | end 527 | elseif down and not event.context.dragging then -- Picked the current widget. 528 | event.context.dragging = self 529 | self._draggedTimestamp = DateTime.ticks() 530 | self._draggedPosition = event.mousePosition 531 | self._draggingPosition = event.mousePosition 532 | self._dragging = false 533 | picking = true 534 | self._draggingEvent = { 535 | mousePosition = event.mousePosition, 536 | mouseDown = false, 537 | mouseWheel = 0, 538 | canceled = false, 539 | context = { 540 | navigated = nil, 541 | focus = nil, 542 | active = nil, 543 | dragging = nil, 544 | popup = nil, 545 | clippingStack = event.clippingStack 546 | } 547 | } 548 | elseif not down and event.context.dragging == self then -- Dropped the current widget. 549 | event.context.dragging = nil 550 | self._draggedTimestamp = nil 551 | self._draggedPosition = nil 552 | self._draggingPosition = nil 553 | self._dragging = false 554 | self._dragOffset = Vec2.new(0, 0) 555 | dropping = true 556 | self._draggingEvent = nil 557 | self._draggingTimeout = false 558 | self._dropping = true 559 | elseif down and event.context.dragging == self then -- Is dragging the current widget. 560 | if self._draggedTimestamp ~= nil then 561 | local diff = DateTime.toSeconds(DateTime.ticks() - self._draggedTimestamp) 562 | if diff < 0.3 and self._draggedPosition ~= self._draggingPosition then 563 | self._dragging = true 564 | self._draggedTimestamp = nil 565 | elseif diff >= 0.3 and self._draggedPosition ~= self._draggingPosition then 566 | event.context.dragging = nil 567 | self._draggedTimestamp = nil 568 | self._draggedPosition = nil 569 | self._draggingPosition = nil 570 | self._dragging = false 571 | self._dragOffset = Vec2.new(0, 0) 572 | self._draggingEvent = nil 573 | self._draggingTimeout = true 574 | end 575 | end 576 | end 577 | 578 | if self._draggingPosition then 579 | if self._dragging then 580 | self._dragOffset = self._dragOffset + (event.mousePosition - self._draggingPosition) 581 | end 582 | self._draggingPosition = event.mousePosition 583 | end 584 | if self._draggingEvent then 585 | if self._dragging then 586 | self._draggingEvent.mousePosition = nil 587 | self._draggingEvent.mouseDown = false 588 | self._draggingEvent.mouseWheel = 0 589 | self._draggingEvent.canceled = true 590 | else 591 | self._draggingEvent.mousePosition = event.mousePosition 592 | self._draggingEvent.mouseDown = event.mouseDown 593 | self._draggingEvent.mouseWheel = event.mouseWheel 594 | self._draggingEvent.canceled = event.canceled 595 | end 596 | end 597 | 598 | self._draggingX, self._draggingY = dx + self._dragOffset.x, dy + self._dragOffset.y 599 | if not picking and not dropping and not self._dragging then 600 | if self._dropping then 601 | self._dropping = false 602 | else 603 | beWidget.Widget._update(self, theme, delta, self._draggingX, self._draggingY, self._draggingEvent or event) 604 | end 605 | end 606 | end 607 | }, beWidget.Widget) 608 | 609 | local Droppable = beClass.class({ 610 | _pressed = false, 611 | _dropping = nil, 612 | _dropped = false, 613 | 614 | -- Constructs a Droppable. 615 | ctor = function (self) 616 | beWidget.Widget.ctor(self) 617 | end, 618 | 619 | __tostring = function (self) 620 | return 'Droppable' 621 | end, 622 | 623 | navigatable = function (self) 624 | return 'all' 625 | end, 626 | 627 | _update = function (self, theme, delta, dx, dy, event) 628 | if not self.visibility then 629 | return 630 | end 631 | 632 | local ox, oy = self:offset() 633 | local px, py = self:position() 634 | local x, y = dx + px + ox, dy + py + oy 635 | local w, h = self:size() 636 | local down = false 637 | local intersects = Math.intersects(event.mousePosition, Rect.byXYWH(x, y, w, h)) 638 | local dropped = nil 639 | if event.canceled then 640 | self._dropping = nil 641 | else 642 | down = event.mouseDown and intersects 643 | end 644 | if down and not self._dropping then 645 | if event.context.dragging then 646 | self._dropping = event.context.dragging 647 | self._dropped = true 648 | self:_trigger('entered', self, self._dropping) 649 | end 650 | elseif not down and self._dropping then 651 | if event.mouseDown then 652 | self:_trigger('left', self, self._dropping) 653 | else 654 | local droppable = self:_trigger('dropping', self, self._dropping) 655 | if droppable then 656 | dropped = droppable -- Doesn't trigger 'clicked' when has droppable. 657 | if self._dropping.parent then 658 | self._dropping.parent:removeChild(self._dropping) 659 | self:addChild(self._dropping) 660 | end 661 | self:_trigger('dropped', self, self._dropping) 662 | else 663 | self:_trigger('left', self, self._dropping) 664 | end 665 | end 666 | self._dropping = nil 667 | end 668 | if dropped then 669 | self._pressed = false 670 | event.context.focus = self 671 | self._dropped = nil 672 | elseif down and not self._pressed then 673 | self._pressed = true 674 | elseif not event.mouseDown and self._pressed then 675 | self._pressed = false 676 | event.context.focus = self 677 | if not self._dropped then 678 | self:_trigger('clicked', self) 679 | end 680 | self._dropped = nil 681 | elseif event.context.focus == self and event.context.navigated == 'press' then 682 | self:_trigger('clicked', self) 683 | event.context.navigated = false 684 | end 685 | 686 | beWidget.Widget._update(self, theme, delta, dx, dy, event) 687 | end 688 | }, beWidget.Widget) 689 | 690 | local Tab = beClass.class({ 691 | _pages = nil, 692 | _pagesInitialized = false, 693 | _tabSize = nil, 694 | _value = 1, 695 | _focusArea = nil, 696 | _headHeight = nil, 697 | _pressed = false, 698 | _scrollable = true, 699 | 700 | -- Constructs a Tab. 701 | ctor = function (self) 702 | beWidget.Widget.ctor(self) 703 | 704 | self.content = { 'Noname' } 705 | 706 | self._pages = { { } } 707 | self._focusArea = { 0, 0, 0, 0 } 708 | end, 709 | 710 | __tostring = function (self) 711 | return 'Tab' 712 | end, 713 | 714 | -- Adds a Tab page with the specific title. 715 | -- `title`: the Tab page title to add 716 | add = function (self, title) 717 | if self._pagesInitialized then 718 | table.insert(self.content, title) 719 | table.insert(self._pages, { }) 720 | else 721 | self.content = { title } 722 | self._pagesInitialized = true 723 | end 724 | 725 | return self 726 | end, 727 | -- Gets the page count of the Tab Widget. 728 | count = function (self) 729 | return #self.content 730 | end, 731 | 732 | -- Gets the active page index. 733 | getValue = function (self) 734 | return self._value 735 | end, 736 | -- Sets the active page index. 737 | setValue = function (self, val) 738 | if self._value == val then 739 | return self 740 | end 741 | if val < 1 or val > #self.content then 742 | return self 743 | end 744 | self._value = val 745 | self:_trigger('changed', self, self._value) 746 | 747 | return self 748 | end, 749 | 750 | -- Gets the specified Tab size. 751 | tabSize = function (self) 752 | return self._tabSize 753 | end, 754 | -- Sets the specified Tab size. 755 | setTabSize = function (self, val) 756 | self._tabSize = val 757 | 758 | return self 759 | end, 760 | 761 | addChild = function (self, child) 762 | local result = beWidget.Widget.addChild(self, child) 763 | table.insert(self._pages[self._value], child) 764 | 765 | return result 766 | end, 767 | removeChild = function (self, child) 768 | local result = beWidget.Widget.removeChild(self, child) 769 | self._pages[self._value] = beUtils.filter(self._pages[self._value], function (val) 770 | return val ~= child 771 | end) 772 | 773 | return result 774 | end, 775 | 776 | -- Gets whether can scroll the widget by mouse wheel. 777 | scrollable = function (self) 778 | return self._scrollable 779 | end, 780 | -- Sets whether can scroll the widget by mouse wheel. 781 | setScrollable = function (self, val) 782 | self._scrollable = val 783 | 784 | return self 785 | end, 786 | 787 | navigatable = function (self) 788 | return 'all' 789 | end, 790 | 791 | _update = function (self, theme, delta, dx, dy, event) 792 | if not self.visibility then 793 | return 794 | end 795 | 796 | local ox, oy = self:offset() 797 | local px, py = self:position() 798 | local x, y = dx + px + ox, dy + py + oy 799 | local w, h = self:size() 800 | local down = false 801 | local intersectsHead = Math.intersects(event.mousePosition, Rect.byXYWH(x, y, w, self._headHeight)) 802 | if event.context.active and event.context.active ~= self then 803 | self._pressed = false 804 | elseif event.canceled or event.context.dragging then 805 | self._pressed = false 806 | elseif self._pressed then 807 | down = event.mouseDown 808 | else 809 | down = event.mouseDown and Math.intersects(event.mousePosition, Rect.byXYWH(x, y, w, h)) 810 | end 811 | local pressed = false 812 | if down and not self._pressed then 813 | self._pressed = true 814 | elseif not down and self._pressed then 815 | self._pressed = false 816 | event.context.focus = self 817 | pressed = true 818 | elseif intersectsHead and event.mouseWheel < 0 and self._scrollable then 819 | if #self.content ~= 0 then 820 | local val = self._value + 1 821 | if val > #self.content then 822 | val = #self.content 823 | end 824 | self:setValue(val) 825 | end 826 | elseif intersectsHead and event.mouseWheel > 0 and self._scrollable then 827 | if #self.content ~= 0 then 828 | local val = self._value - 1 829 | if val < 1 then 830 | val = 1 831 | end 832 | self:setValue(val) 833 | end 834 | elseif event.context.focus == self and event.context.navigated == 'inc' then 835 | local val = self._value + 1 836 | self:setValue(val) 837 | event.context.navigated = false 838 | elseif event.context.focus == self and event.context.navigated == 'dec' then 839 | local val = self._value - 1 840 | self:setValue(val) 841 | event.context.navigated = false 842 | end 843 | 844 | local font_ = theme['font'] 845 | local elem = theme['tab'] 846 | local paddingX, paddingY = elem.content_offset[1] or 2, elem.content_offset[2] or 2 847 | local x_ = x 848 | for i, v in ipairs(self.content) do 849 | local w_, h_ = nil, nil 850 | if self._tabSize == nil then 851 | if type(v) == 'string' then 852 | w_, h_ = measure(v, font_.resource, font_.margin or 1, font_.scale or 1) 853 | else 854 | w_, h_ = v.area[3], v.area[4] 855 | end 856 | w_ = w_ + paddingX * 2 857 | h_ = h_ + paddingY * 2 858 | else 859 | w_, h_ = self._tabSize.x, self._tabSize.y 860 | end 861 | if self._headHeight == nil then 862 | self._headHeight = h_ 863 | end 864 | if pressed then 865 | if Math.intersects(event.mousePosition, Rect.byXYWH(x_, y, w_, h_)) then 866 | local val = i 867 | self:setValue(val) 868 | end 869 | end 870 | local black = Color.new(elem.color.r, elem.color.g, elem.color.b, self.transparency or 255) 871 | if i == self._value then 872 | self._focusArea[1], self._focusArea[2], self._focusArea[3], self._focusArea[4] = 873 | x_ + 1, y + 1, x_ + w_ - 2, y + h_ - 1 874 | -- Tab itself. 875 | line(x_, y, x_ + w_ - 1, y, black) 876 | line(x_ + w_ - 1, y, x_ + w_ - 1, y + h_, black) 877 | -- Border. 878 | line(x, y + h - 1, x + w - 1, y + h - 1, black) 879 | line(x, y, x, y + h - 1, black) 880 | line(x + w - 1, y + h_, x + w - 1, y + h - 1, black) 881 | line(x_ + w_ - 1, y + h_, x + w - 1, y + h_, black) 882 | line(x, y + h_, x_, y + h_, black) 883 | else 884 | line(x_, y, x_ + w_ - 1, y, black) 885 | line(x_ + w_ - 1, y, x_ + w_ - 1, y + h_, black) 886 | end 887 | if type(v) == 'string' then 888 | local elem_ = theme['tab_title'] 889 | beUtils.textCenter(v, font_, x_, y, w_, h_, elem_.content_offset, self.transparency) 890 | else 891 | local sx, sy, sw, sh = nil, nil, nil, nil 892 | if v.area then 893 | sx, sy, sw, sh = v.area[1], v.area[2], v.area[3], v.area[4] 894 | end 895 | if self.transparency then 896 | local col = Color.new(255, 255, 255, self.transparency) 897 | tex(v.resource, x_ + paddingX, y + paddingY, sw, sh, sx, sy, sw, sh, 0, Vec2.new(0.5, 0.5), false, false, col) 898 | else 899 | tex(v.resource, x_ + paddingX, y + paddingY, sw, sh, sx, sy, sw, sh) 900 | end 901 | end 902 | x_ = x_ + w_ - 1 903 | end 904 | 905 | beWidget.Widget._update(self, theme, delta, dx, dy, event) 906 | end, 907 | _updateChildren = function (self, theme, delta, dx, dy, event) 908 | if self.children == nil then 909 | return 910 | end 911 | local page = self._pages[self._value] 912 | if not page then 913 | return 914 | end 915 | for _, c in ipairs(page) do 916 | if not self.popup or self.popup == c then 917 | c:_update(theme, delta, dx, dy, event) 918 | else 919 | c:_update( 920 | theme, delta, dx, dy, 921 | { 922 | mousePosition = nil, 923 | mouseDown = false, 924 | mouseWheel = 0, 925 | canceled = false, 926 | context = event.context 927 | } 928 | ) 929 | end 930 | end 931 | end, 932 | _updateFocus = function (self, delta, x, y) 933 | local HALF_DURATION = 1 934 | self.focusTicks = self.focusTicks + delta 935 | if self.focusTicks >= HALF_DURATION * 2 then 936 | self.focusTicks = self.focusTicks - HALF_DURATION * 2 937 | end 938 | local f = 0 939 | if self.focusTicks < HALF_DURATION then 940 | f = beUtils.clamp(self.focusTicks / HALF_DURATION, 0, 1) 941 | else 942 | f = beUtils.clamp(1 - (self.focusTicks - HALF_DURATION) / HALF_DURATION, 0, 1) 943 | end 944 | 945 | local w, h = self:size() 946 | local col = Color.new(55 + 200 * f, 55 + 200 * f, 55 + 200 * (1 - f)) 947 | rect(self._focusArea[1], self._focusArea[2], self._focusArea[3], self._focusArea[4], false, col) 948 | end, 949 | 950 | _fillNavigation = function (self, lst) 951 | local nav = self:navigatable() 952 | if not nav then 953 | return 954 | end 955 | if not self:visible() then 956 | return 957 | end 958 | if nav == 'all' or nav == 'content' then 959 | table.insert(lst, self) 960 | end 961 | if (nav == 'all' or nav == 'children') and self.children then 962 | for _, c in ipairs(self.children) do 963 | c:_fillNavigation(lst) 964 | end 965 | end 966 | end 967 | }, beWidget.Widget) 968 | 969 | --[[ 970 | Exporting. 971 | ]] 972 | 973 | return { 974 | Group = Group, 975 | List = List, 976 | Draggable = Draggable, 977 | Droppable = Droppable, 978 | Tab = Tab 979 | } 980 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](imgs/logo.png) 2 | 3 | beGUI is a minimal customizable GUI system for Lua, and fully written in Lua. 4 | 5 | Try it [in browser](https://paladin-t.github.io/begui/). 6 | 7 | **Index** 8 | 9 | * [Features](#features) 10 | * [Setup](#setup) 11 | * [Reference](#reference) 12 | * [1. Principles](#1-principles) 13 | * [2. Structures](#2-structures) 14 | * [beStructures.Percent](#bestructurespercent) 15 | * [beGUI.percent](#beguipercent) 16 | * [3. Widget](#3-widget) 17 | * [beGUI.Widget](#beguiwidget) 18 | * [4. Basic Widgets](#4-basic-widgets) 19 | * [beGUI.Label](#beguilabel) 20 | * [beGUI.MultilineLabel](#beguimultilinelabel) 21 | * [beGUI.Url](#beguiurl) 22 | * [beGUI.InputBox](#beguiinputbox) 23 | * [beGUI.Picture](#beguipicture) 24 | * [beGUI.Button](#beguibutton) 25 | * [beGUI.PictureButton](#beguipicturebutton) 26 | * [beGUI.CheckBox](#beguicheckbox) 27 | * [beGUI.RadioBox](#beguiradiobox) 28 | * [beGUI.ComboBox](#beguicombobox) 29 | * [beGUI.NumberBox](#beguinumberbox) 30 | * [beGUI.ProgressBar](#beguiprogressbar) 31 | * [beGUI.Slide](#beguislide) 32 | * [beGUI.Group](#beguigroup) 33 | * [5. Container Widgets](#5-container-widgets) 34 | * [beGUI.List](#beguilist) 35 | * [beGUI.Draggable](#beguidraggable) 36 | * [beGUI.Droppable](#beguidroppable) 37 | * [beGUI.Tab](#beguitab) 38 | * [beGUI.Popup](#beguipopup) 39 | * [beGUI.MessageBox](#beguimessagebox) 40 | * [beGUI.QuestionBox](#beguiquestionbox) 41 | * [6. Custom Widget](#6-custom-widget) 42 | * [beGUI.Custom](#beguicustom) 43 | * [Writing Your Own Widget](#writing-your-own-widget) 44 | * [7. Theme](#7-theme) 45 | * [8. Tweening](#8-tweening) 46 | * [beGUI.Tween](#beguitween) 47 | * [License](#license) 48 | 49 | # Features 50 | 51 | ![beGUI 1](imgs/beGUI1.png) 52 | 53 | ![beGUI 2](imgs/beGUI2.png) 54 | 55 | "beGUI" implements: 56 | 57 | * Placable, resizable, anchorable, and nestable `Widget` 58 | * Textual `Label`, `MultilineLabel`, `Url`, `InputBox` 59 | * `Picture` 60 | * Clickable `Button`, `PictureButton` 61 | * `CheckBox`, `RadioBox` 62 | * `ComboBox` 63 | * `NumberBox` 64 | * `ProgressBar`, `Slide` 65 | * `Group` 66 | * Scrollable `List` 67 | * `Draggable` and `Droppable` 68 | * `Tab` 69 | * `Popup`, `MessageBox`, `QuestionBox` 70 | * `Custom` to make your own update function 71 | * And customizable by writing your own widget 72 | * Navigation by key (or custom method) 73 | 74 | Play live demo [in browser](https://paladin-t.github.io/begui/). 75 | 76 | # Setup 77 | 78 | beGUI is originally created to run within the [Bitty Engine](https://github.com/paladin-t/bitty/). The graphics primitives and input API is quite straightforward in Bitty Engine, it's possible to port it to other Lua-based environments with little twist, if that environment does `rect(...)`, `tex(...)`, `text(...)`, `mouse(...)`, etc. 79 | 80 | 1. Clone this repository or download from [releases](https://github.com/paladin-t/begui/releases) 81 | 2. Open "src" directly to run or copy everything under "src" to your own projects for [Bitty Engine](https://github.com/paladin-t/bitty/) 82 | 83 | # Reference 84 | 85 | ## 1. Principles 86 | 87 | beGUI implements a [retained mode](https://en.wikipedia.org/wiki/Retained_mode) GUI system, it separates behaviour and appearance into widget classes and theme preference. Widgets are organized in tree hierarchies, each widget can have none or one parent, and none or multiple children. There are two phases of the lib, first to construct the hierarchy, second to update it. It enumerates from root widget to it's descendents during update, beGUI will handle internal states like visibility, click event, etc. then draw it properly with the theme preference during this procedure. 88 | 89 | Most of the member functions return the widget object itself, it makes it easier to write internal DSL to construct full hierarchy in a tree style code. 90 | 91 | ```lua 92 | require 'libs/beGUI/beGUI' 93 | require 'libs/beGUI/beTheme' 94 | 95 | local widgets = nil 96 | local theme = nil 97 | 98 | function setup() 99 | local P = beGUI.percent -- Alias of percent. 100 | widgets = beGUI.Widget.new() 101 | :put(0, 0) 102 | :resize(P(100), P(100)) 103 | :addChild( 104 | beGUI.Label.new('beGUI demo') 105 | :setId('label') 106 | :anchor(0, 0) 107 | :put(10, 10) 108 | :resize(100, 23) 109 | ) 110 | :addChild( 111 | beGUI.Button.new('Button') 112 | :setId('button') 113 | :anchor(0, 0) 114 | :put(10, 36) 115 | :resize(100, 23) 116 | :on('clicked', function (sender) 117 | local lbl = widgets:find('label') 118 | lbl:setValue('Clicked ' .. tostring(sender)) 119 | end) 120 | ) 121 | theme = beTheme.default() 122 | end 123 | 124 | function update(delta) 125 | cls(Color.new(255, 255, 255)) 126 | 127 | font(theme['font'].resource) 128 | widgets:update(theme, delta) 129 | font(nil) 130 | end 131 | ``` 132 | 133 | Each widget has an anchor property which represents for the locating point in its local space, and a position property for either absolute or percentage position in its parent's space relatively. The final position is calculated according to these two properties. An anchor component is typically in range of values from 0.0 to 1.0, but it could be also less than 0.0 or greater than 1.0. A relative position component is typically in range of values from `Percent(0)` to `Percent(100)`, but it could be also less than `Percent(0)` or greater than `Percent(100)`. 134 | 135 | ![](imgs/docking_absolutely.png) 136 | 137 | ![](imgs/docking_relatively.png) 138 | 139 | Resources are splitted into nine grids evenly for flex scaled widgets. 140 | 141 | ![](imgs/grids.png) 142 | 143 | ## 2. Structures 144 | 145 |
146 | Structures 147 | 148 | These structures are used to help organizing widget layout. 149 | 150 | ### beStructures.Percent 151 | 152 | beStructures.`Percent` denotes relative value instead of absolute for positioning and sizing depending on its parent properties. 153 | 154 | **Model: `require 'libs/beGUI/beGUI_Structures'`** 155 | 156 | * beStructures.`Percent.new(amount)`: constructs a `Percent` object 157 | * `amount`: real number, no limit but often with range of values from 0 to 100 158 | * `p.__mul(num)`: multiply the `Percent` value with another number 159 | * `num`: the number to multiply 160 | * returns result number 161 | 162 | ### beGUI.percent 163 | 164 | Shortcut to create `Percent` object. 165 | 166 | **Model: `require 'libs/beGUI/beGUI'`** 167 | 168 | * beGUI.`percent(amount)`: constructs a `Percent` object 169 | * `amount`: real number, no limit but often with range of values from 0 to 100 170 | * returns `Percent` 171 | 172 |
173 | 174 | ## 3. Widget 175 | 176 |
177 | Widget 178 | 179 | ### beGUI.Widget 180 | 181 | **Model: `require 'libs/beGUI/beGUI'`** 182 | 183 | * beGUI.`Widget.new()`: constructs a `Widget` object 184 | 185 | * `widget:setId(id)`: sets the ID of the `Widget`; an ID is used to identify a `Widget` from others for accessing 186 | * `id`: ID string 187 | * returns `self` 188 | * `widget:get(...)`: gets a child `Widget` with the specific ID sequence 189 | * `...`: ID sequence of the full hierarchy path 190 | * returns the got `Widget` or `nil` 191 | * `widget:find(id)`: finds the first matched `Widget` with the specific ID 192 | * `id`: ID string at any level of the hierarchy path 193 | * returns the found `Widget` or `nil` 194 | * `widget:visible()`: gets the visibility of the `Widget` 195 | * returns boolean for visibility 196 | * `widget:setVisible(val)`: sets the visibility of the `Widget` 197 | * `val`: whether it's visible 198 | * returns `self` 199 | * `widget:capturable()`: gets the capturability of the `Widget` 200 | * returns boolean or string for capturability 201 | * `widget:setCapturable(val)`: sets the capturability of the `Widget` 202 | * `val`: `true`, `false` or `'children'` 203 | * returns `self` 204 | * `widget:anchor(x, y)`: sets the anchor of the `Widget`; anchor is used to calculate the offset when placing `Widget` 205 | * `x`: x position of the anchor in local space as number, typically [0.0, 1.0] for [left, right], but it could be also less than 0.0 or greater than 1.0 206 | * `y`: y position of the anchor in local space as number, typically [0.0, 1.0] for [top, bottom], but it could be also less than 0.0 or greater than 1.0 207 | * returns `self` 208 | * `widget:offset()`: gets the offset of the `Widget` 209 | * returns offset `x`, `y` in world space 210 | * `widget:put(x, y)`: sets the position of the `Widget` 211 | * `x`: number for absolute position; or `Percent` for relative position, typically with range of values from `Percent(0)` to `Percent(100)`, but it could be also less than `Percent(0)` or greater than `Percent(100)` 212 | * `y`: number for absolute position; or `Percent` for relative position, typically with range of values from `Percent(0)` to `Percent(100)`, but it could be also less than `Percent(0)` or greater than `Percent(100)` 213 | * returns `self` 214 | * `widget:position()`: gets the position of the `Widget` 215 | * returns position `x`, `y` in local space 216 | * `widget:worldPosition()`: gets the position of the `Widget` in world space 217 | * returns position `x`, `y` in world space 218 | * `widget:resize(width, height)`: sets the size of the `Widget` 219 | * `width`: number for absolute size; or `Percent` for relative size, typically with range of values from `Percent(0.00...n)` to `Percent(100)`, but it could be also greater than `Percent(100)` 220 | * `height`: number for absolute size; or `Percent` for relative size, typically with range of values from `Percent(0.00...n)` to `Percent(100)`, but it could be also greater than `Percent(100)` 221 | * returns `self` 222 | * `widget:size()`: gets the size of the `Widget` 223 | * returns size `width`, `height` 224 | * `widget:alpha()`: gets the alpha value of the `Widget` 225 | * returns transparency number, with range of values from 0 to 255 226 | * `widget:setAlpha(val)`: sets the alpha value of the `Widget` 227 | * `val`: number with range of value from 0 to 255, or nil for default (255) 228 | * returns `self` 229 | * `widget:getChild(idOrIndex)`: gets the child with the specific ID or index 230 | * `idOrIndex`: ID string, or index number 231 | * returns the found child or nil 232 | * `widget:insertChild(child, index)`: inserts a child before the specific index 233 | * `child`: the child `Widget` to insert 234 | * returns `self` 235 | * `widget:addChild(child)`: adds a child to the end of the children list 236 | * `child`: the child `Widget` to add 237 | * returns `self` 238 | * `widget:removeChild(childOrIdOrIndex)`: removes a child with the specific child, or its ID or index 239 | * `child`: child `Widget`, or its ID string, or index number 240 | * returns `self` 241 | * `widget:foreachChild(handler)`: iterates all children, and calls the specific handler 242 | * `handler`: the children handler in form of `function (child, index) end` 243 | * returns `self` 244 | * `widget:sortChildren(comp)`: sorts all children with the specific comparer 245 | * `comp`: the comparer in form of `function (left, right) end` 246 | * returns `self` 247 | * `widget:getChildrenCount()`: gets the count of all children 248 | * returns children count number 249 | * `widget:clearChildren()`: clears all children 250 | * returns `self` 251 | * `widget:openPopup(content)`: opens a popup 252 | * `content`: the popup to open 253 | * returns `self` 254 | * `widget:closePopup()`: closes any popup 255 | * returns `self` 256 | * `widget:update(theme, delta, event = nil)`: updates the `Widget` and its children recursively 257 | * `theme`: the theme to draw with 258 | * `delta`: elapsed time since previous update 259 | * `event`: omit it for common usage, pass a prefilled event to prevent default event 260 | 261 | * `widget:on(event, handler)`: registers the handler of the specific event 262 | * `event`: event name string 263 | * `handler`: callback function 264 | * returns `self` 265 | * `widget:off(event)`: unregisters the handlers of the specific event 266 | * `event`: event name string 267 | * returns `self` 268 | 269 | * `widget:navigatable()`: gets whether this `Widget` is navigatable 270 | * returns `'all'` for fully navigatable, `nil` for non-navigatable, `'children'` for children only, `'content'` for content only 271 | * `widget:navigate(dir)`: navigates through widgets, call this to perform key navigation, etc. 272 | * `dir`: can be one in `'prev'`, `'next'`, `'press'`, `'cancel'` 273 | * `widget:queriable()`: gets whether this `Widget` is queriable 274 | * returns `true` for queriable, otherwise `false` 275 | * `widget:setQueriable(val)`: sets whether this `Widget` is queriable 276 | * `val`: whether this `Widget` is queriable 277 | * returns `self` 278 | * `widget:query(x, y)`: queries a `Widget` at the specific position 279 | * `x`: the x position to query 280 | * `y`: the y position to query 281 | * returns the queried `Widget` or `nil` 282 | * `widget:captured()`: gets whether this `Widget` has captured mouse event. 283 | * returns `true` for captured, otherwise `false` 284 | * `widget:tween(t)`: schedules a tweening procedure 285 | * `t`: the tweening object 286 | * returns `self` 287 | * `widget:clearTweenings()`: clears all tweening procedures 288 | * returns `self` 289 | 290 |
291 | 292 | ## 4. Basic Widgets 293 | 294 |
295 | Basic Widgets 296 | 297 | ### beGUI.Label 298 | 299 | **Model: `require 'libs/beGUI/beGUI'`, implements beGUI.`Widget`** 300 | 301 | * beGUI.`Label.new(content, alignment = 'left', clip_ = false, theme = nil, shadow = nil)`: constructs a `Label` with the specific content 302 | * `content`: the content string 303 | * `alignment`: one in `nil`, `'left'`, `'right'`, `'center'` 304 | * `clip_`: whether to clip drawing outside this `Widget`'s bounds 305 | * `theme`: custom theme 306 | * `shadow`: shadow theme for shadowed drawing 307 | 308 | * `label:getValue()`: gets the content text 309 | * returns the content string 310 | * `label:setValue(val)`: sets the content text 311 | * `val`: the specific content string 312 | * returns `self` 313 | * `label:alignment()`: gets the alignment 314 | * returns the alignment preference string 315 | * `label:setAlignment(val)`: sets the alignment preference 316 | * `val`: the specific alignment preference string 317 | * returns `self` 318 | * `label:clipping()`: gets whether to clip drawing outside the `Widget`'s bounds 319 | * returns `true` for clipping, otherwise `false` 320 | * `label:setClipping(val)`: sets whether to clip drawing outside the `Widget`'s bounds 321 | * `val`: `true` to clip 322 | * returns `self` 323 | * `label:setTheme(theme, shadow = nil)`: sets the theme 324 | * `theme`: the custom theme 325 | * `shadow`: the custom shadow theme 326 | * returns `self` 327 | 328 | ### beGUI.MultilineLabel 329 | 330 | **Model: `require 'libs/beGUI/beGUI'`, implements beGUI.`Widget`** 331 | 332 | * beGUI.`MultilineLabel.new(content, lineHeight = nil, alighment = 'left')`: constructs a `MultilineLabel` with the specific content 333 | * `content`: the content string 334 | * `lineHeight`: the custom line height 335 | * `alignment`: one in `nil`, `'left'`, `'right'`, `'center'` 336 | 337 | * `multilinelabel:getValue()`: gets the content text 338 | * returns the content string 339 | * `multilinelabel:setValue(val)`: sets the content text 340 | * `val`: the specific content string 341 | * returns `self` 342 | * `multilinelabel:lineHeight()`: gets the line height 343 | * returns the line height 344 | * `multilinelabel:setLineHeight(val)`: sets the line height 345 | * `val`: the specific line height 346 | * returns `self` 347 | * `multilinelabel:alignment()`: gets the alignment 348 | * returns the alignment preference string 349 | * `multilinelabel:setAlignment(val)`: sets the alignment preference, the preference falls to `'left'` if had set flex width to `true` 350 | * `val`: the specific alignment preference string 351 | * returns `self` 352 | * `multilinelabel:setTheme(theme, widgetTheme)`: sets the theme 353 | * `theme`: the custom font theme 354 | * `widgetTheme`: the custom widget theme 355 | * returns `self` 356 | * `multilinelabel:flexWidth()`: gets whether to calculate `Widget` width automatically 357 | * returns `true` for calculating automatically, otherwise `false` 358 | * `multilinelabel:setFlexWidth(val)`: sets whether to calculate `Widget` width automatically 359 | * `val`: `true` to calculate automatically 360 | * returns `self` 361 | * `multilinelabel:flexHeight()`: gets whether to calculate `Widget` height automatically 362 | * returns `true` for calculating automatically, otherwise `false` 363 | * `multilinelabel:setFlexHeight(val)`: sets whether to calculate `Widget` height automatically 364 | * `val`: `true` to calculate automatically 365 | * returns `self` 366 | * `multilinelabel:pattern()`: gets the word split pattern 367 | * returns the word split pattern string 368 | * `multilinelabel:setPattern(val)`: sets the word split pattern 369 | * `val`: the specific word split pattern 370 | * returns `self` 371 | * `multilinelabel:translator()`: gets the word translator 372 | * returns the word translator 373 | * `multilinelabel:setTranslator(val)`: sets the word translator 374 | * `val`: the specific word translator 375 | * returns `self` 376 | 377 | ### beGUI.Url 378 | 379 | **Model: `require 'libs/beGUI/beGUI'`, implements beGUI.`Widget`** 380 | 381 | * beGUI.`Url.new(content, alighment = 'left', clip_ = false, theme = nil)`: constructs a `Url` with the specific content 382 | * `content`: the content string 383 | * `alignment`: one in `nil`, `'left'`, `'right'`, `'center'` 384 | * `clip_`: whether to clip drawing outside this `Widget`'s bounds 385 | * `theme`: custom theme 386 | 387 | * `url:getValue()`: gets the content text 388 | * returns the content string 389 | * `url:setValue(val)`: sets the content text 390 | * `val`: the specific content string 391 | * returns `self` 392 | * `url:alignment()`: gets the alignment 393 | * returns the alignment preference string 394 | * `url:setAlignment(val)`: sets the alignment preference 395 | * `val`: the specific alignment preference string 396 | * returns `self` 397 | * `url:clipping()`: gets whether to clip drawing outside the `Widget`'s bounds 398 | * returns `true` for clipping, otherwise `false` 399 | * `url:setClipping(val)`: sets whether to clip drawing outside the `Widget`'s bounds 400 | * `val`: `true` to clip 401 | * returns `self` 402 | * `url:setTheme(theme)`: sets the theme 403 | * `theme`: the custom theme 404 | * returns `self` 405 | 406 | * `url:on('clicked', function (sender) end)`: registers an event which will be triggered when the `Widget` has been clicked 407 | * returns `self` 408 | 409 | ### beGUI.InputBox 410 | 411 | **Model: `require 'libs/beGUI/beGUI'`, implements beGUI.`Widget`** 412 | 413 | * beGUI.`InputBox.new(content placeholder)`: constructs an InputBox with the specific content 414 | * `content`: the content string 415 | * `placeholder`: the placeholder string when there's no input yet 416 | 417 | * `inputbox:getValue()`: gets the content text 418 | * returns the content string 419 | * `inputbox:setValue(val)`: sets the content text 420 | * `val`: the specific content string 421 | * `inputbox:setTheme(theme, placeholderTheme)`: sets the theme 422 | * `theme`: the custom font theme 423 | * `placeholderTheme`: the custom placeholder theme 424 | * returns `self` 425 | * `inputbox:placeholder()`: gets the placeholder text 426 | * returns the placeholder string 427 | * `inputbox:setPlaceholder(val)`: sets the placeholder text 428 | * `val`: the specific placeholder string 429 | * returns `self` 430 | 431 | * `inputbox:on('changed', function (sender, value) end)`: registers an event which will be triggered when the `Widget` content text has been changed 432 | * returns `self` 433 | 434 | ### beGUI.Picture 435 | 436 | **Model: `require 'libs/beGUI/beGUI'`, implements beGUI.`Widget`** 437 | 438 | * beGUI.`Picture.new(content, stretched = false, permeation = false)`: constructs a Picture with the specific content 439 | * `content`: the content `Texture` 440 | * `stretched`: whether to use 9-grid-based splitting for stretching 441 | * `permeation`: whether to use permeation correction 442 | 443 | * `picture:setValue(content, stretched = false, permeation = false)`: sets the content `Texture` 444 | * `content`: the content `Texture` 445 | * returns `self` 446 | * `picture:stretched()`: gets whether to use 9-grid-based splitting for stretching 447 | * returns `true` for 9-grid-based splitting, otherwise `false` 448 | * `picture:setStretched(val)`: sets whether to use 9-grid-based splitting for stretching 449 | * `val`: `true` to use 9-grid-based splitting for stretching 450 | * returns `self` 451 | * `picture:permeation()`: gets whether to use permeation correction 452 | * returns `true` for permeation correction, otherwise `false` 453 | * `picture:setPermeation(val)`: sets whether to use permeation correction 454 | * `val`: `true` to use permeation correction 455 | * returns `self` 456 | * `picture:color()`: gets the mask color of the `Picture` 457 | * returns the mask color or `nil` 458 | * `picture:setColor(val)`: sets the mask color of the `Picture` 459 | * `val`: the specific mask color 460 | * returns `self` 461 | 462 | ### beGUI.Button 463 | 464 | **Model: `require 'libs/beGUI/beGUI'`, implements beGUI.`Widget`** 465 | 466 | * beGUI.`Button.new(content)`: constructs a Button with the specific content 467 | * `content`: the content string 468 | 469 | * `button:setValue(content)`: sets the content text 470 | * `val`: the specific content string 471 | * `button:setTheme(theme, themeNormal, themeDown, themeDisabled)`: sets the theme 472 | * `theme`: the custom font theme 473 | * `themeNormal`: the custom theme for normal state 474 | * `themeDown`: the custom theme for pressed state 475 | * `themeDisabled`: the custom theme for disabled state 476 | * returns `self` 477 | * `button:enabled()`: gets whether this `Widget` is enabled 478 | * returns `true` for enabled, otherwise `false` 479 | * `button:setEnabled(val)`: sets whether this `Widget` is enabled 480 | * `val`: `true` for enabled, otherwise `false` 481 | * returns `self` 482 | 483 | * `button:on('clicked', function (sender) end)`: registers an event which will be triggered when the `Widget` has been clicked 484 | * returns `self` 485 | 486 | ### beGUI.PictureButton 487 | 488 | **Model: `require 'libs/beGUI/beGUI'`, implements beGUI.`Widget`** 489 | 490 | * beGUI.`PictureButton.new(content, repeat_ = false, theme = nil, background = nil)`: constructs a `PictureButton` with the specific content 491 | * `content`: the content `Texture` 492 | * `repeat_`: whether to enable repeating event 493 | * `theme`: the custom theme 494 | * `background`: optional, the custom background `Texture` 495 | 496 | * `picturebutton:setTheme(theme, widgetTheme)`: sets the theme 497 | * `theme`: the custom font theme 498 | * `widgetTheme`: the custom widget theme 499 | * returns `self` 500 | * `picturebutton:enabled()`: gets whether this `Widget` is enabled 501 | * returns `true` for enabled, otherwise `false` 502 | * `picturebutton:setEnabled(val)`: sets whether this `Widget` is enabled 503 | * `val`: `true` for enabled, otherwise `false` 504 | * returns `self` 505 | 506 | * `picturebutton:on('clicked', function (sender) end)`: registers an event which will be triggered when the `Widget` has been clicked 507 | * returns `self` 508 | 509 | ### beGUI.CheckBox 510 | 511 | **Model: `require 'libs/beGUI/beGUI'`, implements beGUI.`Widget`** 512 | 513 | * beGUI.`CheckBox.new(content, value = false)`: constructs a `CheckBox` with the specific content 514 | * `content`: the content string 515 | * `value`: the initial checked state 516 | 517 | * `checkbox:getValue()`: gets whether this `Widget` is checked 518 | * returns `true` for checked, otherwise `false` 519 | * `checkbox:setValue(val)`: sets whether this `Widget` is checked 520 | * `val`: `true` for checked, otherwise `false` 521 | * returns `self` 522 | * `checkbox:setContent(val)`: sets the text content of this `Widget` 523 | * `val`: the content string 524 | * returns `self` 525 | * `checkbox:enabled()`: gets whether this `Widget` is enabled 526 | * returns `true` for enabled, otherwise `false` 527 | * `checkbox:setEnabled(val)`: sets whether this `Widget` is enabled 528 | * `val`: `true` for enabled, otherwise `false` 529 | * returns `self` 530 | 531 | * `checkbox:on('changed', function (sender, value) end)`: registers an event which will be triggered when the `Widget` checked state has been changed 532 | * returns `self` 533 | 534 | ### beGUI.RadioBox 535 | 536 | **Model: `require 'libs/beGUI/beGUI'`, implements beGUI.`Widget`** 537 | 538 | * beGUI.`RadioBox.new(content, value = false)`: constructs a `RadioBox` with the specific content 539 | * `content`: the content string 540 | * `value`: the initial checked state 541 | 542 | * `radiobox:getValue()`: gets whether this `Widget` is checked 543 | * returns `true` for checked, otherwise `false` 544 | * `radiobox:setValue(val)`: sets whether this `Widget` is checked; not recommended to call this manually 545 | * `val`: `true` for checked, otherwise `false` 546 | * `radiobox:setContent(val)`: sets the text content of this `Widget` 547 | * `val`: the content string 548 | * returns `self` 549 | 550 | * `radiobox:on('changed', function (sender, value) end)`: registers an event which will be triggered when the `Widget` checked state has been changed 551 | * returns `self` 552 | 553 | ### beGUI.ComboBox 554 | 555 | **Model: `require 'libs/beGUI/beGUI'`, implements beGUI.`Widget`** 556 | 557 | * beGUI.`ComboBox.new(content, value = nil)`: constructs a `ComboBox` with the specific content 558 | * `content`: list of string 559 | * `value`: the selected index number 560 | 561 | * `combobox:getItemAt(index)`: gets the item text at the specific index 562 | * `index`: the specific index to get 563 | * returns got item string or `nil` 564 | * `combobox:addItem(item)`: adds an item string with the specific content text 565 | * `item` the specific item text to add 566 | * returns `self` 567 | * `combobox:removeItemAt(index)`: removes the item at the specific index 568 | * `index` the specific index to remove 569 | * returns `true` for success, otherwise `false` 570 | * `combobox:clearItems()`: clears all items 571 | * returns `self` 572 | * `combobox:getValue()`: gets the selected index 573 | * returns the selected index number 574 | * `combobox:setValue(val)`: sets the selected index 575 | * `val`: the specific selected index 576 | * returns `self` 577 | * `combobox:scrollable()`: gets whether can scroll the widget by mouse wheel 578 | * returns `true` for scrollable, otherwise `false` 579 | * `combobox:setScrollable(val)`: sets whether can scroll the widget by mouse wheel 580 | * `val`: `true` for allowing scrolling with a mouse wheel, otherwise `false` 581 | * returns `self` 582 | 583 | * `combobox:on('changed', function (sender, value) end)`: registers an event which will be triggered when the `Widget` selection state has been changed 584 | * returns `self` 585 | 586 | ### beGUI.NumberBox 587 | 588 | **Model: `require 'libs/beGUI/beGUI'`, implements beGUI.`Widget`** 589 | 590 | * beGUI.`NumberBox.new(value, step, min = nil, max = nil, trim = nil, format = nil)`: constructs a `NumberBox` with the specific value 591 | * `value`: the initial value number 592 | * `step`: the changing step 593 | * `min`: the minumum limit 594 | * `max`: the maximum limit 595 | * `trim`: optional, used to trim before value setting 596 | * `format`: optional, used to format value for output 597 | 598 | * `numberbox:getValue()`: gets the value number 599 | * returns the value number 600 | * `numberbox:setValue(val)`: sets the value number 601 | * `val`: the specific value number 602 | * returns `self` 603 | * `numberbox:getMinValue()`: gets the minimum limit number 604 | * returns the minimum limit number 605 | * `numberbox:setMinValue(val)`: sets the minimum limit number 606 | * `val`: the specific minimum limit number 607 | * returns `self` 608 | * `numberbox:getMaxValue()`: gets the maximum limit number 609 | * returns the maximum limit number 610 | * `numberbox:setMaxValue(val)`: sets the maximum limit number 611 | * `val`: the specific maximum limit number 612 | * returns `self` 613 | * `numberbox:step()`: gets the changing step 614 | * returns the changing step 615 | * `numberbox:setStep(val)`: sets the changing step 616 | * `val`: the specific changing step number 617 | * returns `self` 618 | * `numberbox:trim()`: gets the trim function 619 | * returns the trim function 620 | * `numberbox:setTrim(val)`: sets the trim function 621 | * `val`: the specific trim function 622 | * returns `self` 623 | * `numberbox:format()`: gets the format function 624 | * returns the format function 625 | * `numberbox:setFormat(val)`: sets the format function 626 | * `val`: the specific format function 627 | * returns `self` 628 | * `numberbox:setValueTheme(val)`: sets the value theme 629 | * `val`: the specific theme 630 | * returns `self` 631 | * `numberbox:scrollable()`: gets whether can scroll the widget by mouse wheel 632 | * returns `true` for scrollable, otherwise `false` 633 | * `numberbox:setScrollable(val)`: sets whether can scroll the widget by mouse wheel 634 | * `val`: `true` for allowing scrolling with a mouse wheel, otherwise `false` 635 | * returns `self` 636 | 637 | * `numberbox:on('changed', function (sender, value) end)`: registers an event which will be triggered when the `Widget` value has been changed 638 | * returns `self` 639 | 640 | ### beGUI.ProgressBar 641 | 642 | **Model: `require 'libs/beGUI/beGUI'`, implements beGUI.`Widget`** 643 | 644 | * beGUI.`ProgressBar.new(max, color, increasing = 'right')`: constructs a `ProgressBar` 645 | * `max`: the maximum value 646 | * `color`: the color for the completed bar 647 | * `increasing`: indicates whether to increase from left to right, or reversed, one in `'left'`, `'right'` 648 | 649 | * `progressbar:getValue()`: gets the value number 650 | * returns the value number 651 | * `progressbar:setValue(val)`: sets the value number 652 | * `val`: the specific value number 653 | * returns `self` 654 | * `progressbar:getMaxValue()`: gets the maximum limit number 655 | * returns the maximum limit number 656 | * `progressbar:setMaxValue(val)`: sets the maximum limit number 657 | * `val`: the specific maximum limit number 658 | * returns `self` 659 | * `progressbar:getShadowValue()`: gets the shadow value number 660 | * returns the value number 661 | * `progressbar:setShadowValue(val)`: sets the shadow value number 662 | * `val`: the specific shadow value number 663 | * returns `self` 664 | * `progressbar:setTheme(theme)`: sets the theme 665 | * `theme`: the custom theme 666 | * returns `self` 667 | 668 | * `progressbar:on('changed', function (sender, value, maxValue, shadowValue) end)`: registers an event which will be triggered when the `Widget` value has been changed 669 | * returns `self` 670 | 671 | ### beGUI.Slide 672 | 673 | **Model: `require 'libs/beGUI/beGUI'`, implements beGUI.`Widget`** 674 | 675 | * beGUI.`Slide.new(value, min, max)`: constructs a `Slide` with the specific value 676 | * `value`: the initial value number 677 | * `min`: the minimum limit number 678 | * `max`: the maximum limit number 679 | 680 | * `slide:getValue()`: gets the value number 681 | * returns the value number 682 | * `slide:setValue(val)`: sets the value number 683 | * `val`: the specific value number 684 | * returns `self` 685 | * `slide:getMinValue()`: gets the minimum limit number 686 | * returns the minimum limit number 687 | * `slide:setMinValue(val)`: sets the minimum limit number 688 | * `val`: the specific minimum limit number 689 | * returns `self` 690 | * `slide:getMaxValue()`: gets the maximum limit number 691 | * returns the maximum limit number 692 | * `slide:setMaxValue(val)`: sets the maximum limit number 693 | * `val`: the specific maximum limit number 694 | * returns `self` 695 | * `slide:scrollable()`: gets whether can scroll the widget by mouse wheel 696 | * returns `true` for scrollable, otherwise `false` 697 | * `slide:setScrollable(val)`: sets whether can scroll the widget by mouse wheel 698 | * `val`: `true` for allowing scrolling with a mouse wheel, otherwise `false` 699 | * returns `self` 700 | 701 | * `slide:on('changed', function (sender, value) end)`: registers an event which will be triggered when the `Widget` value has been changed 702 | * returns `self` 703 | 704 | ### beGUI.Group 705 | 706 | **Model: `require 'libs/beGUI/beGUI'`, implements beGUI.`Widget`** 707 | 708 | * beGUI.`Group.new(content)`: constructs a `Group` 709 | * `content`: the content string 710 | 711 | * `group:getValue()`: gets the content text 712 | * returns the content string 713 | * `group:setValue(val)`: sets the content text 714 | * `val`: the specific content string 715 | * returns `self` 716 | 717 |
718 | 719 | ## 5. Container Widgets 720 | 721 |
722 | Container Widgets 723 | 724 | ### beGUI.List 725 | 726 | **Model: `require 'libs/beGUI/beGUI'`, implements beGUI.`Widget`** 727 | 728 | * beGUI.`List.new(withScrollBar = false)`: constructs a `List` 729 | * `withScrollBar`: whether to draw scroll bar(s) 730 | 731 | * `list:scrollableVertically()`: gets whether to allow scrolling vertically 732 | * returns `true` for allowing scrolling vertically, otherwise `false` 733 | * `list:setScrollableVertically(val)`: sets whether to allow scrolling vertically 734 | * `val`: `true` for allowing scrolling vertically, otherwise `false` 735 | * returns `self` 736 | * `list:scrollableHorizontally()`: gets whether to allow scrolling horizontally 737 | * returns `true` for allowing scrolling horizontally, otherwise `false` 738 | * `list:setScrollableHorizontally(val)`: sets whether to allow scrolling horizontally 739 | * `val`: `true` for allowing scrolling horizontally, otherwise `false` 740 | * returns `self` 741 | * `list:scrollSpeed()`: gets the scroll speed 742 | * returns the scroll speed 743 | * `list:setScrollSpeed(val)`: sets the scroll speed 744 | * `val`: the specific scroll speed 745 | * returns `self` 746 | * `list:setTheme(theme)`: sets the theme 747 | * `theme`: the custom theme 748 | * returns `self` 749 | * `list:scrollable()`: gets whether can scroll the widget by mouse wheel 750 | * returns `true` for scrollable, otherwise `false` 751 | * `list:setScrollable(val)`: sets whether can scroll the widget by mouse wheel 752 | * `val`: `true` for allowing scrolling with a mouse wheel, otherwise `false` 753 | * returns `self` 754 | 755 | ### beGUI.Draggable 756 | 757 | **Model: `require 'libs/beGUI/beGUI'`, implements beGUI.`Widget`** 758 | 759 | * beGUI.`Draggable.new()`: constructs a `Draggable` 760 | 761 | ### beGUI.Droppable 762 | 763 | **Model: `require 'libs/beGUI/beGUI'`, implements beGUI.`Widget`** 764 | 765 | * beGUI.`Droppable.new()`: constructs a `Droppable` 766 | 767 | * `droppable:on('entered', function (sender, draggable) end)`: registers an event which will be triggered when the `Widget` has been entered by a `Draggable` 768 | * returns `self` 769 | * `droppable:on('left', function (sender, draggable) end)`: registers an event which will be triggered when the `Widget` has been left by a `Draggable` 770 | * returns `self` 771 | * `droppable:on('dropping', function (sender, draggable) return droppable end)`: registers an event which will be triggered when the `Widget` has been hovering by a `Draggable` 772 | * returns `self` 773 | * `droppable:on('dropped', function (sender, draggable) end)`: registers an event which will be triggered when the `Widget` has been dropped by a `Draggable` 774 | * returns `self` 775 | * `droppable:on('clicked', function (sender) end)`: registers an event which will be triggered when the `Widget` has been clicked 776 | * returns `self` 777 | 778 | ### beGUI.Tab 779 | 780 | **Model: `require 'libs/beGUI/beGUI'`, implements beGUI.`Widget`** 781 | 782 | * beGUI.`Tab.new()`: constructs a `Tab` 783 | 784 | * `tab:add(title)`: adds a `Tab` page with the specific title 785 | * `title`: the `Tab` page title to add 786 | * returns `self` 787 | * `tab:count()`: gets the page count of the `Tab` `Widget` 788 | * returns the `Tab` page count 789 | * `tab:getValue()`: gets the active page index 790 | * returns the active page index number 791 | * `tab:setValue(val)`: sets the active page index 792 | * `val`: the specific page index 793 | * returns `self` 794 | * `tab:tabSize()`: gets the specified `Tab` size 795 | * returns the specified `Tab` size 796 | * `tab:setTabSize(val)`: sets the specified `Tab` size 797 | * `val`: the specified `Tab` size 798 | * returns `self` 799 | * `tab:scrollable()`: gets whether can scroll the widget by mouse wheel 800 | * returns `true` for scrollable, otherwise `false` 801 | * `tab:setScrollable(val)`: sets whether can scroll the widget by mouse wheel 802 | * `val`: `true` for allowing scrolling with a mouse wheel, otherwise `false` 803 | * returns `self` 804 | 805 | * `tab:on('changed', function (sender, value) end)`: registers an event which will be triggered when the `Widget` page has been switched 806 | * returns `self` 807 | 808 | ### beGUI.Popup 809 | 810 | **Model: `require 'libs/beGUI/beGUI'`, implements beGUI.`Widget`** 811 | 812 | * beGUI.`Popup.new()`: constructs a `Popup` 813 | 814 | ### beGUI.MessageBox 815 | 816 | **Model: `require 'libs/beGUI/beGUI'`, implements beGUI.`Popup`** 817 | 818 | * beGUI.`MessageBox.new(closable, title, message, confirm = 'OK')`: constructs a `MessageBox` 819 | * `closable`: `true` to enable the close button, `false` to disable 820 | * `title`: the title text 821 | * `message`: the message text 822 | * `confirm`: the text for the confirm button 823 | 824 | * `messagebox:on('canceled', function (sender) end)`: registers an event which will be triggered when the `Popup` has been canceled 825 | * returns `self` 826 | * `messagebox:on('confirmed', function (sender) end)`: registers an event which will be triggered when the `Popup` has been confirmed 827 | * returns `self` 828 | 829 | ### beGUI.QuestionBox 830 | 831 | **Model: `require 'libs/beGUI/beGUI'`, implements beGUI.`Popup`** 832 | 833 | * beGUI.`QuestionBox.new(closable, title, messsage, confirm, deny)`: constructs a `QuestionBox` 834 | * `closable`: `true` to enable the close button, `false` to disable 835 | * `title`: the title text 836 | * `message`: the message text 837 | * `confirm`: the text for the confirm button 838 | * `deny`: the text for the deny button 839 | 840 | * `questionbox:on('canceled', function (sender) end)`: registers an event which will be triggered when the `Popup` has been canceled 841 | * returns `self` 842 | * `questionbox:on('confirmed', function (sender) end)`: registers an event which will be triggered when the `Popup` has been confirmed 843 | * returns `self` 844 | * `questionbox:on('denied', function (sender) end)`: registers an event which will be triggered when the `Popup` has been denied 845 | * returns `self` 846 | 847 |
848 | 849 | ## 6. Custom Widget 850 | 851 |
852 | Custom Widget 853 | 854 | There are two ways to customize your own `Widget`, one is to use the beWidget.`Custom` `Widget`, the other is to write your own `Widget` class. 855 | 856 | ### beGUI.Custom 857 | 858 | The `Custom` `Widget` exposes a `'updated'` event to let you write short customized update routine in the callback. 859 | 860 | **Model: `require 'libs/beGUI/beGUI'`, implements beGUI.`Widget`** 861 | 862 | * beGUI.`Custom.new(name = 'Custom')`: constructs a `Custom` `Widget` 863 | * `name`: the custom `Widget` name used to perform `__tostring` 864 | 865 | * `custom:name()`: gets the custom `Widget` name 866 | * returns the custom `Widget` name 867 | * `custom:setName(val)`: sets the custom `Widget` name 868 | * `val`: the specific custom `Widget` name 869 | * returns `self` 870 | 871 | * `custom:on('updated', function (sender, x, y, w, h, delta, event) end)`: registers an event which will be triggered when the `Widget` has been updated per frame 872 | * returns `self` 873 | 874 | ### Writing Your Own Widget 875 | 876 | You can also write your own `Widget` inheriting from beWidget.`Widget`. 877 | 878 | ```lua 879 | local beClass = require 'libs/beGUI/beClass' 880 | local beUtils = require 'libs/beGUI/beGUI_Utils' 881 | local beWidget = require 'libs/beGUI/beGUI_Widget' 882 | 883 | local MyWidget = beClass.class({ 884 | _value = nil, -- Define your fields. 885 | _pressed = false, 886 | 887 | ctor = function (self, ...) 888 | beWidget.Widget.ctor(self) 889 | 890 | -- Customize your constructor. 891 | end, 892 | 893 | __tostring = function (self) 894 | return 'MyWidget' 895 | end, 896 | 897 | getValue = function (self) -- Define your properties. 898 | return self._value 899 | end, 900 | setValue = function (self, val) 901 | if self._value == val then 902 | return self 903 | end 904 | self._value = val 905 | self:_trigger('changed', self, self._value) 906 | 907 | return self 908 | end, 909 | 910 | _update = function (self, theme, delta, dx, dy, event) 911 | -- Ignore if invisible. 912 | if not self.visibility then 913 | return 914 | end 915 | 916 | -- Get the offset x, y, which is calculated by this widget's size and its anchor. 917 | local ox, oy = self:offset() 918 | -- Get the position x, y in local space. 919 | local px, py = self:position() 920 | -- Calculate the final position with the delta position, offset and local position, 921 | -- where the delta position (`dx`, `dy`) is from this widget's parent, or 0, 0 for root widget. 922 | local x, y = dx + px + ox, dy + py + oy 923 | -- Get the size width, height of this widget. 924 | local w, h = self:size() 925 | 926 | -- The following code detects clicking. 927 | local down = false 928 | if event.context.active and event.context.active ~= self then 929 | self._pressed = false 930 | elseif event.canceled or event.context.dragging then 931 | event.context.active = nil 932 | self._pressed = false 933 | elseif self._pressed then 934 | down = event.mouseDown 935 | else 936 | -- Intersection detection. 937 | down = event.mouseDown and Math.intersects(event.mousePosition, Rect.byXYWH(x, y, w, h)) 938 | end 939 | if down and not self._pressed then 940 | event.context.active = self 941 | self._pressed = true 942 | elseif not down and self._pressed then 943 | event.context.active = nil 944 | self._pressed = false 945 | event.context.focus = self 946 | self:_trigger('clicked', self) -- Trigger 'clicked' event by clicking. 947 | elseif event.context.focus == self and event.context.navigated == 'press' then 948 | self:_trigger('clicked', self) -- Trigger 'clicked' event by key navigation. 949 | event.context.navigated = false 950 | end 951 | 952 | -- Draw the widget. 953 | local elem = down and theme['button_down'] or theme['button'] -- Using the button theme. 954 | beUtils.tex9Grid(elem, x, y, w, h, nil, self.transparency, nil) -- Draw texture. 955 | beUtils.textCenter(self._value, theme['font'], x, y, w, h, elem.content_offset, self.transparency) -- Draw text. 956 | 957 | -- Call base update to update its children. 958 | beWidget.Widget._update(self, theme, delta, dx, dy, event) 959 | end 960 | }, beWidget.Widget) 961 | ``` 962 | 963 |
964 | 965 | ## 7. Theme 966 | 967 |
968 | Theme 969 | 970 | Defined in "src/libs/beGUI/beTheme.lua". Widget classes will lookup for image resources, client area, content offset, fonts, colors and all other appearance preferences from it. 971 | 972 |
973 | 974 | ## 8. Tweening 975 | 976 |
977 | Tweening 978 | 979 | beGUI is integrated with a tweening lib adapted from [kikito/tween.lua](https://github.com/kikito/tween.lua), which allows to create tweening animations. 980 | 981 | ### beGUI.Tween 982 | 983 | **Model: `require 'libs/beGUI/beGUI'`** 984 | 985 | * beGUI.`Tween.new(duration, subject, target, easing, loop)`: constructs a `Tween` object 986 | * `duration`: the duration in seconds 987 | * `subject`: the tweening subject 988 | * `target`: the tweening target 989 | * `easing`: the easing function 990 | * `loop`: whether to loop the tweening 991 | 992 | * `tween:reset()`: resets the `Tween` object 993 | * returns `self` 994 | * `tween:set(clock)`: sets the `Tween` object to a specific clock point 995 | * `clock`: the click time point 996 | * returns `true` for success, otherwise `false` 997 | * `tween:update(delta)`: updates the `Tween` object with a specific delta time in seconds 998 | * returns `true` for success, otherwise `false` 999 | 1000 | * `tween:on('changed', function (sender) end)`: registers an event which will be triggered when the `Tween` has been updated 1001 | * returns `self` 1002 | * `tween:on('completed', function (sender) end)`: registers an event which will be triggered when the `Tween` has completed or looped 1003 | * returns `self` 1004 | * `tween:off(event)`: unregisters the handlers of the specific event 1005 | * `event`: event name string 1006 | * returns `self` 1007 | 1008 |
1009 | 1010 | # License 1011 | 1012 | beGUI is distributed under the MIT license. 1013 | --------------------------------------------------------------------------------