local ok, bit = pcall(require, "bit") if not ok then ok, bit = pcall(require, "bit32") if not ok then error("no bit library") end end local utf8 = require("utf8") local distribute = require("distribute") local function ui_new(root) return { targets = {}, root = root } end local function compute(x, ...) if type(x) == "function" then return x(...) end return x end local function access(t, k) if t ~= nil then return t[k] end end local function align(kind, pos, needed, avail) if not kind then return pos, avail elseif kind == "start" then return pos, needed elseif kind == "center" then return pos+(avail-needed)/2, needed elseif kind == "end" then return pos+avail-needed, needed end end local function add_computed(node, attr, k, v) node.computed = node.computed or {} node.computed[attr] = node.computed[attr] or {} node.computed[attr][k] = v end local function node_compute_min_size(node, dir) local size = access(compute(node.size, node), dir) add_computed(node, "fixed_size", dir, size) if not size then size = 0 for _, child in ipairs(node) do local child_size = node_compute_min_size(child, dir) if node.stack == dir then size = size + child_size else size = math.max(size, child_size) end end end add_computed(node, "min_size", dir, size) return size end local function node_compute_layout(node, dir, pos, size) size = node.computed.fixed_size[dir] or size local align_as = access(node.align, dir) add_computed(node, "pos", dir, pos) add_computed(node, "size", dir, size) if node.stack ~= dir then for _, child in ipairs(node) do node_compute_layout(child, dir, align(align_as, pos, child.computed.min_size[dir], size)) end return end local min_size_noflex = 0 local flex_elems = {} local has_flex for i, child in ipairs(node) do if child.flex then has_flex = true flex_elems[i] = { min = child.computed.min_size[dir], weight = child.flex, } else min_size_noflex = min_size_noflex + child.computed.min_size[dir] end end distribute(size-min_size_noflex, flex_elems) if not has_flex then pos = align(align_as, pos, min_size_noflex, size) end for i, child in ipairs(node) do local child_size = access(flex_elems[i], "receive") or child.computed.min_size[dir] node_compute_layout(child, dir, pos, child_size) pos = pos + child_size end end local function tree_compute_layout(tree, x, y, width, height) node_compute_min_size(tree, "x") node_compute_min_size(tree, "y") node_compute_layout(tree, "x", x, width) node_compute_layout(tree, "y", y, height) end -- render local function update_flags(node, targets, flags) local new = {} for k, v in pairs(targets) do new[k] = node == v or (flags[k] and not (node.events and node.events[k])) end return new end local function node_render(node, targets, flags) flags = update_flags(node, targets, flags) local pos, size = node.computed.pos, node.computed.size if node.fill then love.graphics.setColor(compute(node.fill, node, flags)) love.graphics.rectangle("fill", pos.x, pos.y, size.x, size.y) end if node.line then love.graphics.setColor(compute(node.line, node, flags)) love.graphics.rectangle("line", pos.x, pos.y, size.x, size.y) end if node.text then love.graphics.setColor(compute(node.color, node, flags)) love.graphics.draw(node.text, pos.x, pos.y) end for _, child in ipairs(node) do node_render(child, targets, flags) end end local function ui_render(ui) node_render(ui.root, ui.targets, {}) end -- events local function node_get_target(node, event, x, y) -- todo: maybe reverse iteration? for _, child in ipairs(node) do local hov = node_get_target(child, event, x, y) if hov then return hov end end if not node.events or not node.events[event] then return nil end local pos, size = node.computed.pos, node.computed.size if x >= pos.x and y >= pos.y and x < pos.x + size.x and y < pos.y + size.y then return node end end local function ui_get_target(ui, event) return node_get_target(ui.root, event, love.mouse.getPosition()) end local function fire_event(node, event, ...) if node and node.events and node.events[event] then if type(node.events[event]) == "function" then node.events[event](node, ...) end return true end return false end local function ui_set_target(ui, event, elem) local old = ui.targets[event] if old ~= elem then fire_event(old, event.."_end") fire_event(elem, event) end ui.targets[event] = elem end local function ui_update_target(ui, event) ui_set_target(ui, event, ui_get_target(ui, event)) end local function ui_mousepressed(ui, x, y, button) if button ~= 1 then return false end ui_update_target(ui, "press") ui_update_target(ui, "focus") local focus = ui.targets.focus if focus and focus.events.input then local pos, size = focus.computed.pos, focus.computed.size love.keyboard.setTextInput(true, pos.x, pos.y, size.x, size.y) else love.keyboard.setTextInput(false) end if ui.targets.press or ui.targets.focus then return true end return false end local function ui_mousereleased(ui, x, y, button) if button ~= 1 then return false end local press = ui_get_target(ui, "press", love.mouse.getPosition()) if press == ui.targets.press then fire_event(press, "click") end ui_set_target(ui, "press", nil) if press then return true end return false end local function ui_textinput(ui, text) return fire_event(ui.targets.focus, "input", text) end local function ui_keypressed(ui, key) if key == "backspace" then return fire_event(ui.targets.focus, "input", "\b") elseif key == "v" and (love.keyboard.isDown("rctrl") or love.keyboard.isDown("lctrl")) then return fire_event(ui.targets.focus, "input", love.system.getClipboardText()) end return false end -- update local function ui_update(ui) tree_compute_layout(ui.root, 0, 0, love.graphics:getDimensions()) ui_update_target(ui, "hover") end -- presets local function color(hex) return { love.math.colorFromBytes( bit.band(bit.rshift(hex, 16), 0xff), bit.band(bit.rshift(hex, 8), 0xff), bit.band(bit.rshift(hex, 0), 0xff) ) } end local function ui_flex(n, ...) return { flex = n, ... } end local function pad(dir, size, ...) local p = { size = { [dir] = size } } local nodes = { ... } if #nodes > 1 then local x = { stack = dir } for i, n in ipairs(nodes) do table.insert(x, n) if i < #nodes then table.insert(x, p) end end return x elseif #nodes == 1 then return { stack = dir, p, ui_flex(1, nodes[1]), p } end return p end local function ui_pad_x(...) return pad("x", ...) end local function ui_pad_y(...) return pad("y", ...) end local function center_x(...) return { align = { x = "center" }, ... } end local function center_y(...) return { align = { y = "center" }, ... } end local function stack_x(...) return { stack = "x", ... } end local function stack_y(...) return { stack = "y", ... } end local function ui_text(str, size, col) return { color = col, text = love.graphics.newText(love.graphics.newFont(size), str), size = function(self) return { x = self.text:getWidth(), y = self.text:getHeight() } end, } end local function ui_button(label, click) return { fill = function(self, f) return f.hover and color(0x8080a0) or color(0x808080) end, line = color(0x505050), events = { hover = true, press = true, click = click, }, ui_pad_y(5, center_x(ui_pad_x(10, ui_text(label, 25, color(0x000000)) ))) } end local function ui_input(placeholder) local function is_placeholder(self, focus) return self.input_value == "" and not focus end local function update_text(self, focus) self.input_text:set(is_placeholder(self, focus) and self.input_placeholder or self.input_value) self.input_interact_time = love.timer.getTime() end local text = ui_text(placeholder, 25, function(self, f) return is_placeholder(self.input_parent, f.focus) and color(0xb0b0b0) or color(0x000000) end) local marker = { size = { x = 1, y = text.text:getHeight() }, fill = function(self, f) return (f.focus and math.floor(love.timer.getTime() - self.input_parent.input_interact_time) % 2 == 0) and color(0x000000) or color(0xffffff) end } -- TODO: cursor -- TODO: overflow as scroll -- TODO: paste icon (optional) local elem = { fill = color(0xffffff), line = function(self, f) return f.focus and color(0x5050ff) or color(0x505050) end, events = { focus = function(self) update_text(self, true) end, focus_end = function(self) update_text(self, false) end, input = function(self, text) if text == "\b" then local byteoffset = utf8.offset(self.input_value, -1) if byteoffset then self.input_value = string.sub(self.input_value, 1, byteoffset - 1) end else self.input_value = self.input_value .. text end update_text(self, true) end, }, input_value = "", input_text = text.text, input_placeholder = placeholder, ui_pad_y(5, ui_pad_x(10, stack_x(text, marker))) } text.input_parent = elem marker.input_parent = elem return elem end return { new = ui_new, render = ui_render, update = ui_update, keypressed = ui_keypressed, textinput = ui_textinput, mousepressed = ui_mousepressed, mousereleased = ui_mousereleased, color = color, pad_x = ui_pad_x, pad_y = ui_pad_y, center_x = center_x, center_y = center_y, stack_x = stack_x, stack_y = stack_y, flex = ui_flex, text = ui_text, button = ui_button, input = ui_input, }