2026-05-21 00:49:54 +00:00
|
|
|
local ui = {}
|
|
|
|
|
|
|
|
|
|
ui.theme = {
|
|
|
|
|
bg = {0.13, 0.13, 0.14},
|
|
|
|
|
panel = {0.17, 0.17, 0.19},
|
2026-06-05 06:53:15 +00:00
|
|
|
border = {0.28, 0.28, 0.30},
|
2026-05-21 00:49:54 +00:00
|
|
|
borderwidth = 3,
|
|
|
|
|
text = {0.90, 0.90, 0.90},
|
2026-06-05 06:53:15 +00:00
|
|
|
text_dim = {0.50, 0.50, 0.55},
|
|
|
|
|
accent = {0.27, 0.52, 0.90},
|
2026-05-27 00:05:56 +00:00
|
|
|
hover = {0.22, 0.25, 0.35},
|
2026-06-05 06:53:15 +00:00
|
|
|
active = {0.20, 0.40, 0.75},
|
2026-05-21 00:49:54 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-05 11:05:19 +00:00
|
|
|
ui.theme2 = {
|
|
|
|
|
bg = {0.5, 0, 0},
|
|
|
|
|
panel = {0.25, 0, 0},
|
|
|
|
|
border = {1, 0, 0},
|
|
|
|
|
borderwidth = 3,
|
|
|
|
|
text = {0, 0, 1},
|
|
|
|
|
text_dim = {1, 0, 0},
|
|
|
|
|
accent = {0.7, 0, 0},
|
|
|
|
|
hover = {1, 0, 0},
|
|
|
|
|
active = {1, 0, 0},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ui.activeTheme = ui.theme
|
|
|
|
|
|
2026-06-05 06:53:15 +00:00
|
|
|
-- per-frame state (most/all gets reset if i remember)
|
|
|
|
|
ui._wasDown = false
|
|
|
|
|
ui._focused = nil
|
|
|
|
|
ui._inputs = {}
|
|
|
|
|
ui._claimedFocus = false
|
2026-05-27 00:05:56 +00:00
|
|
|
ui._clickConsumed = false
|
2026-06-05 06:53:15 +00:00
|
|
|
ui._elements = {} -- queue of UI elements, processed at end of frame
|
|
|
|
|
ui._hoverConsumed = false -- so it sets hovered to the correct thing
|
2026-05-22 01:38:35 +00:00
|
|
|
|
2026-06-05 06:53:15 +00:00
|
|
|
-- draws a basic bordered panel
|
|
|
|
|
function ui._draw_panel(e)
|
2026-06-05 11:05:19 +00:00
|
|
|
love.graphics.setColor(ui.activeTheme.panel)
|
2026-06-05 06:53:15 +00:00
|
|
|
love.graphics.rectangle("fill", e.x, e.y, e.w, e.h)
|
2026-06-05 11:05:19 +00:00
|
|
|
love.graphics.setColor(ui.activeTheme.border)
|
|
|
|
|
love.graphics.setLineWidth(ui.activeTheme.borderwidth)
|
2026-06-05 06:53:15 +00:00
|
|
|
love.graphics.rectangle("line", e.x, e.y, e.w, e.h)
|
2026-05-22 01:38:35 +00:00
|
|
|
end
|
|
|
|
|
|
2026-06-05 06:53:15 +00:00
|
|
|
-- draws centered, wrapped, clipped text inside a box
|
|
|
|
|
function ui._draw_text(e)
|
|
|
|
|
local font = love.graphics.getFont()
|
|
|
|
|
local _, lines = font:getWrap(e.text, e.w)
|
|
|
|
|
local totalHeight = #lines * font:getHeight()
|
|
|
|
|
local startY = e.y + (e.h - totalHeight) / 2
|
2026-05-22 01:38:35 +00:00
|
|
|
|
2026-06-05 06:53:15 +00:00
|
|
|
love.graphics.setScissor(e.x, e.y, e.w, e.h) -- clip to box bounds
|
2026-06-05 11:05:19 +00:00
|
|
|
love.graphics.setColor(ui.activeTheme.text)
|
2026-06-05 06:53:15 +00:00
|
|
|
love.graphics.printf(e.text, e.x, startY, e.w, "center")
|
|
|
|
|
love.graphics.setScissor() -- reset clip
|
2026-05-22 01:38:35 +00:00
|
|
|
end
|
|
|
|
|
|
2026-06-05 06:53:15 +00:00
|
|
|
-- draws a button, highlighted on hover
|
|
|
|
|
function ui._draw_button(e)
|
|
|
|
|
local mx, my = love.mouse.getPosition()
|
|
|
|
|
local hovered = not ui._hoverConsumed and mx > e.x and mx < e.x + e.w and my > e.y and my < e.y + e.h
|
|
|
|
|
|
2026-06-05 11:05:19 +00:00
|
|
|
love.graphics.setColor(e._hovered and ui.activeTheme.hover or ui.activeTheme.panel)
|
2026-06-05 06:53:15 +00:00
|
|
|
love.graphics.rectangle("fill", e.x, e.y, e.w, e.h)
|
2026-06-05 11:05:19 +00:00
|
|
|
love.graphics.setColor(ui.activeTheme.border)
|
|
|
|
|
love.graphics.setLineWidth(ui.activeTheme.borderwidth)
|
2026-06-05 06:53:15 +00:00
|
|
|
love.graphics.rectangle("line", e.x, e.y, e.w, e.h)
|
|
|
|
|
ui._draw_text(e)
|
2026-05-21 00:49:54 +00:00
|
|
|
end
|
|
|
|
|
|
2026-06-05 06:53:15 +00:00
|
|
|
-- draws a text input, changes appearance based on focus state
|
|
|
|
|
function ui._draw_textInput(e)
|
|
|
|
|
local focused = ui._focused == e.placeholder
|
|
|
|
|
local text = ui._inputs[e.placeholder] or ""
|
|
|
|
|
|
|
|
|
|
if focused then
|
2026-06-05 11:05:19 +00:00
|
|
|
love.graphics.setColor(ui.activeTheme.panel)
|
2026-06-05 06:53:15 +00:00
|
|
|
love.graphics.rectangle("fill", e.x, e.y, e.w, e.h)
|
2026-06-05 11:05:19 +00:00
|
|
|
love.graphics.setColor(ui.activeTheme.accent)
|
|
|
|
|
love.graphics.setLineWidth(ui.activeTheme.borderwidth)
|
2026-06-05 06:53:15 +00:00
|
|
|
love.graphics.rectangle("line", e.x, e.y, e.w, e.h)
|
|
|
|
|
|
|
|
|
|
if text == "" then
|
|
|
|
|
-- show dimmed placeholder text when empty
|
|
|
|
|
local font = love.graphics.getFont()
|
|
|
|
|
local startY = e.y + (e.h - font:getHeight()) / 2
|
2026-06-05 11:05:19 +00:00
|
|
|
love.graphics.setColor(ui.activeTheme.text_dim)
|
2026-06-05 06:53:15 +00:00
|
|
|
love.graphics.printf(e.placeholder, e.x + 4, startY, e.w - 8, "left")
|
|
|
|
|
else
|
|
|
|
|
ui._draw_text({ x=e.x, y=e.y, w=e.w, h=e.h, text=text })
|
|
|
|
|
end
|
2026-05-22 01:38:35 +00:00
|
|
|
else
|
2026-06-05 06:53:15 +00:00
|
|
|
-- unfocused
|
|
|
|
|
local display = text == "" and e.placeholder or text
|
|
|
|
|
ui._draw_button({ x=e.x, y=e.y, w=e.w, h=e.h, text=display })
|
2026-05-22 01:38:35 +00:00
|
|
|
end
|
2026-06-05 06:53:15 +00:00
|
|
|
end
|
2026-05-22 01:59:46 +00:00
|
|
|
|
2026-06-05 06:53:15 +00:00
|
|
|
-- bordered panel, no interaction
|
|
|
|
|
function ui.panel(x, y, w, h)
|
|
|
|
|
table.insert(ui._elements, {
|
|
|
|
|
type = "panel",
|
|
|
|
|
x=x, y=y, w=w, h=h
|
|
|
|
|
})
|
|
|
|
|
end
|
2026-05-22 01:38:35 +00:00
|
|
|
|
2026-06-05 06:53:15 +00:00
|
|
|
-- centered text inside a box, no background
|
|
|
|
|
function ui.text(x, y, w, h, text)
|
|
|
|
|
table.insert(ui._elements, {
|
|
|
|
|
type = "text",
|
|
|
|
|
x=x, y=y, w=w, h=h, text=text
|
|
|
|
|
})
|
2026-05-27 00:05:56 +00:00
|
|
|
end
|
|
|
|
|
|
2026-06-05 06:53:15 +00:00
|
|
|
-- panel with centered text inside (justs draws a panel and puts text ontop, dependant on ui.text)
|
|
|
|
|
function ui.textPanel(x, y, w, h, text)
|
|
|
|
|
table.insert(ui._elements, {
|
|
|
|
|
type = "textPanel",
|
|
|
|
|
x=x, y=y, w=w, h=h, text=text
|
|
|
|
|
})
|
2026-05-21 00:49:54 +00:00
|
|
|
end
|
|
|
|
|
|
2026-06-05 06:53:15 +00:00
|
|
|
-- clickable button. callback is called when clicked (optional)
|
|
|
|
|
-- example: ui.button(x, y, w, h, "Click me", function() print("hi") end)
|
|
|
|
|
function ui.button(x, y, w, h, text, callback)
|
|
|
|
|
table.insert(ui._elements, {
|
|
|
|
|
type = "button",
|
|
|
|
|
x=x, y=y, w=w, h=h, text=text,
|
|
|
|
|
callback = callback
|
|
|
|
|
})
|
|
|
|
|
end
|
2026-05-27 00:05:56 +00:00
|
|
|
|
2026-06-05 06:53:15 +00:00
|
|
|
-- text input field. placeholder is also used as its unique ID
|
|
|
|
|
-- read typed text with: ui._inputs["placeholder"]
|
|
|
|
|
function ui.textInput(x, y, w, h, placeholder)
|
|
|
|
|
table.insert(ui._elements, {
|
|
|
|
|
type = "textInput",
|
|
|
|
|
x=x, y=y, w=w, h=h,
|
|
|
|
|
placeholder = placeholder
|
|
|
|
|
})
|
2026-05-22 01:59:46 +00:00
|
|
|
end
|
2026-05-22 01:38:35 +00:00
|
|
|
|
2026-06-05 06:53:15 +00:00
|
|
|
function ui.process()
|
|
|
|
|
local mx, my = love.mouse.getPosition()
|
|
|
|
|
local clicked = love.mouse.isDown(1) and not ui._wasDown
|
|
|
|
|
for i = #ui._elements, 1, -1 do
|
|
|
|
|
local e = ui._elements[i]
|
|
|
|
|
local inside = mx > e.x and mx < e.x + e.w and my > e.y and my < e.y + e.h
|
|
|
|
|
|
|
|
|
|
-- consume hover for top thing
|
|
|
|
|
if inside and not ui._hoverConsumed then
|
|
|
|
|
e._hovered = true
|
|
|
|
|
ui._hoverConsumed = true
|
|
|
|
|
else
|
|
|
|
|
e._hovered = false
|
|
|
|
|
end
|
|
|
|
|
end
|
2026-05-27 00:05:56 +00:00
|
|
|
|
2026-06-05 06:53:15 +00:00
|
|
|
-- reverse hit test so topmost element gets the click, similar to above but just for clicking
|
|
|
|
|
for i = #ui._elements, 1, -1 do
|
|
|
|
|
local e = ui._elements[i]
|
|
|
|
|
if clicked and not ui._clickConsumed then
|
|
|
|
|
local inside = mx > e.x and mx < e.x + e.w and my > e.y and my < e.y + e.h
|
|
|
|
|
if inside then
|
|
|
|
|
if e.type == "button" then
|
|
|
|
|
if e.callback then e.callback() end -- fire callback if provided
|
|
|
|
|
ui._clickConsumed = true
|
|
|
|
|
|
|
|
|
|
elseif e.type == "textInput" then
|
|
|
|
|
ui._focused = e.placeholder
|
|
|
|
|
ui._claimedFocus = true
|
|
|
|
|
ui._clickConsumed = true
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
2026-05-27 00:05:56 +00:00
|
|
|
end
|
|
|
|
|
|
2026-06-05 06:53:15 +00:00
|
|
|
-- clicked outside everything, clear focus
|
|
|
|
|
if clicked and not ui._claimedFocus then
|
|
|
|
|
ui._focused = nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- draw in order
|
|
|
|
|
for i = 1, #ui._elements do
|
|
|
|
|
local e = ui._elements[i]
|
|
|
|
|
if e.type == "panel" then ui._draw_panel(e)
|
|
|
|
|
elseif e.type == "text" then ui._draw_text(e)
|
|
|
|
|
elseif e.type == "textPanel" then ui._draw_panel(e) ui._draw_text(e)
|
|
|
|
|
elseif e.type == "button" then ui._draw_button(e)
|
|
|
|
|
elseif e.type == "textInput" then ui._draw_textInput(e)
|
2026-05-27 00:05:56 +00:00
|
|
|
end
|
2026-05-24 01:56:00 +00:00
|
|
|
end
|
2026-06-05 06:53:15 +00:00
|
|
|
|
|
|
|
|
-- clear queue/list for next frame (leave at end)
|
|
|
|
|
ui._elements = {}
|
2026-05-24 01:56:00 +00:00
|
|
|
end
|
|
|
|
|
|
2026-06-05 06:53:15 +00:00
|
|
|
function ui.flush()
|
|
|
|
|
ui._wasDown = love.mouse.isDown(1)
|
|
|
|
|
ui._claimedFocus = false
|
|
|
|
|
ui._clickConsumed = false
|
|
|
|
|
ui._hoverConsumed = false
|
|
|
|
|
end
|
2026-05-27 00:05:56 +00:00
|
|
|
|
2026-06-05 06:53:15 +00:00
|
|
|
-- text things, both used in the process of rendering text
|
2026-05-27 00:05:56 +00:00
|
|
|
function ui.textinput_char(t)
|
|
|
|
|
if ui._focused then
|
|
|
|
|
ui._inputs[ui._focused] = (ui._inputs[ui._focused] or "") .. t
|
|
|
|
|
end
|
2026-05-24 01:56:00 +00:00
|
|
|
end
|
|
|
|
|
|
2026-05-27 00:05:56 +00:00
|
|
|
function ui.textinput_backspace()
|
|
|
|
|
if ui._focused then
|
|
|
|
|
local str = ui._inputs[ui._focused] or ""
|
|
|
|
|
ui._inputs[ui._focused] = str:sub(1, -2)
|
2026-05-24 01:56:00 +00:00
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2026-05-21 00:49:54 +00:00
|
|
|
return ui
|