123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516 |
- --[[
- MIT License
- Copyright (c) 2019 Mitchell Davis <coding.jackalope@gmail.com>
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- --]]
- local Cursor = require(SLAB_PATH .. '.Internal.Core.Cursor')
- local Window = require(SLAB_PATH .. '.Internal.UI.Window')
- local LayoutManager = {}
- local Instances = {}
- local Stack = {}
- local Active = nil
- local function GetWindowBounds()
- local WinX, WinY, WinW, WinH = Window.GetBounds(true)
- local Border = Window.GetBorder()
- WinX = WinX + Border
- WinY = WinY + Border
- WinW = WinW - Border * 2
- WinH = WinH - Border * 2
- return WinX, WinY, WinW, WinH
- end
- local function GetRowSize(Instance)
- if Instance ~= nil then
- local Column = Instance.Columns[Instance.ColumnNo]
- if Column.Rows ~= nil then
- local Row = Column.Rows[Column.RowNo]
- if Row ~= nil then
- return Row.W, Row.H
- end
- end
- end
- return 0, 0
- end
- local function GetRowCursorPos(Instance)
- if Instance ~= nil then
- local Column = Instance.Columns[Instance.ColumnNo]
- if Column.Rows ~= nil then
- local Row = Column.Rows[Column.RowNo]
- if Row ~= nil then
- return Row.CursorX, Row.CursorY
- end
- end
- end
- return nil, nil
- end
- local function GetLayoutH(Instance, IncludePad)
- IncludePad = IncludePad == nil and true or IncludePad
- if Instance ~= nil then
- local Column = Instance.Columns[Instance.ColumnNo]
- if Column.Rows ~= nil then
- local H = 0
- for I, V in ipairs(Column.Rows) do
- H = H + V.H
- if IncludePad then
- H = H + Cursor.PadY()
- end
- end
- return H
- end
- end
- return 0
- end
- local function GetPreviousRowBottom(Instance)
- if Instance ~= nil then
- local Column = Instance.Columns[Instance.ColumnNo]
- if Column.Rows ~= nil and Column.RowNo > 1 and Column.RowNo <= #Column.Rows then
- local Y = Column.Rows[Column.RowNo - 1].CursorY
- local H = Column.Rows[Column.RowNo - 1].H
- return Y + H
- end
- end
- return nil
- end
- local function GetColumnPosition(Instance)
- if Instance ~= nil then
- local WinX, WinY, WinW, WinH = GetWindowBounds()
- local WinL, WinT = Window.GetPosition()
- local Count = #Instance.Columns
- local ColumnW = WinW / Count
- local TotalW = 0
- for I = 1, Instance.ColumnNo - 1, 1 do
- local Column = Instance.Columns[I]
- TotalW = TotalW + Column.W
- end
- local AnchorX, AnchorY = Instance.X, Instance.Y
- if not Instance.AnchorX then
- AnchorX = WinX - WinL - Window.GetBorder()
- end
- if not Instance.AnchorY then
- AnchorY = WinY - WinT - Window.GetBorder()
- end
- return AnchorX + TotalW, AnchorY
- end
- return 0, 0
- end
- local function GetColumnSize(Instance)
- if Instance ~= nil then
- local Column = Instance.Columns[Instance.ColumnNo]
- local WinX, WinY, WinW, WinH = GetWindowBounds()
- local Count = #Instance.Columns
- local ColumnW = WinW / Count
- local W, H = 0, GetLayoutH(Instance)
- if not Window.IsAutoSize() then
- W = ColumnW
- H = WinH
- Column.W = W
- else
- W = math.max(Column.W, ColumnW)
- end
- return W, H
- end
- return 0, 0
- end
- local function AddControl(Instance, W, H, Type)
- if Instance ~= nil then
- local RowW, RowH = GetRowSize(Instance)
- local WinX, WinY, WinW, WinH = GetWindowBounds()
- local CursorX, CursorY = Cursor.GetPosition()
- local X, Y = GetRowCursorPos(Instance)
- local LayoutH = GetLayoutH(Instance)
- local PrevRowBottom = GetPreviousRowBottom(Instance)
- local AnchorX, AnchorY = GetColumnPosition(Instance)
- WinW, WinH = GetColumnSize(Instance)
- local Column = Instance.Columns[Instance.ColumnNo]
- if RowW == 0 then
- RowW = W
- end
- if RowH == 0 then
- RowH = H
- end
- if X == nil then
- if Instance.AlignX == 'center' then
- X = math.max(WinW * 0.5 - RowW * 0.5 + AnchorX, AnchorX)
- elseif Instance.AlignX == 'right' then
- local Right = WinW - RowW
- if not Window.IsAutoSize() then
- Right = Right + Window.GetBorder()
- end
- X = math.max(Right, AnchorX)
- else
- X = AnchorX
- end
- end
- if Y == nil then
- if PrevRowBottom ~= nil then
- Y = PrevRowBottom + Cursor.PadY()
- else
- local RegionH = WinY + WinH - CursorY
- if Instance.AlignY == 'center' then
- Y = math.max(RegionH * 0.5 - LayoutH * 0.5 + AnchorY, AnchorY)
- elseif Instance.AlignY == 'bottom' then
- Y = math.max(WinH - LayoutH, AnchorY)
- else
- Y = AnchorY
- end
- end
- end
- Cursor.SetX(WinX + X)
- Cursor.SetY(WinY + Y)
- if H < RowH then
- if Instance.AlignRowY == 'center' then
- Cursor.SetY(WinY + Y + RowH * 0.5 - H * 0.5)
- elseif Instance.AlignRowY == 'bottom' then
- Cursor.SetY(WinY + Y + RowH - H)
- end
- end
- local RowNo = Column.RowNo
- if Column.Rows ~= nil then
- local Row = Column.Rows[RowNo]
- if Row ~= nil then
- Row.CursorX = X + W + Cursor.PadX()
- Row.CursorY = Y
- end
- end
- if Column.PendingRows[RowNo] == nil then
- local Row = {
- CursorX = nil,
- CursorY = nil,
- W = 0,
- H = 0,
- RequestH = 0,
- MaxH = 0,
- Controls = {}
- }
- table.insert(Column.PendingRows, Row)
- end
- local Row = Column.PendingRows[RowNo]
- table.insert(Row.Controls, {
- X = Cursor.GetX(),
- Y = Cursor.GetY(),
- W = W,
- H = H,
- AlteredSize = Column.AlteredSize,
- Type = Type
- })
- Row.W = Row.W + W + Cursor.PadX()
- Row.H = math.max(Row.H, H)
- Column.RowNo = RowNo + 1
- Column.AlteredSize = false
- Column.W = math.max(Row.W, Column.W)
- end
- end
- local function GetInstance(Id)
- local Key = Window.GetId() .. '.' .. Id
- if Instances[Key] == nil then
- local Instance = {}
- Instance.Id = Id
- Instance.WindowId = Window.GetId()
- Instance.AlignX = 'left'
- Instance.AlignY = 'top'
- Instance.AlignRowY = 'top'
- Instance.Ignore = false
- Instance.ExpandW = false
- Instance.X = 0
- Instance.Y = 0
- Instance.Columns = {}
- Instance.ColumnNo = 1
- Instances[Key] = Instance
- end
- return Instances[Key]
- end
- function LayoutManager.AddControl(W, H, Type)
- if Active ~= nil and not Active.Ignore then
- AddControl(Active, W, H)
- end
- end
- function LayoutManager.ComputeSize(W, H)
- if Active ~= nil then
- local X, Y = GetColumnPosition(Active)
- local WinW, WinH = GetColumnSize(Active)
- local RealW = WinW - X
- local RealH = WinH - Y
- local Column = Active.Columns[Active.ColumnNo]
- if not Active.AnchorX then
- RealW = WinW
- end
- if not Active.AnchorY then
- RealH = WinH
- end
- if Window.IsAutoSize() then
- local LayoutH = GetLayoutH(Active, false)
- if LayoutH > 0 then
- RealH = LayoutH
- end
- end
- if Active.ExpandW then
- if Column.Rows ~= nil then
- local Count = 0
- local ReduceW = 0
- local Pad = 0
- local Row = Column.Rows[Column.RowNo]
- if Row ~= nil then
- for I, V in ipairs(Row.Controls) do
- if V.AlteredSize then
- Count = Count + 1
- else
- ReduceW = ReduceW + V.W
- end
- end
- if #Row.Controls > 1 then
- Pad = Cursor.PadX() * (#Row.Controls - 1)
- end
- end
- Count = math.max(Count, 1)
- W = (RealW - ReduceW - Pad) / Count
- end
- end
- if Active.ExpandH then
- if Column.Rows ~= nil then
- local Count = 0
- local ReduceH = 0
- local Pad = 0
- local MaxRowH = 0
- for I, Row in ipairs(Column.Rows) do
- local IsSizeAltered = false
- if I == Column.RowNo then
- MaxRowH = Row.MaxH
- Row.RequestH = math.max(Row.RequestH, H)
- end
- for J, Control in ipairs(Row.Controls) do
- if Control.AlteredSize then
- if not IsSizeAltered then
- Count = Count + 1
- IsSizeAltered = true
- end
- end
- end
- if not IsSizeAltered then
- ReduceH = ReduceH + Row.H
- end
- end
- if #Column.Rows > 1 then
- Pad = Cursor.PadY() * (#Column.Rows - 1)
- end
- Count = math.max(Count, 1)
- RealH = math.max(RealH - ReduceH - Pad, 0)
- H = math.max(RealH / Count, H)
- H = math.max(H, MaxRowH)
- end
- end
- Column.AlteredSize = Active.ExpandW or Active.ExpandH
- end
- return W, H
- end
- function LayoutManager.Begin(Id, Options)
- assert(Id ~= nil or type(Id) ~= string, "A valid string Id must be given to BeginLayout!")
- Options = Options == nil and {} or Options
- Options.AlignX = Options.AlignX == nil and 'left' or Options.AlignX
- Options.AlignY = Options.AlignY == nil and 'top' or Options.AlignY
- Options.AlignRowY = Options.AlignRowY == nil and 'top' or Options.AlignRowY
- Options.Ignore = Options.Ignore == nil and false or Options.Ignore
- Options.ExpandW = Options.ExpandW == nil and false or Options.ExpandW
- Options.ExpandH = Options.ExpandH == nil and false or Options.ExpandH
- Options.AnchorX = Options.AnchorX == nil and false or Options.AnchorX
- Options.AnchorY = Options.AnchorY == nil and true or Options.AnchorY
- Options.Columns = Options.Columns == nil and 1 or Options.Columns
- Options.Columns = math.max(Options.Columns, 1)
- local Instance = GetInstance(Id)
- Instance.AlignX = Options.AlignX
- Instance.AlignY = Options.AlignY
- Instance.AlignRowY = Options.AlignRowY
- Instance.Ignore = Options.Ignore
- Instance.ExpandW = Options.ExpandW
- Instance.ExpandH = Options.ExpandH
- Instance.X, Instance.Y = Cursor.GetRelativePosition()
- Instance.AnchorX = Options.AnchorX
- Instance.AnchorY = Options.AnchorY
- if Options.Columns ~= #Instance.Columns then
- Instance.Columns = {}
- for I = 1, Options.Columns, 1 do
- local Column = {
- Rows = nil,
- PendingRows = {},
- RowNo = 1,
- W = 0
- }
- table.insert(Instance.Columns, Column)
- end
- end
- for I, Column in ipairs(Instance.Columns) do
- Column.PendingRows = {}
- Column.RowNo = 1
- end
- table.insert(Stack, 1, Instance)
- Active = Instance
- end
- function LayoutManager.End()
- assert(Active ~= nil, "LayoutManager.End was called without a call to LayoutManager.Begin!")
- for I, Column in ipairs(Active.Columns) do
- local Rows = Column.Rows
- Column.Rows = Column.PendingRows
- Column.PendingRows = nil
- if Rows ~= nil and Column.Rows ~= nil and #Rows == #Column.Rows then
- for I, V in ipairs(Rows) do
- Column.Rows[I].MaxH = Rows[I].RequestH
- end
- end
- end
- table.remove(Stack, 1)
- Active = nil
- if #Stack > 0 then
- Active = Stack[1]
- end
- end
- function LayoutManager.SameLine(CursorOptions)
- Cursor.SameLine(CursorOptions)
- if Active ~= nil then
- local Column = Active.Columns[Active.ColumnNo]
- Column.RowNo = math.max(Column.RowNo - 1, 1)
- end
- end
- function LayoutManager.NewLine()
- if Active ~= nil then
- AddControl(Active, 0, Cursor.GetNewLineSize(), 'NewLine')
- end
- Cursor.NewLine()
- end
- function LayoutManager.SetColumn(Index)
- if Active ~= nil then
- Index = math.max(Index, 1)
- Index = math.min(Index, #Active.Columns)
- Active.ColumnNo = Index
- end
- end
- function LayoutManager.GetActiveSize()
- if Active ~= nil then
- return GetColumnSize(Active)
- end
- return 0, 0
- end
- function LayoutManager.Validate()
- local Message = nil
- for I, V in ipairs(Stack) do
- if Message == nil then
- Message = "The following layouts have not had EndLayout called:\n"
- end
- Message = Message .. "'" .. V.Id .. "' in window '" .. V.WindowId .. "'\n"
- end
- assert(Message == nil, Message)
- end
- return LayoutManager
|