diff options
Diffstat (limited to 'ui.lua')
| -rw-r--r-- | ui.lua | 412 |
1 files changed, 412 insertions, 0 deletions
@@ -0,0 +1,412 @@ +local bit32 = require("bit32") +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( + bit32.band(bit32.rshift(hex, 16), 0xff), + bit32.band(bit32.rshift(hex, 8), 0xff), + bit32.band(bit32.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, +} |
