LayoutManager.lua 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. --[[
  2. MIT License
  3. Copyright (c) 2019 Mitchell Davis <coding.jackalope@gmail.com>
  4. Permission is hereby granted, free of charge, to any person obtaining a copy
  5. of this software and associated documentation files (the "Software"), to deal
  6. in the Software without restriction, including without limitation the rights
  7. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. copies of the Software, and to permit persons to whom the Software is
  9. furnished to do so, subject to the following conditions:
  10. The above copyright notice and this permission notice shall be included in all
  11. copies or substantial portions of the Software.
  12. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  13. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  14. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  15. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  16. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  17. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  18. SOFTWARE.
  19. --]]
  20. local Cursor = require(SLAB_PATH .. '.Internal.Core.Cursor')
  21. local Window = require(SLAB_PATH .. '.Internal.UI.Window')
  22. local LayoutManager = {}
  23. local Instances = {}
  24. local Stack = {}
  25. local Active = nil
  26. local function GetWindowBounds()
  27. local WinX, WinY, WinW, WinH = Window.GetBounds(true)
  28. local Border = Window.GetBorder()
  29. WinX = WinX + Border
  30. WinY = WinY + Border
  31. WinW = WinW - Border * 2
  32. WinH = WinH - Border * 2
  33. return WinX, WinY, WinW, WinH
  34. end
  35. local function GetRowSize(Instance)
  36. if Instance ~= nil then
  37. local Column = Instance.Columns[Instance.ColumnNo]
  38. if Column.Rows ~= nil then
  39. local Row = Column.Rows[Column.RowNo]
  40. if Row ~= nil then
  41. return Row.W, Row.H
  42. end
  43. end
  44. end
  45. return 0, 0
  46. end
  47. local function GetRowCursorPos(Instance)
  48. if Instance ~= nil then
  49. local Column = Instance.Columns[Instance.ColumnNo]
  50. if Column.Rows ~= nil then
  51. local Row = Column.Rows[Column.RowNo]
  52. if Row ~= nil then
  53. return Row.CursorX, Row.CursorY
  54. end
  55. end
  56. end
  57. return nil, nil
  58. end
  59. local function GetLayoutH(Instance, IncludePad)
  60. IncludePad = IncludePad == nil and true or IncludePad
  61. if Instance ~= nil then
  62. local Column = Instance.Columns[Instance.ColumnNo]
  63. if Column.Rows ~= nil then
  64. local H = 0
  65. for I, V in ipairs(Column.Rows) do
  66. H = H + V.H
  67. if IncludePad then
  68. H = H + Cursor.PadY()
  69. end
  70. end
  71. return H
  72. end
  73. end
  74. return 0
  75. end
  76. local function GetPreviousRowBottom(Instance)
  77. if Instance ~= nil then
  78. local Column = Instance.Columns[Instance.ColumnNo]
  79. if Column.Rows ~= nil and Column.RowNo > 1 and Column.RowNo <= #Column.Rows then
  80. local Y = Column.Rows[Column.RowNo - 1].CursorY
  81. local H = Column.Rows[Column.RowNo - 1].H
  82. return Y + H
  83. end
  84. end
  85. return nil
  86. end
  87. local function GetColumnPosition(Instance)
  88. if Instance ~= nil then
  89. local WinX, WinY, WinW, WinH = GetWindowBounds()
  90. local WinL, WinT = Window.GetPosition()
  91. local Count = #Instance.Columns
  92. local ColumnW = WinW / Count
  93. local TotalW = 0
  94. for I = 1, Instance.ColumnNo - 1, 1 do
  95. local Column = Instance.Columns[I]
  96. TotalW = TotalW + Column.W
  97. end
  98. local AnchorX, AnchorY = Instance.X, Instance.Y
  99. if not Instance.AnchorX then
  100. AnchorX = WinX - WinL - Window.GetBorder()
  101. end
  102. if not Instance.AnchorY then
  103. AnchorY = WinY - WinT - Window.GetBorder()
  104. end
  105. return AnchorX + TotalW, AnchorY
  106. end
  107. return 0, 0
  108. end
  109. local function GetColumnSize(Instance)
  110. if Instance ~= nil then
  111. local Column = Instance.Columns[Instance.ColumnNo]
  112. local WinX, WinY, WinW, WinH = GetWindowBounds()
  113. local Count = #Instance.Columns
  114. local ColumnW = WinW / Count
  115. local W, H = 0, GetLayoutH(Instance)
  116. if not Window.IsAutoSize() then
  117. W = ColumnW
  118. H = WinH
  119. Column.W = W
  120. else
  121. W = math.max(Column.W, ColumnW)
  122. end
  123. return W, H
  124. end
  125. return 0, 0
  126. end
  127. local function AddControl(Instance, W, H, Type)
  128. if Instance ~= nil then
  129. local RowW, RowH = GetRowSize(Instance)
  130. local WinX, WinY, WinW, WinH = GetWindowBounds()
  131. local CursorX, CursorY = Cursor.GetPosition()
  132. local X, Y = GetRowCursorPos(Instance)
  133. local LayoutH = GetLayoutH(Instance)
  134. local PrevRowBottom = GetPreviousRowBottom(Instance)
  135. local AnchorX, AnchorY = GetColumnPosition(Instance)
  136. WinW, WinH = GetColumnSize(Instance)
  137. local Column = Instance.Columns[Instance.ColumnNo]
  138. if RowW == 0 then
  139. RowW = W
  140. end
  141. if RowH == 0 then
  142. RowH = H
  143. end
  144. if X == nil then
  145. if Instance.AlignX == 'center' then
  146. X = math.max(WinW * 0.5 - RowW * 0.5 + AnchorX, AnchorX)
  147. elseif Instance.AlignX == 'right' then
  148. local Right = WinW - RowW
  149. if not Window.IsAutoSize() then
  150. Right = Right + Window.GetBorder()
  151. end
  152. X = math.max(Right, AnchorX)
  153. else
  154. X = AnchorX
  155. end
  156. end
  157. if Y == nil then
  158. if PrevRowBottom ~= nil then
  159. Y = PrevRowBottom + Cursor.PadY()
  160. else
  161. local RegionH = WinY + WinH - CursorY
  162. if Instance.AlignY == 'center' then
  163. Y = math.max(RegionH * 0.5 - LayoutH * 0.5 + AnchorY, AnchorY)
  164. elseif Instance.AlignY == 'bottom' then
  165. Y = math.max(WinH - LayoutH, AnchorY)
  166. else
  167. Y = AnchorY
  168. end
  169. end
  170. end
  171. Cursor.SetX(WinX + X)
  172. Cursor.SetY(WinY + Y)
  173. if H < RowH then
  174. if Instance.AlignRowY == 'center' then
  175. Cursor.SetY(WinY + Y + RowH * 0.5 - H * 0.5)
  176. elseif Instance.AlignRowY == 'bottom' then
  177. Cursor.SetY(WinY + Y + RowH - H)
  178. end
  179. end
  180. local RowNo = Column.RowNo
  181. if Column.Rows ~= nil then
  182. local Row = Column.Rows[RowNo]
  183. if Row ~= nil then
  184. Row.CursorX = X + W + Cursor.PadX()
  185. Row.CursorY = Y
  186. end
  187. end
  188. if Column.PendingRows[RowNo] == nil then
  189. local Row = {
  190. CursorX = nil,
  191. CursorY = nil,
  192. W = 0,
  193. H = 0,
  194. RequestH = 0,
  195. MaxH = 0,
  196. Controls = {}
  197. }
  198. table.insert(Column.PendingRows, Row)
  199. end
  200. local Row = Column.PendingRows[RowNo]
  201. table.insert(Row.Controls, {
  202. X = Cursor.GetX(),
  203. Y = Cursor.GetY(),
  204. W = W,
  205. H = H,
  206. AlteredSize = Column.AlteredSize,
  207. Type = Type
  208. })
  209. Row.W = Row.W + W + Cursor.PadX()
  210. Row.H = math.max(Row.H, H)
  211. Column.RowNo = RowNo + 1
  212. Column.AlteredSize = false
  213. Column.W = math.max(Row.W, Column.W)
  214. end
  215. end
  216. local function GetInstance(Id)
  217. local Key = Window.GetId() .. '.' .. Id
  218. if Instances[Key] == nil then
  219. local Instance = {}
  220. Instance.Id = Id
  221. Instance.WindowId = Window.GetId()
  222. Instance.AlignX = 'left'
  223. Instance.AlignY = 'top'
  224. Instance.AlignRowY = 'top'
  225. Instance.Ignore = false
  226. Instance.ExpandW = false
  227. Instance.X = 0
  228. Instance.Y = 0
  229. Instance.Columns = {}
  230. Instance.ColumnNo = 1
  231. Instances[Key] = Instance
  232. end
  233. return Instances[Key]
  234. end
  235. function LayoutManager.AddControl(W, H, Type)
  236. if Active ~= nil and not Active.Ignore then
  237. AddControl(Active, W, H)
  238. end
  239. end
  240. function LayoutManager.ComputeSize(W, H)
  241. if Active ~= nil then
  242. local X, Y = GetColumnPosition(Active)
  243. local WinW, WinH = GetColumnSize(Active)
  244. local RealW = WinW - X
  245. local RealH = WinH - Y
  246. local Column = Active.Columns[Active.ColumnNo]
  247. if not Active.AnchorX then
  248. RealW = WinW
  249. end
  250. if not Active.AnchorY then
  251. RealH = WinH
  252. end
  253. if Window.IsAutoSize() then
  254. local LayoutH = GetLayoutH(Active, false)
  255. if LayoutH > 0 then
  256. RealH = LayoutH
  257. end
  258. end
  259. if Active.ExpandW then
  260. if Column.Rows ~= nil then
  261. local Count = 0
  262. local ReduceW = 0
  263. local Pad = 0
  264. local Row = Column.Rows[Column.RowNo]
  265. if Row ~= nil then
  266. for I, V in ipairs(Row.Controls) do
  267. if V.AlteredSize then
  268. Count = Count + 1
  269. else
  270. ReduceW = ReduceW + V.W
  271. end
  272. end
  273. if #Row.Controls > 1 then
  274. Pad = Cursor.PadX() * (#Row.Controls - 1)
  275. end
  276. end
  277. Count = math.max(Count, 1)
  278. W = (RealW - ReduceW - Pad) / Count
  279. end
  280. end
  281. if Active.ExpandH then
  282. if Column.Rows ~= nil then
  283. local Count = 0
  284. local ReduceH = 0
  285. local Pad = 0
  286. local MaxRowH = 0
  287. for I, Row in ipairs(Column.Rows) do
  288. local IsSizeAltered = false
  289. if I == Column.RowNo then
  290. MaxRowH = Row.MaxH
  291. Row.RequestH = math.max(Row.RequestH, H)
  292. end
  293. for J, Control in ipairs(Row.Controls) do
  294. if Control.AlteredSize then
  295. if not IsSizeAltered then
  296. Count = Count + 1
  297. IsSizeAltered = true
  298. end
  299. end
  300. end
  301. if not IsSizeAltered then
  302. ReduceH = ReduceH + Row.H
  303. end
  304. end
  305. if #Column.Rows > 1 then
  306. Pad = Cursor.PadY() * (#Column.Rows - 1)
  307. end
  308. Count = math.max(Count, 1)
  309. RealH = math.max(RealH - ReduceH - Pad, 0)
  310. H = math.max(RealH / Count, H)
  311. H = math.max(H, MaxRowH)
  312. end
  313. end
  314. Column.AlteredSize = Active.ExpandW or Active.ExpandH
  315. end
  316. return W, H
  317. end
  318. function LayoutManager.Begin(Id, Options)
  319. assert(Id ~= nil or type(Id) ~= string, "A valid string Id must be given to BeginLayout!")
  320. Options = Options == nil and {} or Options
  321. Options.AlignX = Options.AlignX == nil and 'left' or Options.AlignX
  322. Options.AlignY = Options.AlignY == nil and 'top' or Options.AlignY
  323. Options.AlignRowY = Options.AlignRowY == nil and 'top' or Options.AlignRowY
  324. Options.Ignore = Options.Ignore == nil and false or Options.Ignore
  325. Options.ExpandW = Options.ExpandW == nil and false or Options.ExpandW
  326. Options.ExpandH = Options.ExpandH == nil and false or Options.ExpandH
  327. Options.AnchorX = Options.AnchorX == nil and false or Options.AnchorX
  328. Options.AnchorY = Options.AnchorY == nil and true or Options.AnchorY
  329. Options.Columns = Options.Columns == nil and 1 or Options.Columns
  330. Options.Columns = math.max(Options.Columns, 1)
  331. local Instance = GetInstance(Id)
  332. Instance.AlignX = Options.AlignX
  333. Instance.AlignY = Options.AlignY
  334. Instance.AlignRowY = Options.AlignRowY
  335. Instance.Ignore = Options.Ignore
  336. Instance.ExpandW = Options.ExpandW
  337. Instance.ExpandH = Options.ExpandH
  338. Instance.X, Instance.Y = Cursor.GetRelativePosition()
  339. Instance.AnchorX = Options.AnchorX
  340. Instance.AnchorY = Options.AnchorY
  341. if Options.Columns ~= #Instance.Columns then
  342. Instance.Columns = {}
  343. for I = 1, Options.Columns, 1 do
  344. local Column = {
  345. Rows = nil,
  346. PendingRows = {},
  347. RowNo = 1,
  348. W = 0
  349. }
  350. table.insert(Instance.Columns, Column)
  351. end
  352. end
  353. for I, Column in ipairs(Instance.Columns) do
  354. Column.PendingRows = {}
  355. Column.RowNo = 1
  356. end
  357. table.insert(Stack, 1, Instance)
  358. Active = Instance
  359. end
  360. function LayoutManager.End()
  361. assert(Active ~= nil, "LayoutManager.End was called without a call to LayoutManager.Begin!")
  362. for I, Column in ipairs(Active.Columns) do
  363. local Rows = Column.Rows
  364. Column.Rows = Column.PendingRows
  365. Column.PendingRows = nil
  366. if Rows ~= nil and Column.Rows ~= nil and #Rows == #Column.Rows then
  367. for I, V in ipairs(Rows) do
  368. Column.Rows[I].MaxH = Rows[I].RequestH
  369. end
  370. end
  371. end
  372. table.remove(Stack, 1)
  373. Active = nil
  374. if #Stack > 0 then
  375. Active = Stack[1]
  376. end
  377. end
  378. function LayoutManager.SameLine(CursorOptions)
  379. Cursor.SameLine(CursorOptions)
  380. if Active ~= nil then
  381. local Column = Active.Columns[Active.ColumnNo]
  382. Column.RowNo = math.max(Column.RowNo - 1, 1)
  383. end
  384. end
  385. function LayoutManager.NewLine()
  386. if Active ~= nil then
  387. AddControl(Active, 0, Cursor.GetNewLineSize(), 'NewLine')
  388. end
  389. Cursor.NewLine()
  390. end
  391. function LayoutManager.SetColumn(Index)
  392. if Active ~= nil then
  393. Index = math.max(Index, 1)
  394. Index = math.min(Index, #Active.Columns)
  395. Active.ColumnNo = Index
  396. end
  397. end
  398. function LayoutManager.GetActiveSize()
  399. if Active ~= nil then
  400. return GetColumnSize(Active)
  401. end
  402. return 0, 0
  403. end
  404. function LayoutManager.Validate()
  405. local Message = nil
  406. for I, V in ipairs(Stack) do
  407. if Message == nil then
  408. Message = "The following layouts have not had EndLayout called:\n"
  409. end
  410. Message = Message .. "'" .. V.Id .. "' in window '" .. V.WindowId .. "'\n"
  411. end
  412. assert(Message == nil, Message)
  413. end
  414. return LayoutManager