-- This file is part of SUIT, copyright (c) 2016 Matthias Richter local Layout = {} function Layout.new(x,y,padx,pady) return setmetatable({_stack = {}}, {__index = Layout}):reset(x,y,padx,pady) end function Layout:reset(x,y, padx,pady) self._x = x or 0 self._y = y or 0 self._padx = padx or 0 self._pady = pady or self._padx self._w = nil self._h = nil self._widths = {} self._heights = {} self._isFirstCell = true self._start_x = self._x return self end function Layout:padding(padx,pady) if padx then self._padx = padx self._pady = pady or padx end return self._padx, self._pady end function Layout:size() return self._w, self._h end function Layout:nextRow() return self._x, self._y + self._h + self._pady end function Layout:newRow() self._x = self._start_x self._y = self._y + self._h + self._pady self._w = nil end Layout.nextDown = Layout.nextRow function Layout:nextCol() return self._x + self._w + self._padx, self._y end Layout.nextRight = Layout.nextCol function Layout:push(x,y) self._stack[#self._stack+1] = { self._x, self._y, self._padx, self._pady, self._w, self._h, self._widths, self._heights, } return self:reset(x,y, padx or self._padx, pady or self._pady) end function Layout:pop() assert(#self._stack > 0, "Nothing to pop") local w,h = self._w, self._h self._x, self._y, self._padx,self._pady, self._w, self._h, self._widths, self._heights = unpack(self._stack[#self._stack]) self._isFirstCell = false self._stack[#self._stack] = nil self._w, self._h = math.max(w, self._w or 0), math.max(h, self._h or 0) return w, h end --- recursive binary search for position of v local function insert_sorted_helper(t, i0, i1, v) if i1 <= i0 then table.insert(t, i0, v) return end local i = i0 + math.floor((i1-i0)/2) if t[i] < v then return insert_sorted_helper(t, i+1, i1, v) elseif t[i] > v then return insert_sorted_helper(t, i0, i-1, v) else table.insert(t, i, v) end end local function insert_sorted(t, v) if v <= 0 then return end insert_sorted_helper(t, 1, #t, v) end local function calc_width_height(self, w, h) if w == "" or w == nil then w = self._w elseif w == "max" then w = self._widths[#self._widths] elseif w == "min" then w = self._widths[1] elseif w == "median" then w = self._widths[math.ceil(#self._widths/2)] or 0 elseif type(w) ~= "number" then error("width: invalid value (" .. tostring(w) .. ")", 3) end if h == "" or h == nil then h = self._h elseif h == "max" then h = self._heights[#self._heights] elseif h == "min" then h = self._heights[1] elseif h == "median" then h = self._heights[math.ceil(#self._heights/2)] or 0 elseif type(h) ~= "number" then error("width: invalid value (" .. tostring(w) .. ")", 3) end if not w or not h then error("Invalid cell size", 3) end insert_sorted(self._widths, w) insert_sorted(self._heights, h) return w,h end function Layout:row(w, h) w,h = calc_width_height(self, w, h) local x,y = self._x, self._y + (self._h or 0) if not self._isFirstCell then y = y + self._pady end self._isFirstCell = false self._y, self._w, self._h = y, w, h return x,y,w,h end Layout.down = Layout.row function Layout:up(w, h) w,h = calc_width_height(self, w, h) local x,y = self._x, self._y - (self._h and h or 0) if not self._isFirstCell then y = y - self._pady end self._isFirstCell = false self._y, self._w, self._h = y, w, h return x,y,w,h end function Layout:col(w, h) w,h = calc_width_height(self, w, h) local x,y = self._x + (self._w or 0), self._y if not self._isFirstCell then x = x + self._padx end self._isFirstCell = false self._x, self._w, self._h = x, w, h return x,y,w,h end Layout.right = Layout.col function Layout:left(w, h) w,h = calc_width_height(self, w, h) local x,y = self._x - (self._w and w or 0), self._y if not self._isFirstCell then x = x - self._padx end self._isFirstCell = false self._x, self._w, self._h = x, w, h return x,y,w,h end local function layout_iterator(t, idx) idx = (idx or 1) + 1 if t[idx] == nil then return nil end return idx, unpack(t[idx]) end local function layout_retained_mode(self, t, constructor, string_argument_to_table, fill_width, fill_height) -- sanity check local p = t.pos or {0,0} if type(p) ~= "table" then error("Invalid argument `pos' (table expected, got "..type(p)..")", 2) end local pad = t.padding or {} if type(p) ~= "table" then error("Invalid argument `padding' (table expected, got "..type(p)..")", 2) end self:push(p[1] or 0, p[2] or 0) self:padding(pad[1] or self._padx, pad[2] or self._pady) -- first pass: get dimensions, add layout info local layout = {n_fill_w = 0, n_fill_h = 0} for i,v in ipairs(t) do if type(v) == "string" then v = string_argument_to_table(v) end local x,y,w,h = 0,0, v[1], v[2] if v[1] == "fill" then w = 0 end if v[2] == "fill" then h = 0 end x,y, w,h = constructor(self, w,h) if v[1] == "fill" then w = "fill" layout.n_fill_w = layout.n_fill_w + 1 end if v[2] == "fill" then h = "fill" layout.n_fill_h = layout.n_fill_h + 1 end layout[i] = {x,y,w,h, unpack(v,3)} end -- second pass: extend "fill" cells and shift others accordingly local fill_w = fill_width(layout, t.min_width or 0, self._x + self._w - p[1]) local fill_h = fill_height(layout, t.min_height or 0, self._y + self._h - p[2]) local dx,dy = 0,0 for _,v in ipairs(layout) do v[1], v[2] = v[1] + dx, v[2] + dy if v[3] == "fill" then v[3] = fill_w dx = dx + v[3] end if v[4] == "fill" then v[4] = fill_h dy = dy + v[4] end end -- finally: return layout with iterator local w, h = self:pop() layout.cell = function(self, i) if self ~= layout then -- allow either colon or dot syntax i = self end return unpack(layout[i]) end layout.size = function() return w, h end return setmetatable(layout, {__call = function() return layout_iterator, layout, 0 end}) end function Layout:rows(t) return layout_retained_mode(self, t, self.row, function(v) return {nil, v} end, function() return self._widths[#self._widths] end, -- fill width function(l,mh,h) return (mh - h) / l.n_fill_h end) -- fill height end function Layout:cols(t) return layout_retained_mode(self, t, self.col, function(v) return {v} end, function(l,mw,w) return (mw - w) / l.n_fill_w end, -- fill width function() return self._heights[#self._heights] end) -- fill height end --[[ "Tests" do L = Layout.new() print("immediate mode") print("--------------") x,y,w,h = L:row(100,20) -- x,y,w,h = 0,0, 100,20 print(1,x,y,w,h) x,y,w,h = L:row() -- x,y,w,h = 0, 20, 100,20 (default: reuse last dimensions) print(2,x,y,w,h) x,y,w,h = L:col(20) -- x,y,w,h = 100, 20, 20, 20 print(3,x,y,w,h) x,y,w,h = L:row(nil,30) -- x,y,w,h = 100, 20, 20, 30 print(4,x,y,w,h) print('','','', L:size()) -- w,h = 20, 30 print() L:reset() local layout = L:rows{ pos = {10,10}, -- optional, default {0,0} {100, 10}, {nil, 10}, -- {100, 10} {100, 20}, -- {100, 20} {}, -- {100, 20} -- default = last value {nil, "median"}, -- {100, 20} "median", -- {100, 20} "max", -- {100, 20} "min", -- {100, 10} "" -- {100, 10} -- default = last value } print("rows") print("----") for i,x,y,w,h in layout() do print(i,x,y,w,h) end print() -- +-------+-------+----------------+-------+ -- | | | | | -- 70 {100, | "max" | "fill" | "min" | -- | 70} | | | | -- +--100--+--100--+------220-------+--100--+ -- -- `-------------------,--------------------' -- 520 local layout = L:cols{ pos = {10,10}, min_width = 520, {100, 70}, "max", -- {100, 70} "fill", -- {min_width - width_of_items, 70} = {220, 70} "min", -- {100,70} } print("cols") print("----") for i,x,y,w,h in layout() do print(i,x,y,w,h) end print() L:push() L:row() end --]] -- TODO: nesting a la rows{..., cols{...} } ? local instance = Layout.new() return setmetatable({ new = Layout.new, reset = function(...) return instance:reset(...) end, padding = function(...) return instance:padding(...) end, push = function(...) return instance:push(...) end, pop = function(...) return instance:pop(...) end, row = function(...) return instance:row(...) end, col = function(...) return instance:col(...) end, down = function(...) return instance:down(...) end, up = function(...) return instance:up(...) end, left = function(...) return instance:left(...) end, right = function(...) return instance:right(...) end, rows = function(...) return instance:rows(...) end, cols = function(...) return instance:cols(...) end, }, {__call = function(_,...) return Layout.new(...) end})