diff options
-rw-r--r-- | README.md | 47 | ||||
-rw-r--r-- | button.lua | 27 | ||||
-rw-r--r-- | checkbox.lua | 19 | ||||
-rw-r--r-- | core.lua | 153 | ||||
-rw-r--r-- | imgui.love | bin | 0 -> 5053 bytes | |||
-rw-r--r-- | init.lua | 11 | ||||
-rw-r--r-- | input.lua | 43 | ||||
-rw-r--r-- | label.lua | 9 | ||||
-rw-r--r-- | slider.lua | 44 | ||||
-rw-r--r-- | slider2d.lua | 55 | ||||
-rw-r--r-- | style-default.lua | 134 |
11 files changed, 542 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..5aed0ff --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# QUICKIE + +Quickie is an [immediate mode gui][IMGUI] library for LÖVE. + +## Example + + local gui = require 'quickie' + + -- widgets are "created" by calling their corresponding functions in love.update. + -- if you want to remove a widget, simply don't call the function (just like with + -- any other love drawable). widgets dont hold their own state - this is your job: + -- + -- sliders have a value and optional a minimum (default = 0) and maximum (default = 1) + local slider = {value = 10, min = 0, max = 100} + -- input boxes have a text and a cursor position (defaults to end of string) + local input = {text = "Hello, World!", cursor = 0} + -- checkboxes have only a `checked' status + local checkbox = {checked = false} + + function love.update(dt) + -- widgets are defined by simply calling them. usually a widget returns true if + -- if its value changed or if it was activated (click on button, ...) + if gui.Input(input, 10, 10, 300, 20) then + print('Text changed:', input.text) + end + + if gui.Button('Clear', 320,10,100,20) then + input.text = "" + end + + -- add more widgets here + end + + function love.draw() + -- draw the widgets which were "created" in love.update + gui.core.draw() + end + + function love.keypressed(key,code) + -- forward keyboard events to the gui. If you don't want widget tabbing and + -- input widgets, skip this line + gui.core.keyboard.pressed(key, code) + end + +## Documentation + +TODO diff --git a/button.lua b/button.lua new file mode 100644 index 0000000..2374645 --- /dev/null +++ b/button.lua @@ -0,0 +1,27 @@ +local core = require((...):match("^(.+)%.[^%.]+") .. '.core') + +-- the widget +return function(title, x,y, w,h, draw) + -- Generate unique identifier for gui state update and querying. + local id = core.generateID() + + -- The widget mouse-state can be: + -- hot (mouse over widget), + -- active (mouse pressed on widget) or + -- normal (mouse not on widget and not pressed on widget). + -- + -- core.mouse.updateState(id, x,y,w,h) updates the state for this widget. + core.mouse.updateState(id, x,y,w,h) + + -- core.makeTabable makes the item focus on tab. Tab order is determied + -- by the order you call the widget functions. + core.makeTabable(id) + + -- core.registerDraw(id, drawfunction, drawfunction-arguments...) + -- shows widget when core.draw() is called. + core.registerDraw(id, draw or core.style.Button, title,x,y,w,h) + + return core.mouse.releasedOn(id) or + (core.keyboard.key == 'return' and core.hasKeyFocus(id)) +end + diff --git a/checkbox.lua b/checkbox.lua new file mode 100644 index 0000000..c5f5fce --- /dev/null +++ b/checkbox.lua @@ -0,0 +1,19 @@ +local core = require((...):match("^(.+)%.[^%.]+") .. '.core') + +return function(info, x,y, w,h, draw) + local id = core.generateID() + + core.mouse.updateState(id, x,y,w,h) + core.makeTabable(id) + core.registerDraw(id, draw or core.style.Checkbox, info.checked,x,y,w,h) + + local checked = info.checked + local key = core.keyboard.key + if core.mouse.releasedOn(id) or + (core.hasKeyFocus(id) and key == 'return' or key == ' ') then + info.checked = not info.checked + end + + return info.checked ~= checked +end + diff --git a/core.lua b/core.lua new file mode 100644 index 0000000..f57caa9 --- /dev/null +++ b/core.lua @@ -0,0 +1,153 @@ +-- state +local context = {maxid = 0} +local NO_WIDGET = function()end + +local function generateID() + context.maxid = context.maxid + 1 + return context.maxid +end + +local function setHot(id) context.hot = id end +local function isHot(id) return context.hot == id end + +local function setActive(id) context.active = id end +local function isActive(id) return context.active == id end + +local function setKeyFocus(id) context.keyfocus = id end +local function hasKeyFocus(id) return context.keyfocus == id end + +-- input +local mouse = {x = 0, y = 0, down = false} +local keyboard = {key = nil, code = -1} + +function mouse.inRect(x,y,w,h) + return mouse.x >= x and mouse.x <= x+w and mouse.y >= y and mouse.y <= y+h +end + +function mouse.updateState(id, x,y,w,h) + if mouse.inRect(x,y,w,h) then + setHot(id) + if not context.active and mouse.down then + setActive(id) + end + end +end + +function mouse.releasedOn(id) + return not mouse.down and isHot(id) and isActive(id) +end + +function keyboard.pressed(key, code) + keyboard.key = key + keyboard.code = code +end + +function keyboard.tryGrab(id) + if not context.keyfocus then + context.keyfocus = id + end +end + +local function makeTabable(id) + keyboard.tryGrab(id) + if hasKeyFocus(id) and keyboard.key == 'tab' then + if love.keyboard.isDown('rshift', 'lshift') then + setKeyFocus(context.lastwidget) + else + setKeyFocus(nil) + end + keyboard.key = nil + end + context.lastwidget = id +end + +-- helper functions +local function strictAnd(...) + local n = select("#", ...) + local ret = true + for i = 1,n do ret = select(i, ...) and ret end + return ret +end + +local function strictOr(...) + local n = select("#", ...) + local ret = false + for i = 1,n do ret = select(i, ...) or ret end + return ret +end + +-- allow packed nil +local function save_pack(...) + return {n = select('#', ...), ...} +end + +local function save_unpack(t, i) + i = i or 1 + if i >= t.n then return t[i] end + return t[i], save_unpack(t, i+1) +end + +local draw_items = {n = 0} +local function registerDraw(id, f, ...) + assert(type(f) == 'function' or (getmetatable(f) or {}).__call, + 'Drawing function is not a callable type!') + + local state = 'normal' + if isHot(id) or hasKeyFocus(id) then + state = isActive(id) and 'active' or 'hot' + end + local rest = save_pack(...) + draw_items.n = draw_items.n + 1 + draw_items[draw_items.n] = function() f(state, save_unpack(rest)) end +end + +-- actually update-and-draw +local function draw() + -- close frame state + if not mouse.down then -- released + setActive(nil) + elseif not context.active then -- clicked outside + setActive(NO_WIDGET) + end + + for i = 1,draw_items.n do draw_items[i]() end + + -- prepare for next frame + draw_items.n = 0 + context.maxid = 0 + + -- update mouse status + setHot(nil) + mouse.x, mouse.y = love.mouse.getPosition() + mouse.down = love.mouse.isDown('l') + + -- clear keyboard focus if nobody wants it + if keyboard.key == 'tab' then + context.keyboardfocus = nil + end + keyboard.key, keyboard.code = nil, -1 +end + +return { + mouse = mouse, + keyboard = keyboard, + + generateID = generateID, + setHot = setHot, + setActive = setActive, + setKeyFocus = setKeyFocus, + isHot = isHot, + isActive = isActive, + hasKeyFocus = hasKeyFocus, + makeTabable = makeTabable, + + style = require((...):match("^(.+)%.[^%.]+") .. '.style-default'), + color = color, + registerDraw = registerDraw, + draw = draw, + + strictAnd = strictAnd, + strictOr = strictOr, + save_pack = save_pack, + save_unpack = save_unpack, +} diff --git a/imgui.love b/imgui.love Binary files differnew file mode 100644 index 0000000..720c9b1 --- /dev/null +++ b/imgui.love diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..3b4e70c --- /dev/null +++ b/init.lua @@ -0,0 +1,11 @@ +local BASE = (...) .. '.' + +return { + core = require(BASE .. 'core'), + Button = require(BASE .. 'button'), + Slider = require(BASE .. 'slider'), + Slider2D = require(BASE .. 'slider2d'), + Label = require(BASE .. 'label'), + Input = require(BASE .. 'input'), + Checkbox = require(BASE .. 'checkbox') +} diff --git a/input.lua b/input.lua new file mode 100644 index 0000000..2ba29b5 --- /dev/null +++ b/input.lua @@ -0,0 +1,43 @@ +local core = require((...):match("^(.+)%.[^%.]+") .. '.core') + +return function(info, x,y,w,h, draw) + info.text = info.text or "" + info.cursor = math.min(info.cursor or info.text:len(), info.text:len()) + + local id = core.generateID() + core.mouse.updateState(id, x,y,w,h) + core.makeTabable(id) + if core.isActive(id) then core.setKeyFocus(id) end + + core.registerDraw(id, draw or core.style.Input, info.text, info.cursor, x,y,w,h) + + local changed = false + -- editing + if core.keyboard.key == 'backspace' then + info.text = info.text:sub(1,info.cursor-1) .. info.text:sub(info.cursor+1) + info.cursor = math.max(0, info.cursor-1) + changed = true + elseif core.keyboard.key == 'delete' then + info.text = info.text:sub(1,info.cursor) .. info.text:sub(info.cursor+2) + info.cursor = math.min(info.text:len(), info.cursor) + changed = true + -- movement + elseif core.keyboard.key == 'left' then + info.cursor = math.max(0, info.cursor-1) + elseif core.keyboard.key == 'right' then + info.cursor = math.min(info.text:len(), info.cursor+1) + elseif core.keyboard.key == 'home' then + info.cursor = 0 + elseif core.keyboard.key == 'end' then + info.cursor = info.text:len() + -- input + elseif core.keyboard.code >= 32 and core.keyboard.code < 127 then + local left = info.text:sub(1,info.cursor) + local right = info.text:sub(info.cursor+1) + info.text = table.concat{left, string.char(core.keyboard.code), right} + info.cursor = info.cursor + 1 + changed = true + end + + return changed +end diff --git a/label.lua b/label.lua new file mode 100644 index 0000000..76f3c92 --- /dev/null +++ b/label.lua @@ -0,0 +1,9 @@ +local core = require((...):match("^(.+)%.[^%.]+") .. '.core') + +return function(text, x,y,w,h,align, draw) + local id = core.generateID() + w, h, align = w or 0, h or 0, align or 'left' + core.registerDraw(id, draw or core.style.Label, text,x,y,w,h,align) + return false +end + diff --git a/slider.lua b/slider.lua new file mode 100644 index 0000000..ae75c01 --- /dev/null +++ b/slider.lua @@ -0,0 +1,44 @@ +local core = require((...):match("^(.+)%.[^%.]+") .. '.core') + +return function(info, x,y,w,h, draw) + assert(type(info) == 'table' and info.value, "Incomplete slider value info") + info.min = info.min or 0 + info.max = info.max or math.max(info.value, 1) + info.step = info.step or (info.max - info.min) / 50 + local fraction = (info.value - info.min) / (info.max - info.min) + + local id = core.generateID() + core.mouse.updateState(id, x,y,w,h) + core.makeTabable(id) + core.registerDraw(id,draw or core.style.Slider, fraction, x,y,w,h, info.vertical) + + -- mouse update + if core.isActive(id) then + core.setKeyFocus(id) + if info.vertical then + fraction = math.min(1, math.max(0, (y - core.mouse.y + h) / h)) + else + fraction = math.min(1, math.max(0, (core.mouse.x - x) / w)) + end + local v = fraction * (info.max - info.min) + info.min + if v ~= info.value then + info.value = v + return true + end + end + + -- keyboard update + local changed = false + if core.hasKeyFocus(id) then + local keys = info.vertical and {'up', 'down'} or {'right', 'left'} + if core.keyboard.key == keys[1] then + info.value = math.min(info.max, info.value + info.step) + changed = true + elseif core.keyboard.key == keys[2] then + info.value = math.max(info.min, info.value - info.step) + changed = true + end + end + + return changed +end diff --git a/slider2d.lua b/slider2d.lua new file mode 100644 index 0000000..023d16f --- /dev/null +++ b/slider2d.lua @@ -0,0 +1,55 @@ +local core = require((...):match("^(.+)%.[^%.]+") .. '.core') + +return function(info, x,y,w,h, draw) + assert(type(info) == 'table' and type(info.value) == "table", "Incomplete slider value info") + info.min = info.min or {x = 0, y = 0} + info.max = info.max or {x = math.max(info.value.x or 0, 1), y = math.max(info.value.y or 0, 1)} + info.step = info.step or {x = (info.max.x - info.min.x)/50, y = (info.max.y - info.min.y)/50} + local fraction = { + x = (info.value.x - info.min.x) / (info.max.x - info.min.x), + y = (info.value.y - info.min.y) / (info.max.y - info.min.y), + } + + local id = core.generateID() + core.mouse.updateState(id, x,y,w,h) + core.makeTabable(id) + core.registerDraw(id,draw or core.style.Slider2D, fraction, x,y,w,h) + + -- update value + if core.isActive(id) then + core.setKeyFocus(id) + fraction = { + x = (core.mouse.x - x) / w, + y = (core.mouse.y - y) / h, + } + fraction.x = math.min(1, math.max(0, fraction.x)) + fraction.y = math.min(1, math.max(0, fraction.y)) + local v = { + x = fraction.x * (info.max.x - info.min.x) + info.min.x, + y = fraction.y * (info.max.y - info.min.y) + info.min.y, + } + if v.x ~= info.value.x or v.y ~= info.value.y then + info.value = v + return true + end + end + + local changed = false + if core.hasKeyFocus(id) then + if core.keyboard.key == 'down' then + info.value.y = math.min(info.max.y, info.value.y + info.step.y) + changed = true + elseif core.keyboard.key == 'up' then + info.value.y = math.max(info.min.y, info.value.y - info.step.y) + changed = true + end + if core.keyboard.key == 'right' then + info.value.x = math.min(info.max.x, info.value.x + info.step.x) + changed = true + elseif core.keyboard.key == 'left' then + info.value.x = math.max(info.min.x, info.value.x - info.step.x) + changed = true + end + end + return changed +end diff --git a/style-default.lua b/style-default.lua new file mode 100644 index 0000000..7c85e46 --- /dev/null +++ b/style-default.lua @@ -0,0 +1,134 @@ +-- default style +local color = { + normal = {bg = {128,128,128,200}, fg = {59,59,59,200}}, + hot = {bg = {145,153,153,200}, fg = {60,61,54,200}}, + active = {bg = {145,153,153,255}, fg = {60,61,54,255}} +} + +-- load default font +if not love.graphics.getFont() then + love.graphics.setFont(love.graphics.newFont(12)) +end + +local function Button(state, title, x,y,w,h) + local c = color[state] + if state ~= 'normal' then + love.graphics.setColor(c.fg) + love.graphics.rectangle('fill', x+3,y+3,w,h) + end + love.graphics.setColor(c.bg) + love.graphics.rectangle('fill', x,y,w,h) + love.graphics.setColor(c.fg) + local f = love.graphics.getFont() + love.graphics.print(title, x + (w-f:getWidth(title))/2, y + (h-f:getHeight(title))/2) +end + +local function Label(state, text, x,y,w,h,align) + local c = color[state] + love.graphics.setColor(c.fg) + local f = assert(love.graphics.getFont()) + if align == 'center' then + x = x + (w - f:getWidth(text))/2 + y = y + (h - f:getHeight(text))/2 + elseif align == 'right' then + x = x + w - f:getWidth(text) + y = y + h - f:getHeight(text) + end + love.graphics.print(text, x,y) +end + +local function Slider(state, fraction, x,y,w,h, vertical) + local c = color[state] + if state ~= 'normal' then + love.graphics.setColor(c.fg) + love.graphics.rectangle('fill', x+3,y+3,w,h) + end + love.graphics.setColor(c.bg) + love.graphics.rectangle('fill', x,y,w,h) + + love.graphics.setColor(c.fg) + local hw,hh = w,h + if vertical then + hh = h * fraction + y = y + (h - hh) + else + hw = w * fraction + end + love.graphics.rectangle('fill', x,y,hw,hh) +end + +local function Slider2D(state, fraction, x,y,w,h, vertical) + local c = color[state] + if state ~= 'normal' then + love.graphics.setColor(c.fg) + love.graphics.rectangle('fill', x+3,y+3,w,h) + end + love.graphics.setColor(c.bg) + love.graphics.rectangle('fill', x,y,w,h) + + -- draw quadrants + local lw = love.graphics.getLineWidth() + love.graphics.setLineWidth(1) + love.graphics.setColor(c.fg[1], c.fg[2], c.fg[3], math.min(c.fg[4], 127)) + love.graphics.line(x+w/2,y, x+w/2,y+h) + love.graphics.line(x,y+h/2, x+w,y+h/2) + love.graphics.setLineWidth(lw) + + -- draw cursor + local xx = x + fraction.x * w + local yy = y + fraction.y * h + love.graphics.setColor(c.fg) + love.graphics.circle('fill', xx,yy,4,4) +end + +local function Input(state, text, cursor, x,y,w,h) + local c = color[state] + if state ~= 'normal' then + love.graphics.setColor(c.fg) + love.graphics.rectangle('fill', x+3,y+3,w,h) + end + love.graphics.setColor(c.bg) + love.graphics.rectangle('fill', x,y,w,h) + love.graphics.setColor(c.fg) + + local lw = love.graphics.getLineWidth() + love.graphics.setLineWidth(1) + love.graphics.rectangle('line', x,y,w,h) + + local f = love.graphics.getFont() + local th = f:getHeight(text) + local cursorPos = x + 2 + f:getWidth(text:sub(1,cursor)) + + love.graphics.print(text, x+2,y+(h-th)/2) + love.graphics.line(cursorPos, y+4, cursorPos, y+h-4) + + love.graphics.setLineWidth(lw) +end + +local function Checkbox(state, checked, x,y,w,h) + local c = color[state] + if state ~= 'normal' then + love.graphics.setColor(c.fg) + love.graphics.rectangle('fill', x+3,y+3,w,h) + end + love.graphics.setColor(c.bg) + love.graphics.rectangle('fill', x,y,w,h) + love.graphics.setColor(c.fg) + love.graphics.rectangle('line', x,y,w,h) + if checked then + local r = math.max(math.min(w/2,h/2) - 3, 2) + love.graphics.circle('fill', x+w/2,y+h/2,r) + end +end + + +-- the style +return { + color = color, + Button = Button, + Label = Label, + Slider = Slider, + Slider2D = Slider2D, + Input = Input, + Checkbox = Checkbox, +} |