-
Notifications
You must be signed in to change notification settings - Fork 223
Expand file tree
/
Copy pathOverflow.lua
More file actions
447 lines (405 loc) · 15.5 KB
/
Copy pathOverflow.lua
File metadata and controls
447 lines (405 loc) · 15.5 KB
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
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
-- When designing your game for many devices and screen sizes, icons may occasionally
-- particularly for smaller devices like phones, overlap with other icons or the bounds
-- of the screen. The overflow handler solves this challenge by moving the out-of-bounds
-- icon into an overflow menu (with a limited scrolling canvas) preventing overlaps occuring
-- LOCAL
local Overflow = {}
local holders = {}
local orderedAvailableIcons = {}
local iconsDict
local currentCamera = workspace.CurrentCamera
local overflowIcons = {}
local overflowIconUIDs = {}
local Utility = require(script.Parent.Parent.Utility)
local beginCheckingCenterIcons = false
local beganSecondaryCenterCheck = false
local Icon
-- FUNCTIONS
-- This is called upon the Icon initializing
function Overflow.start(incomingIcon)
Icon = incomingIcon
iconsDict = Icon.iconsDictionary
local primaryScreenGui
for _, screenGui in pairs(Icon.container) do
if primaryScreenGui == nil and screenGui.ScreenInsets == Enum.ScreenInsets.TopbarSafeInsets then
primaryScreenGui = screenGui
end
for _, holder in pairs(screenGui.Holders:GetChildren()) do
if holder:GetAttribute("IsAHolder") then
holders[holder.Name] = holder
end
end
end
-- We listen for changes in icons (such as them being added, removed,
-- the setting of a different alignment, the widget size changing, etc)
local beginOverflow = false
local updateBoundaries = Utility.createStagger(0.1, function(ignoreAvailable)
if not beginOverflow then
return
end
if not ignoreAvailable then
Overflow.updateAvailableIcons("Center")
end
Overflow.updateBoundary("Left")
Overflow.updateBoundary("Right")
end)
task.delay(0.5, function()
beginOverflow = true
updateBoundaries()
end)
task.delay(2, function()
-- This is essential to prevent central icons begin added
-- left or right due to incomplete UIListLayout calculations
-- within the first few frames
beginCheckingCenterIcons = true
updateBoundaries()
end)
Icon.iconAdded:Connect(updateBoundaries)
Icon.iconRemoved:Connect(updateBoundaries)
Icon.iconChanged:Connect(updateBoundaries)
currentCamera:GetPropertyChangedSignal("ViewportSize"):Connect(function()
updateBoundaries(true)
end)
primaryScreenGui:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
updateBoundaries(true)
end)
end
function Overflow.getWidth(icon, getMaxWidth)
local widget = icon.widget
return widget:GetAttribute("TargetWidth") or widget.AbsoluteSize.X
end
function Overflow.getAvailableIcons(alignment)
local ourOrderedIcons = orderedAvailableIcons[alignment]
if not ourOrderedIcons then
ourOrderedIcons = Overflow.updateAvailableIcons(alignment)
end
return ourOrderedIcons
end
function Overflow.updateAvailableIcons(alignment)
-- We only track items that are directly on the topbar (i.e. not within a parent icon)
local ourTotal = 0
local ourOrderedIcons = {}
for _, icon in pairs(iconsDict) do
local parentUID = icon.parentIconUID
local isDirectlyOnTopbar = not parentUID or overflowIconUIDs[parentUID]
local isOverflow = overflowIconUIDs[icon.UID]
if isDirectlyOnTopbar and icon.alignment == alignment and not isOverflow and icon.isEnabled then
table.insert(ourOrderedIcons, icon)
ourTotal += 1
end
end
-- Ignore if no icons are available
if ourTotal <= 0 then
return {}
end
-- This sorts these icons by smallest order, or if equal, left-most position
-- (even for the right alignment because all icons are sorted left-to-right)
table.sort(ourOrderedIcons, function(iconA, iconB)
local orderA = iconA.widget.LayoutOrder
local orderB = iconB.widget.LayoutOrder
local hasParentA = iconA.parentIconUID
local hasParentB = iconB.parentIconUID
if hasParentA == hasParentB then
if orderA < orderB then
return true
end
if orderA > orderB then
return false
end
return iconA.widget.AbsolutePosition.X < iconB.widget.AbsolutePosition.X
elseif hasParentB then
return false
elseif hasParentA then
return true
end
return nil
end)
-- Finish up
orderedAvailableIcons[alignment] = ourOrderedIcons
return ourOrderedIcons
end
function Overflow.getRealXPositions(alignment, orderedIcons)
-- We calculate the the absolute position of icons instead of reading
-- directly to determine where they would be if not within an overflow
local isLeft = alignment == "Left"
local holder = holders[alignment]
local holderXPos = holder.AbsolutePosition.X
local holderXSize = holder.AbsoluteSize.X
local holderUIList = holder.UIListLayout
local topbarInset = holderUIList.Padding.Offset
local absoluteX = (isLeft and holderXPos) or holderXPos + holderXSize
local realXPositions = {}
if isLeft then
Utility.reverseTable(orderedIcons)
end
for i = #orderedIcons, 1, -1 do
local icon = orderedIcons[i]
local sizeX = Overflow.getWidth(icon)
if not isLeft then
absoluteX -= sizeX
end
realXPositions[icon.UID] = absoluteX
if isLeft then
absoluteX += sizeX
end
absoluteX += (isLeft and topbarInset) or -topbarInset
end
return realXPositions
end
function Overflow.updateBoundary(alignment)
-- We only track items that are directly on the topbar (i.e. not within a parent icon) or within an overflow
local holder = holders[alignment]
local holderUIList = holder.UIListLayout
local holderXPos = holder.AbsolutePosition.X
local holderXSize = holder.AbsoluteSize.X
local topbarInset = holderUIList.Padding.Offset
local topbarPadding = holderUIList.Padding.Offset
local BOUNDARY_GAP = topbarInset
local ourOrderedIcons = Overflow.updateAvailableIcons(alignment)
local boundWidth = 0
local ourTotal = 0
for _, icon in pairs(ourOrderedIcons) do
boundWidth += Overflow.getWidth(icon) + topbarPadding
ourTotal += 1
end
if ourTotal <= 0 then
return
end
-- These are the icons with menus which icons will be moved into
-- when overflowing
local isCentral = alignment == "Center"
local isLeft = alignment == "Left"
local isRight = not isLeft
local overflowIcon = overflowIcons[alignment]
if not overflowIcon and not isCentral and #ourOrderedIcons > 0 then
local order = (isLeft and -9999999) or 9999999
overflowIcon = Icon.new()--:setLabel(`{alignment}`)
overflowIcon:setImage(6069276526, "Deselected")
overflowIcon:setName("Overflow"..alignment)
overflowIcon:setOrder(order)
overflowIcon:setAlignment(alignment)
overflowIcon:autoDeselect(false)
overflowIcon.isAnOverflow = true
--overflowIcon:freezeMenu()
overflowIcon:select("OverflowStart", overflowIcon)
overflowIcon:setEnabled(false)
overflowIcons[alignment] = overflowIcon
overflowIconUIDs[overflowIcon.UID] = true
if not Icon.closeableOverflowMenus then
local iconSpot = overflowIcon:getInstance("IconSpot")
iconSpot.Visible = false
end
end
-- The default boundary is the point where both the left-most-right-icon
-- and left-most-right-icon meet OR the opposite side of the screen
local oppositeAlignment = (alignment == "Left" and "Right") or "Left"
local oppositeOrderedIcons = Overflow.updateAvailableIcons(oppositeAlignment)
local nearestOppositeIcon = (isLeft and oppositeOrderedIcons[1]) or (isRight and oppositeOrderedIcons[#oppositeOrderedIcons])
local oppositeOverflowIcon = overflowIcons[oppositeAlignment]
local boundary = (isLeft and holderXPos + holderXSize) or holderXPos
if nearestOppositeIcon then
local oppositeRealXPositions = Overflow.getRealXPositions(oppositeAlignment, oppositeOrderedIcons)
local oppositeX = oppositeRealXPositions[nearestOppositeIcon.UID]
local oppositeXSize = Overflow.getWidth(nearestOppositeIcon)
boundary = (isLeft and oppositeX - BOUNDARY_GAP) or oppositeX + oppositeXSize + BOUNDARY_GAP
end
-- We get the left-most icon (if left alignment) or right-most-icon (if
-- right alignment) of the central icons group to see if we need to change
-- the boundary (if the central icon boundary is smaller than the alignment
-- boundary then we use the central)
local totalChecks = 0
local usingNearestCenter = false
local function checkToShiftCentralIcon()
local centerOrderedIcons = Overflow.getAvailableIcons("Center")
local centerPos = (isLeft and 1) or #centerOrderedIcons
local nearestCenterIcon = centerOrderedIcons[centerPos]
local function secondaryCheck()
if not beganSecondaryCenterCheck then
beganSecondaryCenterCheck = true
task.delay(3, Overflow.updateBoundary, alignment)
end
end
if nearestCenterIcon and not nearestCenterIcon.hasRelocatedInOverflow then
local ourNearestIcon = (isLeft and ourOrderedIcons[#ourOrderedIcons]) or (isRight and ourOrderedIcons[1])
local centralNearestXPos = nearestCenterIcon.widget.AbsolutePosition.X
local ourNearestXPos = ourNearestIcon.widget.AbsolutePosition.X
local ourNearestXSize = Overflow.getWidth(ourNearestIcon)
local centerBoundary = (isLeft and centralNearestXPos-BOUNDARY_GAP) or centralNearestXPos + Overflow.getWidth(nearestCenterIcon) + BOUNDARY_GAP
local removeBoundary = (isLeft and ourNearestXPos + ourNearestXSize) or ourNearestXPos
local hasShifted = false
if isLeft then
if centerBoundary < removeBoundary then
if not beginCheckingCenterIcons then
secondaryCheck()
return
end
nearestCenterIcon:align("Left")
nearestCenterIcon.hasRelocatedInOverflow = true
hasShifted = true
end
elseif isRight then
if centerBoundary > removeBoundary then
if not beginCheckingCenterIcons or removeBoundary < 0 then
secondaryCheck()
return
end
nearestCenterIcon:align("Right")
nearestCenterIcon.hasRelocatedInOverflow = true
hasShifted = true
end
end
if hasShifted then
totalChecks += 1
if totalChecks <= 4 then
Overflow.updateAvailableIcons("Center")
checkToShiftCentralIcon()
end
end
end
end
checkToShiftCentralIcon()
--[[
This updates the maximum size of the overflow menus
The menu determines its bounds from the smallest of either:
1. The closest center-aligned icon (i.e. the boundary)
2. The edge of the opposite overflow menu UNLESS...
3. ... the edge exceeds more than half the screenGui
--]]
if overflowIcon then
local menuBoundary = boundary
local menu = overflowIcon:getInstance("Menu")
local holderXEndPos = holderXPos + holderXSize
local menuWidth = holderXSize
if menu and oppositeOverflowIcon then
local oppositeWidget = oppositeOverflowIcon.widget
local oppositeXPos = oppositeWidget.AbsolutePosition.X
local oppositeXSize = Overflow.getWidth(oppositeOverflowIcon)
local oppositeBoundary = (isLeft and oppositeXPos - BOUNDARY_GAP) or oppositeXPos + oppositeXSize + BOUNDARY_GAP
local oppositeMenu = oppositeOverflowIcon:getInstance("Menu")
local isDominant = menu.AbsoluteCanvasSize.X >= oppositeMenu.AbsoluteCanvasSize.X
if not usingNearestCenter then
local halfwayXPos = holderXPos + holderXSize/2
local halfwayBoundary = (isLeft and halfwayXPos - BOUNDARY_GAP/2) or halfwayXPos + BOUNDARY_GAP/2
menuBoundary = halfwayBoundary
if isDominant then
menuBoundary = oppositeBoundary
end
end
menuWidth = (isLeft and menuBoundary - holderXPos) or (holderXEndPos - menuBoundary)
end
local currentMaxWidth = menu and menu:GetAttribute("MaxWidth")
menuWidth = Utility.round(menuWidth)
if menu and currentMaxWidth ~= menuWidth then
menu:SetAttribute("MaxWidth", menuWidth)
end
end
-- Parent ALL icons of that alignment into the overflow if at least on
-- sibling exceeds the bounds.
-- We calculate the the absolute position of icons instead of reading
-- directly to determine where they would be if not within an overflow
local joinOverflow = false
local realXPositions = Overflow.getRealXPositions(alignment, ourOrderedIcons)
for i = #ourOrderedIcons, 1, -1 do
local icon = ourOrderedIcons[i]
local widgetX = Overflow.getWidth(icon)
local xPos = realXPositions[icon.UID]
if (isLeft and xPos + widgetX >= boundary) or (isRight and xPos <= boundary) then
joinOverflow = true
end
end
for i = #ourOrderedIcons, 1, -1 do
local icon = ourOrderedIcons[i]
local isOverflow = overflowIconUIDs[icon.UID]
if not isOverflow then
if joinOverflow and not icon.parentIconUID then
icon:joinMenu(overflowIcon)
elseif not joinOverflow and icon.parentIconUID then
icon:leave()
end
end
end
-- Hide the overflows when not in use
if overflowIcon.isEnabled ~= joinOverflow then
overflowIcon:setEnabled(joinOverflow)
end
-- Have the menus auto selected
if overflowIcon.isEnabled and not overflowIcon.overflowAlreadyOpened then
overflowIcon.overflowAlreadyOpened = true
overflowIcon:select()
end
-- Restore relocated center icons when space is available
if not isCentral then
-- Only restore if both overflow menus are completely inactive
local leftOverflow = overflowIcons["Left"]
local rightOverflow = overflowIcons["Right"]
local leftIsInactive = not leftOverflow or not leftOverflow.isEnabled
local rightIsInactive = not rightOverflow or not rightOverflow.isEnabled
local bothSidesStable = leftIsInactive and rightIsInactive
if not joinOverflow and bothSidesStable then
local relocatedIcons = {}
for _, icon in pairs(ourOrderedIcons) do
if icon.hasRelocatedInOverflow and not icon.parentIconUID then
table.insert(relocatedIcons, icon)
end
end
if #relocatedIcons > 0 then
table.sort(relocatedIcons, function(a, b)
return (a.widget.LayoutOrder or 0) < (b.widget.LayoutOrder or 0)
end)
for _, icon in ipairs(relocatedIcons) do
local centerOrderedIcons = Overflow.getAvailableIcons("Center")
local centerHolder = holders["Center"]
local centerHolderXPos = centerHolder.AbsolutePosition.X
local centerHolderXSize = centerHolder.AbsoluteSize.X
local centerUIList = centerHolder.UIListLayout
local centerPadding = centerUIList.Padding.Offset
local totalCenterWidth = 0
for _, centerIcon in pairs(centerOrderedIcons) do
totalCenterWidth += Overflow.getWidth(centerIcon) + centerPadding
end
totalCenterWidth += Overflow.getWidth(icon) + centerPadding
local leftOrderedIcons = Overflow.getAvailableIcons("Left")
local rightOrderedIcons = Overflow.getAvailableIcons("Right")
local leftBoundary = centerHolderXPos
local rightBoundary = centerHolderXPos + centerHolderXSize
-- Calculate boundaries excluding relocated icons
if #leftOrderedIcons > 0 then
local leftRealXPositions = Overflow.getRealXPositions("Left", leftOrderedIcons)
for _, leftIcon in pairs(leftOrderedIcons) do
if not leftIcon.hasRelocatedInOverflow then
local leftIconX = leftRealXPositions[leftIcon.UID]
local leftIconWidth = Overflow.getWidth(leftIcon)
leftBoundary = math.max(leftBoundary, leftIconX + leftIconWidth + BOUNDARY_GAP)
end
end
end
if #rightOrderedIcons > 0 then
local rightRealXPositions = Overflow.getRealXPositions("Right", rightOrderedIcons)
for _, rightIcon in pairs(rightOrderedIcons) do
if not rightIcon.hasRelocatedInOverflow then
local rightIconX = rightRealXPositions[rightIcon.UID]
rightBoundary = math.min(rightBoundary, rightIconX - BOUNDARY_GAP)
end
end
end
-- Safety margin to prevent oscillation
local SAFETY_MARGIN = BOUNDARY_GAP * 2.5
local availableCenterWidth = rightBoundary - leftBoundary - SAFETY_MARGIN
if availableCenterWidth > 0 and totalCenterWidth <= availableCenterWidth then
icon:align("Center")
icon.hasRelocatedInOverflow = nil
Overflow.updateAvailableIcons("Center")
Overflow.updateAvailableIcons(alignment)
task.spawn(function()
task.wait(0.2)
Overflow.updateBoundary("Left")
Overflow.updateBoundary("Right")
end)
break
end
end
end
end
end
end
return Overflow