local M = {} M.core = require("libdiaphragm") M.util = {} function M.util.eprint(s, ...) io.stderr:write(string.format(s, ...)) io.stderr:write("\n") end function M.util.list_concat(...) local result = {} for i = 1, select("#", ...) do local current = select(i, ...) if current ~= nil then for _, v in ipairs(current) do table.insert(result, v) end end end return result end function M.util.list_intersperse_with(func, list) local result = {} for i = 1, #list - 1 do table.insert(result, list[i]) table.insert(result, func()) end table.insert(result, list[#list]) return result end function M.util.tbl_map(func, t) local result = {} for k, v in pairs(t) do result[k] = func(v) end return result end function M.util.tbl_print(t) io.stderr:write("{ ") for k, v in pairs(t) do io.stderr:write(string.format("%s = %s, ", k, v)) end io.stderr:write("}\n") end function M.util.tbl_extend(...) local result = {} for i = 1, select("#", ...) do local current = select(i, ...) if current ~= nil then for k, v in pairs(current) do result[k] = v end end end return result end function M.util.tbl_contains(t, value) for _, v in pairs(t) do if v == value then return true end end return false end -- TODO: depending on objects function M.util.is_reserved(s) return M.util.tbl_contains({ "top", "left", "width", "height", "right", "bottom", "vert_center", "horiz_center", "top_left", "top_right", "bottom_left", "bottom_right", "middle_left", "middle_right", "top_middle", "bottom_middle", "center", "stroke_color", "stroke_width", "fill_color", -- "x", -- "y", -- "size", }, s) end function M.util.tbl_filter_by_key(func, t) local result = {} for k, v in pairs(t) do if func(k) then result[k] = v end end return result end function M.util.tbl_filter_reserved(t) return M.util.tbl_filter_by_key(function(k) return not M.util.is_reserved(k) end, t) end function M.util.tbl_assign_reserved(t, rhs) for k, v in pairs(t) do if M.util.is_reserved(k) then rhs[k] = v end end end -- Return lhs, but with keys that are not in rhs function M.util.tbl_diff(lhs, rhs) local result = {} for k, v in pairs(lhs) do if rhs[k] == nil then result[k] = v end end return result end -- Assign keys from lhs to rhs, useful if rhs is userdata function M.util.tbl_assign(lhs, rhs) for k, v in pairs(lhs) do rhs[k] = v end end M.float = {} M.float.new = M.core.float M.float.max = M.core.float_max M.float.min = M.core.float_min M.text = {} M.text.font = M.core.font function M.text.new(params) params = params or {} local result = M.core.text(params) M.util.tbl_assign_reserved(params, result) return result end M.rectangle = {} function M.rectangle.new(params) params = params or {} local result = M.core.rectangle() M.util.tbl_assign_reserved(params, result) return result end function M.rectangle.surrounding(content, params) local result = M.rectangle.new(params) M.constraint.inset(content, result, params) return result end M.straight_path = {} function M.straight_path.new(params) params = params or {} local result = M.core.straight_path(params) M.util.tbl_assign_reserved(params, result) return result end function M.shape(params) params = params or {} local result = M.util.tbl_filter_reserved(params) local shape = M.core.complex_shape() setmetatable(result, { __index = function(_, k) if M.util.is_reserved(k) then return shape[k] end end, }) M.util.tbl_assign_reserved(params, shape) return result end M.layout = {} function M.layout.margin(params) local result = M.shape(params) result.margin_left = result.margin_left or M.float.new() result.margin_right = result.margin_right or M.float.new() result.margin_top = result.margin_top or M.float.new() result.margin_bottom = result.margin_bottom or M.float.new() function result:draw() M.constrain(self.left:eq(self.content.left - self.margin_left)) M.constrain(self.right:eq(self.content.right + self.margin_right)) M.constrain(self.top:eq(self.content.top - self.margin_top)) M.constrain(self.bottom:eq(self.content.bottom + self.margin_bottom)) self.content:draw() end return result end function M.layout.margin_left(params) return M.layout.margin(M.util.tbl_extend(params, { margin_right = 0, margin_top = 0, margin_bottom = 0, })) end function M.layout.margin_right(params) return M.layout.margin(M.util.tbl_extend(params, { margin_left = 0, margin_top = 0, margin_bottom = 0, })) end function M.layout.margin_top(params) return M.layout.margin(M.util.tbl_extend(params, { margin_left = 0, margin_right = 0, margin_bottom = 0, })) end function M.layout.margin_bottom(params) return M.layout.margin(M.util.tbl_extend(params, { margin_left = 0, margin_right = 0, margin_top = 0, })) end -- TODO: factor with vstack -- TODO: also as just a set of constraints -- TODO: add support for "growing" element height (or elements have same size) -- TODO: add orientation (ltr or rtl) function M.layout.hstack(params) local result = M.shape(params) result.spacing = result.spacing or 0 result.align = result.align or "top" if result.align == "middle" or result.align == "center" then result.align = "vert_center" end function result:draw() local len = #self.elements assert(len >= 1, "hstack must have at least 1 element in `elements`") local tops = {} local bottoms = {} local previous_right = self.left - self.spacing local previous_vert_anchor = self[self.align] for _, el in pairs(self.elements) do local el_vert_anchor = el[self.align] M.constrain(el.left:eq(previous_right + self.spacing)) M.constrain(el_vert_anchor:eq(previous_vert_anchor)) table.insert(tops, el.top) table.insert(bottoms, el.bottom) el:draw() previous_right = el.right previous_vert_anchor = el_vert_anchor end M.constrain(self.top:eq(M.float.min(tops))) M.constrain(self.bottom:eq(M.float.max(bottoms))) M.constrain(self.right:eq(previous_right)) end return result end function M.layout.vstack(params) local result = M.shape(params) result.spacing = result.spacing or 0 result.align = result.align or "left" if result.align == "middle" or result.align == "center" then result.align = "horiz_center" end function result:draw() local len = #self.elements assert(len >= 1, "vstack must have at least 1 element in `elements`") local lefts = {} local rights = {} local previous_bottom = self.top - self.spacing local previous_horiz_anchor = self[self.align] for _, el in pairs(self.elements) do local el_horiz_anchor = el[self.align] M.constrain(el.top:eq(previous_bottom + self.spacing)) M.constrain(el_horiz_anchor:eq(previous_horiz_anchor)) table.insert(lefts, el.left) table.insert(rights, el.right) el:draw() previous_bottom = el.bottom previous_horiz_anchor = el_horiz_anchor end M.constrain(self.left:eq(M.float.min(lefts))) M.constrain(self.right:eq(M.float.max(rights))) M.constrain(self.bottom:eq(previous_bottom)) end return result end M.constraint = {} function M.constraint.left_of(lhs, rhs, margin) M.constrain(lhs.right:eq(rhs.left - (margin or 0))) end function M.constraint.right_of(rhs, lhs, margin) M.constrain(lhs.right:eq(rhs.left - (margin or 0))) end function M.constraint.above(above, below, margin) M.constrain(above.bottom:eq(below.top - (margin or 0))) end function M.constraint.below(below, above, margin) M.constrain(above.bottom:eq(below.top - (margin or 0))) end function M.constraint.same_size(elems) local height = M.float.new() local width = M.float.new() for _, el in pairs(elems) do M.constrain(el.height:eq(height)) M.constrain(el.width:eq(width)) end end function M.constraint.same_height(elems) local height = M.float.new() for _, el in pairs(elems) do M.constrain(el.height:eq(height)) end end function M.constraint.same_width(elems) local width = M.float.new() for _, el in pairs(elems) do M.constrain(el.width:eq(width)) end end -- TODO: factor with rectangle surrounding function M.constraint.inset(child, parent, params) params = params or {} local margin = params.margin or 0 local margin_left = params.margin_left or margin local margin_right = params.margin_right or margin local margin_top = params.margin_top or margin local margin_bottom = params.margin_bottom or margin M.constrain(parent.left:eq(child.left - margin_left)) M.constrain(parent.right:eq(child.right + margin_right)) M.constrain(parent.top:eq(child.top - margin_top)) M.constrain(parent.bottom:eq(child.bottom + margin_bottom)) end M.constrain = M.core.constrain M.draw = function(params) M.core.draw(M.util.tbl_extend(params, { draw = function(figure) local self = M.shape({ left = 0, top = 0 }) M.constraint.inset(self, figure) params.draw(self) end, })) end return M