Files
gen1recomp/src/ui/kit/VirtualKeyboard.lua
T

283 lines
10 KiB
Lua
Executable File

-- Controller-driven on-screen keyboard (OSK) for handhelds and gamepads.
-- Allows typing URLs (mod index, repositories), mod search queries, and custom names
-- without needing a physical keyboard.
local Theme = require("src.ui.kit.Theme")
local PAL = Theme.PAL
local Kit = nil
local function getKit()
if not Kit then Kit = require("src.ui.kit.Kit") end
return Kit
end
local VirtualKeyboard = {
active = false,
text = "",
title = "Enter Text",
targetId = nil,
onDone = nil,
row = 1,
col = 1,
mode = 1, -- 1: lower, 2: upper, 3: symbols
}
local ROWS_LOWER = {
{ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=" },
{ "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "/", ":" },
{ "a", "s", "d", "f", "g", "h", "j", "k", "l", ".", "_", "@" },
{ "z", "x", "c", "v", "b", "n", "m", "?", "!", "&", "%", "#" },
{ { id = "shift", label = "Shift", w = 2 }, { id = "sym", label = "Sym", w = 2 }, { id = "space", label = "Space", w = 4 }, { id = "back", label = "Bksp", w = 2 }, { id = "done", label = "Done", w = 2 } },
{ { id = "url_https", label = "https://", w = 3 }, { id = "url_http", label = "http://", w = 2 }, { id = "url_json", label = ".json", w = 2 }, { id = "url_zip", label = ".zip", w = 2 }, { id = "clear", label = "Clear", w = 3 } },
}
local ROWS_UPPER = {
{ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=" },
{ "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "/", ":" },
{ "A", "S", "D", "F", "G", "H", "J", "K", "L", ".", "_", "@" },
{ "Z", "X", "C", "V", "B", "N", "M", "?", "!", "&", "%", "#" },
{ { id = "shift", label = "shift", w = 2 }, { id = "sym", label = "Sym", w = 2 }, { id = "space", label = "Space", w = 4 }, { id = "back", label = "Bksp", w = 2 }, { id = "done", label = "Done", w = 2 } },
{ { id = "url_https", label = "https://", w = 3 }, { id = "url_http", label = "http://", w = 2 }, { id = "url_json", label = ".json", w = 2 }, { id = "url_zip", label = ".zip", w = 2 }, { id = "clear", label = "Clear", w = 3 } },
}
local ROWS_SYM = {
{ "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "+" },
{ "~", "`", "{", "}", "[", "]", "|", "\\", ";", ":", "'", "\"" },
{ "<", ">", ",", ".", "/", "?", "=", "-", "*", "/", "\\", "~" },
{ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", ".", "=" },
{ { id = "shift", label = "ABC", w = 2 }, { id = "sym", label = "123", w = 2 }, { id = "space", label = "Space", w = 4 }, { id = "back", label = "Bksp", w = 2 }, { id = "done", label = "Done", w = 2 } },
{ { id = "url_https", label = "https://", w = 3 }, { id = "url_http", label = "http://", w = 2 }, { id = "url_json", label = ".json", w = 2 }, { id = "url_zip", label = ".zip", w = 2 }, { id = "clear", label = "Clear", w = 3 } },
}
local function isLinuxHandheld()
return os.getenv("HANDHELD") == "1"
or os.getenv("PORTMASTER") == "1"
or os.getenv("POKEPORT_HANDHELD") == "1"
or os.getenv("TRIMUI") == "1"
or os.getenv("MUOS") == "1"
or os.getenv("KNULLI") == "1"
or os.getenv("POKEPORT_SBC") == "1"
or os.getenv("ANBERNIC") == "1"
end
function VirtualKeyboard.isSupported()
return isLinuxHandheld()
end
function VirtualKeyboard.open(opts)
if not isLinuxHandheld() then return false end
opts = opts or {}
VirtualKeyboard.active = true
VirtualKeyboard.text = tostring(opts.text or "")
VirtualKeyboard.title = opts.title or "Enter Text"
VirtualKeyboard.targetId = opts.targetId
VirtualKeyboard.onDone = opts.onDone
VirtualKeyboard.row = 1
VirtualKeyboard.col = 1
VirtualKeyboard.mode = 1
return true
end
function VirtualKeyboard.close(confirmed)
if not VirtualKeyboard.active then return end
local txt = VirtualKeyboard.text
local cb = VirtualKeyboard.onDone
VirtualKeyboard.active = false
VirtualKeyboard.targetId = nil
VirtualKeyboard.onDone = nil
if cb then
cb(txt, confirmed == true)
end
end
local function currentRows()
if VirtualKeyboard.mode == 2 then return ROWS_UPPER end
if VirtualKeyboard.mode == 3 then return ROWS_SYM end
return ROWS_LOWER
end
local function triggerKey(key)
if type(key) == "string" then
VirtualKeyboard.text = VirtualKeyboard.text .. key
elseif type(key) == "table" then
if key.id == "shift" then
VirtualKeyboard.mode = (VirtualKeyboard.mode == 2) and 1 or 2
elseif key.id == "sym" then
VirtualKeyboard.mode = (VirtualKeyboard.mode == 3) and 1 or 3
elseif key.id == "space" then
VirtualKeyboard.text = VirtualKeyboard.text .. " "
elseif key.id == "back" then
VirtualKeyboard.text = VirtualKeyboard.text:sub(1, -2)
elseif key.id == "clear" then
VirtualKeyboard.text = ""
elseif key.id == "done" then
VirtualKeyboard.close(true)
elseif key.id == "url_https" then
VirtualKeyboard.text = VirtualKeyboard.text .. "https://"
elseif key.id == "url_http" then
VirtualKeyboard.text = VirtualKeyboard.text .. "http://"
elseif key.id == "url_json" then
VirtualKeyboard.text = VirtualKeyboard.text .. ".json"
elseif key.id == "url_zip" then
VirtualKeyboard.text = VirtualKeyboard.text .. ".zip"
end
end
end
function VirtualKeyboard.gamepadpressed(button)
if not VirtualKeyboard.active then return false end
local rows = currentRows()
local r = VirtualKeyboard.row
local c = VirtualKeyboard.col
if button == "dpup" then
VirtualKeyboard.row = math.max(1, r - 1)
local maxC = #rows[VirtualKeyboard.row]
if VirtualKeyboard.col > maxC then VirtualKeyboard.col = maxC end
return true
elseif button == "dpdown" then
VirtualKeyboard.row = math.min(#rows, r + 1)
local maxC = #rows[VirtualKeyboard.row]
if VirtualKeyboard.col > maxC then VirtualKeyboard.col = maxC end
return true
elseif button == "dpleft" then
VirtualKeyboard.col = math.max(1, c - 1)
return true
elseif button == "dpright" then
VirtualKeyboard.col = math.min(#rows[r], c + 1)
return true
elseif button == "a" then
local key = rows[r] and rows[r][c]
if key then triggerKey(key) end
return true
elseif button == "b" then
VirtualKeyboard.close(false)
return true
elseif button == "x" then
VirtualKeyboard.text = VirtualKeyboard.text:sub(1, -2)
return true
elseif button == "y" then
VirtualKeyboard.text = VirtualKeyboard.text .. " "
return true
elseif button == "leftshoulder" or button == "rightshoulder" then
VirtualKeyboard.mode = (VirtualKeyboard.mode % 3) + 1
local maxC = #currentRows()[VirtualKeyboard.row]
if VirtualKeyboard.col > maxC then VirtualKeyboard.col = maxC end
return true
elseif button == "start" then
VirtualKeyboard.close(true)
return true
end
return true -- consume all input while keyboard modal is up
end
function VirtualKeyboard.keypressed(key)
if not VirtualKeyboard.active then return false end
if key == "escape" then
VirtualKeyboard.close(false)
return true
elseif key == "return" then
VirtualKeyboard.close(true)
return true
elseif key == "backspace" then
VirtualKeyboard.text = VirtualKeyboard.text:sub(1, -2)
return true
end
return false
end
function VirtualKeyboard.textinput(text)
if not VirtualKeyboard.active then return false end
VirtualKeyboard.text = VirtualKeyboard.text .. text
return true
end
function VirtualKeyboard.draw(m)
if not VirtualKeyboard.active then return end
local Kit = getKit()
local s = m and m.s or Kit.scale or 1
local W = (m and m.W) or (love.graphics and love.graphics.getWidth()) or 640
local H = (m and m.H) or (love.graphics and love.graphics.getHeight()) or 480
-- Scrim background
Theme.fill(0, 0, W, H, PAL.bg, 0.92)
Kit.blockClicks = true
local pad = math.floor(12 * s)
local modalW = math.floor(math.min(540 * s, W - 20 * s))
local modalH = math.floor(math.min(360 * s, H - 20 * s))
local px = math.floor((W - modalW) / 2)
local py = math.floor((H - modalH) / 2)
Kit.card(px, py, modalW, modalH, true)
local cy = py + pad
-- Title & hint
Kit.text("button", VirtualKeyboard.title, px + pad, cy, PAL.heading)
Kit.textRight("micro", "L1/R1: Mode X: Bksp Y: Space Start: Done B: Cancel",
px + modalW - pad, cy + 2 * s, PAL.muted)
cy = cy + Kit.textHeight("button") + math.floor(8 * s)
-- Preview textfield box
local fieldH = math.floor(32 * s)
Theme.fillRounded(px + pad, cy, modalW - 2 * pad, fieldH, PAL.bg, 1)
Theme.strokeRounded(px + pad, cy, modalW - 2 * pad, fieldH, PAL.lineStrong, 1, 2)
local shown = Kit.ellipsizeLeft("mono", VirtualKeyboard.text .. "_", modalW - 4 * pad)
Kit.text("mono", shown, px + pad + math.floor(8 * s), cy + math.floor(7 * s), PAL.heading)
cy = cy + fieldH + math.floor(10 * s)
-- Keyboard Rows
local rows = currentRows()
local rowH = math.floor(30 * s)
local gap = math.floor(4 * s)
local availableW = modalW - 2 * pad
for rIdx, rData in ipairs(rows) do
local totalUnits = 0
for _, key in ipairs(rData) do
totalUnits = totalUnits + (type(key) == "table" and (key.w or 1) or 1)
end
local unitW = (availableW - (gap * (#rData - 1))) / totalUnits
local kx = px + pad
for cIdx, key in ipairs(rData) do
local units = type(key) == "table" and (key.w or 1) or 1
local kw = math.floor(unitW * units)
local isSelected = (VirtualKeyboard.row == rIdx and VirtualKeyboard.col == cIdx)
local label = type(key) == "table" and key.label or key
local isSpecial = type(key) == "table"
-- Click / touch detection
if Kit.press(kx, cy, kw, rowH) then
VirtualKeyboard.row = rIdx
VirtualKeyboard.col = cIdx
triggerKey(key)
end
if isSelected then
Theme.fillRounded(kx, cy, kw, rowH, PAL.ink, 1)
Kit.textCenterBold("small", label, kx, cy + math.floor(6 * s), kw, PAL.inverse)
else
Theme.fillRounded(kx, cy, kw, rowH, isSpecial and PAL.raised or PAL.surface, 1)
Theme.strokeRounded(kx, cy, kw, rowH, PAL.line, Theme.A.hairline, 1)
Kit.textCenter("small", label, kx, cy + math.floor(6 * s), kw, isSpecial and PAL.heading or PAL.text)
end
kx = kx + kw + gap
end
cy = cy + rowH + gap
end
Kit.blockClicks = false
end
return VirtualKeyboard