im = { cols = { title_bar = {0.35, 0.35, 0.75}, border = {0.1, 0.1, 0.1}, bg = {0.2, 0.2, 0.2}, hover = {0.5, 0.5, 0.75}, active = {0.15, 0.15, 0.15}, title_text = {0.20, 0.16, 0.35}, text = {0.8, 0.8, 0.8}, }, padding = 1, scroll_speed = 15, slider_handle_width = 5, resize_handle_size = 5, } --- if true, then the focused window is already decided local focus_finalized = false local next_focused --- next focused window local focused_win local wins = {} local current_win local mouse_down = false local mouse_just_up = false local mx, my = 0, 0 local mdx, mdy = 0, 0 local scroll = 0 local layout local layout_i = 1 local max_h = 0 local next_x = 0 local next_y = 0 local next_w = 0 local function rect_contains(x, y, w, h, px, py) return px > x and py > y and px < x + w and py < y + h end local function draw_text(text, font, r, g, b, x, y) lg.setColor(r, g, b) lg.setFont(font) lg.print(text, x, y) end local function draw_rect(mode, x, y, w, h, r, g, b) lg.setColor(r, g, b) lg.rectangle(mode, x, y, w, h) end local function draw_img(img, x, y, quad) lg.setColor(1, 1, 1) if not quad then lg.draw(img, x, y) else lg.draw(img, quad, x, y) end end local function draw_stencil(x, y, w, h) lg.stencil(function() lg.rectangle("fill", x, y, w, h) end, "replace", 1) end local function text_cmd(text, x, y, r, g, b) table.insert(current_win.cmds, { fn = draw_text, args = {text, lg.getFont(), r, g, b, x, y}, }) end local function rect_cmd(x, y, w, h, r, g, b) table.insert(current_win.cmds, { fn = draw_rect, args = {"fill", x, y, w, h, r, g, b}, }) end local function img_cmd(img, x, y, quad) table.insert(current_win.cmds, { fn = draw_img, args = {img, x, y, quad} }) end local function stencil_cmd(x, y, w, h) table.insert(current_win.cmds, { fn = draw_stencil, args = {x, y, w, h}, }) end local function control_stencil(x, y, w, h) local win = current_win local tlx, tly = x, y local brx, bry = x + w, y + h tlx = clamp(tlx, win.cx, win.cx + win.cw) tly = clamp(tly, win.cy, win.cy + win.ch) brx = clamp(brx, win.cx, win.cx + win.cw) bry = clamp(bry, win.cy, win.cy + win.ch) return tlx, tly, math.max(brx - tlx, 0), math.max(bry - tly, 0) end local function reset_stencil() stencil_cmd(current_win.cx, current_win.cy, current_win.cw, current_win.ch) end local function shrink(x, y, w, h, by) x = x + by y = y + by w = w - by * 2 h = h - by * 2 return x, y, w, h end function im.layout(new_layout, start) assert(current_win, "cannot set layout outside of a window") new_layout = new_layout or {1} local proc = {} local last = start or 0 local win_x = current_win.x + im.padding local win_w = current_win.w - im.padding * 2 for i, v in ipairs(new_layout) do assert(v >= last, "layout must be sorted low to high") local w = win_w * (v - last) - im.padding * 2 proc[i] = {x=win_x + win_w * last + im.padding, w=w} last = v end layout = proc layout_i = 1 im.next_position(0) end function im.next_position(h) h = h or 0 local x, y, w = next_x, next_y, next_w max_h = math.max(max_h, h) if layout_i > #layout then -- no more columns next_y = next_y + max_h + im.padding current_win.content_max = math.max( current_win.content_max, next_y + current_win.scroll - current_win.cy) layout_i = 1 max_h = 0 end next_x = layout[layout_i].x next_w = layout[layout_i].w layout_i = layout_i + 1 return x, y, w end function im.image(img, quad) assert(current_win, "a window must be active") local w, h = img:getDimensions() if quad then _, _, w, h = quad:getViewport() end local x, y, max_w = im.next_position(h) w = math.min(w, max_w) stencil_cmd(control_stencil(x, y, w, h)) img_cmd(img, round(x), round(y), quad) reset_stencil() end function im.separator() assert(current_win, "a window must be active") local x, y, w = im.next_position(1) rect_cmd(x, y, w, 1, unpack(im.cols.border)) end function im.text(text, r, g, b) assert(current_win, "a window must be active") r = r or im.cols.text[1] g = g or im.cols.text[2] b = b or im.cols.text[3] local font = lg.getFont() local h = font:getHeight() local x, y, w = im.next_position(h) stencil_cmd(control_stencil(x, y, w, h)) text_cmd(text, x, y, r, g, b) reset_stencil() end function im.button(text, r, g, b) assert(current_win, "a window must be active") r = r or im.cols.text[1] g = g or im.cols.text[2] b = b or im.cols.text[3] local font = lg.getFont() local text_w, text_h = font:getWidth(text), font:getHeight() + im.padding local w, h = text_w * 1.1, text_h * 1.1 local x, y = im.next_position(h) stencil_cmd(control_stencil(x, y, w, h)) rect_cmd(x, y, w, h, unpack(im.cols.border)) x, y, w, h = shrink(x, y, w, h, 1) local col = im.cols.bg local pressed = false if current_win == focused_win and rect_contains(x, y, w, h, mx, my) then col = im.cols.hover if mouse_just_up then col = im.cols.active pressed = true end end rect_cmd(x, y, w, h, unpack(col)) local text_x, text_y = x + w / 2 - text_w / 2, y + h / 2 - text_h / 2 text_cmd(text, text_x, text_y, r, g, b) reset_stencil() local ret = pressed return ret end function im.slider(val, min, max, step, h) assert(current_win, "a window must be active") assert(max > min, "min must be lesser than max") step = step or 0.1 h = h or lg.getFont():getHeight() local p = (val - min) / (max - min) local x, y, w = im.next_position(h) rect_cmd(x, y, w, h, unpack(im.cols.border)) x, y, w, h = shrink(x, y, w, h, 1) rect_cmd(x, y, w, h, unpack(im.cols.bg)) local max_w = w - im.slider_handle_width local handle = max_w * p local col = im.cols.bg if rect_contains(x, y, w, h, mx - mdx, my - mdy) and current_win == focused_win then col = im.cols.hover if mouse_down then col = im.cols.active handle = clamp(mx - x, 0, max_w) end end rect_cmd(x, y, handle, h, unpack(im.cols.hover)) x = x + handle w = im.slider_handle_width y = y h = h rect_cmd(x, y, w, h, unpack(im.cols.border)) x, y, w, h = shrink(x, y, w, h, 1) rect_cmd(x, y, w, h, unpack(col)) p = handle / max_w return snap(clamp(p * (max - min) + min, min, max), step) end function im.begin_window(title, x, y, w, h, opts) assert(not current_win, "window is already active") opts = opts or {} local win = wins[title] if not win then win = { win = true, x = x, y = y, w = w, h = h, opts = opts, content_max = 0, scroll = 0, open = true, } wins[title] = win table.insert(wins, win) win.idx = #wins end win.cmds = {} if rect_contains(win.x, win.y, win.w, win.h, mx, my) then if (not next_focused or next_focused.idx < win.idx) and not focus_finalized then next_focused = win end if mouse_down and focused_win == win then if wins[#wins] ~= win then local last = table.remove(wins) wins[win.idx] = last last.idx = win.idx table.insert(wins, win) win.idx = #wins end end end if not win.open then return false end current_win = win local font = lg.getFont() local title_x, title_y = win.x + 1, win.y + 1 local title_w, title_h = win.w - 2, font:getHeight() + im.padding if focused_win == win and rect_contains(title_x, title_y, title_w, title_h, mx - mdx, my - mdy) and mouse_down then next_focused = current_win focus_finalized = true win.x = round(win.x + mdx) win.y = round(win.y + mdy) title_x, title_y = win.x + 1, win.y + 1 end stencil_cmd(0, 0, love.graphics.getDimensions()) rect_cmd(win.x, win.y, win.w, win.h, unpack(im.cols.border)) stencil_cmd(win.x, win.y, win.w, win.h) rect_cmd(title_x, title_y, title_w, title_h, unpack(im.cols.title_bar)) text_cmd(title, title_x + im.padding, title_y, unpack(im.cols.title_text)) local cx, cy = win.x + 1, win.y + title_h local cw, ch = win.w - 2, win.h - title_h - 1 win.cx, win.cy, win.cw, win.ch = cx, cy, cw, ch rect_cmd(cx, cy, cw, ch, unpack(im.cols.bg)) -- resizing if focused_win == win then local o = im.resize_handle_size / 2 local hs = im.resize_handle_size if rect_contains(cx + cw - o, cy + ch - o, hs, hs, mx - mdx, my - mdy) or rect_contains(cx, cy + ch - o, cw, hs, mx - mdx, my - mdy) or rect_contains(cx + cw - o, cy, hs, ch, mx - mdx, my - mdy) then if mouse_down then next_focused = current_win focus_finalized = true win.w = win.w + mdx win.h = win.h + mdy end rect_cmd(cx + cw, cy, 1, ch + 1, unpack(im.cols.hover)) rect_cmd(cx, cy + ch, cw, 1, unpack(im.cols.hover)) end end win.w = math.max(win.w, 30) win.h = math.max(win.h, title_h + 30) stencil_cmd(cx, cy, cw, ch) if rect_contains(cx, cy, cw, ch, mx, my) then if win.content_max > win.ch then win.scroll = clamp(win.scroll - scroll, 0, win.content_max - win.ch) else win.scroll = 0 end win.content_max = 0 end -- reset layout im.layout() next_x, next_y = cx + im.padding, cy - win.scroll return true end function im.end_window() assert(current_win, "a window must be active to end one") reset_stencil() current_win = nil end function im.begin_step() end function im.end_step() mdx, mdy = 0, 0 mouse_just_up = false scroll = 0 focused_win = next_focused next_focused = nil focus_finalized = false end function im.draw() lg.setStencilTest("greater", 0) for _, win in ipairs(wins) do for _, cmd in ipairs(win.cmds) do cmd.fn(unpack(cmd.args)) end win.cmds = {} end lg.setStencilTest() end function im.has_focus() -- double not to ensure boolean :) return not not focused_win end function im.mousereleased(_, _, btn) if btn == 1 then mouse_down = false mouse_just_up = true end end function im.mousepressed(_, _, btn) if btn == 1 then mouse_down = true end end function im.mousemoved(x, y, dx, dy) mx, my = x, y -- accumlate for processing in step mdx = mdx + dx mdy = mdy + dy end function im.wheelmoved(_, y) scroll = scroll + y * im.scroll_speed end