From 40dbc7134bae5cff48c2262d92721a3dd5394f84 Mon Sep 17 00:00:00 2001 From: Matthias Richter Date: Tue, 7 Feb 2012 23:10:29 +0100 Subject: Initial commit --- README.md | 47 +++++++++++++++++ button.lua | 27 ++++++++++ checkbox.lua | 19 +++++++ core.lua | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ imgui.love | Bin 0 -> 5053 bytes init.lua | 11 ++++ input.lua | 43 +++++++++++++++ label.lua | 9 ++++ slider.lua | 44 ++++++++++++++++ slider2d.lua | 55 ++++++++++++++++++++ style-default.lua | 134 +++++++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 542 insertions(+) create mode 100644 README.md create mode 100644 button.lua create mode 100644 checkbox.lua create mode 100644 core.lua create mode 100644 imgui.love create mode 100644 init.lua create mode 100644 input.lua create mode 100644 label.lua create mode 100644 slider.lua create mode 100644 slider2d.lua create mode 100644 style-default.lua 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 new file mode 100644 index 0000000..720c9b1 Binary files /dev/null and b/imgui.love differ 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, +} -- cgit v1.2.3-70-g09d2