-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathruu.lua
416 lines (368 loc) · 12.3 KB
/
ruu.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
local _basePath = (...):gsub("ruu$", "")
local Class = require(_basePath .. "base-class")
local defaultThemes = require(_basePath .. "defaultThemes")
local util = require(_basePath .. "ruutilities")
local Ruu = Class:extend()
local Button = require(_basePath .. "widgets.Button")
local ToggleButton = require(_basePath .. "widgets.ToggleButton")
local RadioButton = require(_basePath .. "widgets.RadioButton")
local Slider = require(_basePath .. "widgets.Slider")
local InputField = require(_basePath .. "widgets.InputField")
local Panel = require(_basePath .. "widgets.Panel")
Ruu.MOUSE_MOVED = "mouse moved"
Ruu.CLICK = "click"
Ruu.ENTER = "enter"
Ruu.TEXT = "text"
Ruu.DELETE = "delete"
Ruu.BACKSPACE = "backspace"
Ruu.CANCEL = "cancel"
Ruu.NAV_DIRS = {
["up"] = "up", ["down"] = "down", ["left"] = "left", ["right"] = "right",
["next"] = "next", ["prev"] = "prev"
}
Ruu.SCROLL = "scroll"
Ruu.END = "end"
Ruu.HOME = "home"
Ruu.SELECTION_MODIFIER = "selection modifier"
local IS_KEYBOARD = true
local IS_NOT_KEYBOARD = false
Ruu.isHoverAction = { [Ruu.MOUSE_MOVED] = true, [Ruu.SCROLL] = true }
Ruu.layerPrecision = 10000 -- Number of different nodes allowed in each layer.
-- Layer index multiplied by this in getDrawIndex() calculation.
local function addWidget(self, widget)
self.allWgts[widget] = true
self.enabledWgts[widget] = true
end
function Ruu.hasWidget(self, widget)
return self.allWgts[widget]
end
function Ruu.Button(self, themeData, releaseFn, theme)
local wgt = Button(self, themeData, releaseFn, theme or self.themes.Button)
addWidget(self, wgt)
return wgt
end
function Ruu.ToggleButton(self, themeData, releaseFn, isChecked, theme)
local wgt = ToggleButton(self, themeData, releaseFn, isChecked, theme or self.themes.ToggleButton)
addWidget(self, wgt)
return wgt
end
function Ruu.RadioButton(self, themeData, releaseFn, isChecked, theme)
local wgt = RadioButton(self, themeData, releaseFn, isChecked, theme or self.themes.RadioButton)
addWidget(self, wgt)
return wgt
end
function Ruu.groupRadioButtons(self, widgets)
RadioButton.setGroup(widgets)
end
function Ruu.Slider(self, themeData, releaseFn, fraction, length, theme)
local wgt = Slider(self, themeData, releaseFn, fraction, length, theme or self.themes.Slider)
addWidget(self, wgt)
return wgt
end
function Ruu.InputField(self, themeData, confirmFn, text, theme)
local wgt = InputField(self, themeData, confirmFn, text, theme or self.themes.InputField)
addWidget(self, wgt)
return wgt
end
function Ruu.Panel(self, themeData, theme)
local wgt = Panel(self, themeData, theme or self.themes.Panel)
addWidget(self, wgt)
return wgt
end
local function contains(list, item)
for i=1,#list do
if list[i] == item then
return i
end
end
end
local function clear(list)
for i=#list,1,-1 do
list[i] = nil
end
end
-- Get a - b.
-- AKA: Get the list of items that are -only- in `a`, -not- in `b`.
local function getSubtraction(a, b)
local has = {}
for i=1,#a do has[ a[i] ] = true end
for i=1,#b do has[ b[i] ] = nil end
local onlyInA = {}
for v,_ in pairs(has) do
table.insert(onlyInA, v)
end
return onlyInA
end
function Ruu.setEnabled(self, widget, enabled)
self.enabledWgts[widget] = enabled or nil
widget.isEnabled = enabled
if not enabled then
if self.dragsOnWgt[widget] then self:stopDraggingWidget(widget) end
if contains(self.hoveredWgts, widget) then
self:mouseMoved(self.mx, self.my, 0, 0)
end
if contains(self.focusedWgts, widget) then
local topFocused = self.focusedWgts[1]
if widget == topFocused then
self:setFocus(nil) -- If we disable the top focused widget, remove focus.
else
self:setFocus(topFocused) -- Otherwise just refresh the focused widget stack.
end
end
if widget.isPressed then widget:release(1, true) end
end
end
function Ruu.destroy(self, widget)
if not self.allWgts[widget] then
local t = type(widget)
if t ~= "table" then error("Ruu.destroy - Requires a widget object, not '" .. tostring(widget) .. "' of type '" .. t .. "'.")
else error("Ruu.destroy - Widget not found " .. tostring(widget)) end
end
self.setEnabled(self, widget, false)
self.allWgts[widget] = nil
if widget.final then widget:final() end
end
function Ruu.setFocus(self, widget, isKeyboard)
-- We don't know what changed, so just unfocus all the old ones.
self:bubble(self.focusedWgts, "unfocus", isKeyboard)
clear(self.focusedWgts)
if widget then
self.focusedWgts[1] = widget
self.themeEssentials.getAncestorPanels(widget, self.focusedWgts)
self:bubble(self.focusedWgts, "focus", isKeyboard)
end
end
local function loopedIndex(list, index)
return (index - 1) % #list + 1
end
local function findNextInMap(self, map, x, y, axis, dir)
local foundWidget = nil
while not foundWidget do
if axis == "y" then
y = loopedIndex(map, y + dir)
elseif axis == "x" then
x = loopedIndex(map[y], x + dir)
end
foundWidget = map[y][x]
if foundWidget == self then break end
end
return foundWidget ~= self and foundWidget or nil
end
-- WARNING: EMPTY CELLS IN MAP MUST BE `FALSE`, not `NIL`!
function Ruu.mapNeighbors(self, map)
for y,row in ipairs(map) do
for x,widget in ipairs(row) do
if widget then -- Skip empty cells.
-- Up and Down
if #map > 1 then
widget.neighbor.up = findNextInMap(widget, map, x, y, "y", -1)
widget.neighbor.down = findNextInMap(widget, map, x, y, "y", 1)
end
-- Left and Right
if #row > 1 then
widget.neighbor.left = findNextInMap(widget, map, x, y, "x", -1)
widget.neighbor.right = findNextInMap(widget, map, x, y, "x", 1)
end
end
end
end
end
-- Shortcut for mapping vertically with a simple list.
function Ruu.mapVerticalNeighbors(self, list)
local lastI = #list
local first, last = list[1], list[lastI]
for i=1,lastI do
local wgt = list[i]
wgt.neighbor.up = list[i-1] or last
wgt.neighbor.down = list[i+1] or first
end
end
function Ruu.mapNextPrev(self, map)
if #map <= 1 then return end
map = { map } -- Make into a 2D array so findNextInMap just works.
for i,widget in ipairs(map[1]) do
if widget then
widget.neighbor.next = findNextInMap(widget, map, i, 1, "x", 1)
widget.neighbor.prev = findNextInMap(widget, map, i, 1, "x", -1)
end
end
end
function Ruu.startDrag(self, widget, dragType)
if widget.drag then
-- Keep track of whether or not we're dragging a widget as well as the number of different
-- drags (generally only 1), so we can know when there it's no longer being dragged.
local dragsOnWgt = (self.dragsOnWgt[widget] or 0) + 1
self.dragsOnWgt[widget] = dragsOnWgt
local drag = { widget = widget, type = dragType }
table.insert(self.drags, drag)
end
end
local function removeDrag(self, index)
local drag = self.drags[index]
table.remove(self.drags, index)
local dragsOnWgt = self.dragsOnWgt[drag.widget] - 1
dragsOnWgt = dragsOnWgt > 0 and dragsOnWgt or nil
self.dragsOnWgt[drag.widget] = dragsOnWgt
end
function Ruu.stopDrag(self, dragType)
for i=#self.drags,1,-1 do
if self.drags[i].type == dragType then
removeDrag(self, i)
end
end
end
function Ruu.stopDraggingWidget(self, widget)
for i=#self.drags,1,-1 do
if self.drags[i].widget == widget then
removeDrag(self, i)
end
end
end
function Ruu.mouseMoved(self, x, y, dx, dy)
self.mx, self.my = x, y
local isDragging = self.drags[1]
if isDragging then
for i,drag in ipairs(self.drags) do
drag.widget:drag(dx, dy, drag.type)
end
-- Don't update hover while dragging.
-- Still Check collision for drag-and-drop.
--[[
local hoveredWidgets = {}
for widget,_ in pairs(self.enabledWgts) do
if self.themeEssentials.hitsPoint(widget, x, y) then
table.insert(hoveredWidgets, widget)
end
end
if hoveredWidgets[1] then
util.sortByDepth(hoveredWidgets, self.layerDepths)
self:bubble(hoveredWidgets, "dragOver") -- TODO: change bubble to take a list.
-- TODO: Bubble an event for each drag.
end
--]]
else -- Not dragging.
local newHovered
for widget,_ in pairs(self.enabledWgts) do
if self.themeEssentials.hitsPoint(widget, x, y) then
newHovered = newHovered or {}
table.insert(newHovered, widget)
end
end
if self.hoveredWgts[1] then
if newHovered then
local wgtsToUnhover = getSubtraction(self.hoveredWgts, newHovered)
util.sortByDepth(wgtsToUnhover, self.layerDepths)
self:bubble(wgtsToUnhover, "unhover")
else
self:bubble(self.hoveredWgts, "unhover")
clear(self.hoveredWgts)
end
end
if newHovered then
self.hoveredWgts = newHovered
util.sortByDepth(self.hoveredWgts, self.layerDepths)
-- Will get repeat "hover" events, but NOT repeat "unhover" events.
self:bubble(self.hoveredWgts, "hover")
end
end
end
function Ruu.bubble(self, wgtList, fnName, ...)
-- local wgtList = isHoverAction and self.hoveredWgts or self.focusedWgts
for depth,wgt in ipairs(wgtList) do
if wgt[fnName] then
local r = wgt[fnName](wgt, depth, ...)
if r then return r end
end
end
end
function Ruu.input(self, action, value, change, rawChange, isRepeat, x, y, dx, dy, isTouch, presses)
if action == self.MOUSE_MOVED then
self:mouseMoved(x, y, dx, dy)
elseif action == self.CLICK then
if change == 1 then
self:setFocus(self.hoveredWgts[1], IS_NOT_KEYBOARD)
local r = self:bubble(self.hoveredWgts, "press", self.mx, self.my, IS_NOT_KEYBOARD)
if r then return r end
elseif change == -1 then
local r = self:bubble(self.hoveredWgts, "release", false, self.mx, self.my, IS_NOT_KEYBOARD)
if r then return r end
end
elseif action == self.ENTER then
if change == 1 then
local r = self:bubble(self.focusedWgts, "press", nil, nil, IS_KEYBOARD)
if r then return r end
elseif change == -1 then
local r = self:bubble(self.focusedWgts, "release", false, nil, nil, IS_KEYBOARD)
if r then return r end
end
elseif self.NAV_DIRS[action] and (change == 1 or isRepeat) then
if self.focusedWgts[1] then
local dirStr = self.NAV_DIRS[action]
local neighbor = self:bubble(self.focusedWgts, "getFocusNeighbor", dirStr)
if neighbor == true then -- No neighbor, but used input.
return true
elseif neighbor then
self:setFocus(neighbor, IS_KEYBOARD)
return true
end
end
elseif action == self.TEXT then
local r = self:bubble(self.focusedWgts, "textInput", value)
if r then return r end
elseif action == self.SCROLL and change == 1 then
local r = self:bubble(self.hoveredWgts, "scroll", dx, dy)
if r then return r end
elseif action == self.BACKSPACE and (change == 1 or isRepeat) then
local r = self:bubble(self.focusedWgts, "backspace")
if r then return r end
elseif action == self.DELETE and (change == 1 or isRepeat) then
local r = self:bubble(self.focusedWgts, "delete")
if r then return r end
elseif action == self.HOME and change == 1 then
local r = self:bubble(self.focusedWgts, "home")
if r then return r end
elseif action == self.END and change == 1 then
local r = self:bubble(self.focusedWgts, "end")
if r then return r end
elseif action == self.CANCEL and change == 1 then
local r = self:bubble(self.focusedWgts, "cancel")
if r then return r end
end
-- Pass on any unused input to hovered or focused widgets for custom uses.
local isHoverAction = self.isHoverAction[action]
local wgtList = isHoverAction and self.hoveredWgts or self.focusedWgts
local r = self:bubble(wgtList, "ruuInput", action, value, change, rawChange, isRepeat, x, y, dx, dy, isTouch, presses)
if r then return r end
end
function Ruu.isSelectionModifierPressed(self)
return Input.isPressed(self.SELECTION_MODIFIER)
end
local function registerLayer(layer, map, i)
if type(layer) ~= "string" then
error("Ruu.registerLayers() - Invalid layer '" .. tostring(layer) .. "'. Must be a string.")
end
map[layer] = i * Ruu.layerPrecision
end
function Ruu.registerLayers(self, layerList, isTopToBottom)
self.layerDepths = {}
local first, last, incr = 1,#layerList,1
if isTopToBottom then first, last, incr = last, first, -1 end
local height=0
for i=first,last,incr do
height = height + 1
registerLayer(layerList[i], self.layerDepths, height)
end
end
function Ruu.set(self, themes, themeEssentials)
self.allWgts = {}
self.enabledWgts = {}
self.hoveredWgts = {}
self.focusedWgts = {}
self.themes = themes or defaultThemes
self.themeEssentials = themeEssentials or self.themes.essentials
self.mx, self.my = 0, 0
self.layerDepths = {}
self.drags = {}
self.dragsOnWgt = {} -- A dictionary of currently dragged widgets, with the number of active drags on each (in case of custom drags).
end
return Ruu