yui/input.lua

241 lines
6.9 KiB
Lua

--- Implements a text input field widget (textfield)
--
-- @classmod yui.Input
-- @copyright 2022, The DoubleFourteen Code Forge
-- @author Lorenzo Cogotti, Andrea Pasquini
--
--- Input widget receives the following callbacks: @{yui.Widget.WidgetCallbacks|onEnter}(), @{yui.Widget.WidgetCallbacks|onChange}(), @{yui.Widget.WidgetCallbacks|onLeave}().
local BASE = (...):gsub('input$', '')
local Widget = require(BASE..'widget')
local core = require(BASE..'core')
local utf8 = require 'utf8'
-- NOTE: Input manages keyboard directly.
local Input = setmetatable({
grabkeyboard = true,
__call = function(cls, args) return cls.new(args) end
}, Widget)
Input.__index = Input
local function split(str, pos)
local ofs = utf8.offset(str, pos) or 0
return str:sub(1, ofs-1), str:sub(ofs)
end
--- Attributes accepted by the @{Input} widget beyond the standard @{yui.Widget.WidgetAttributes|attributes}
-- and @{yui.Widget.WidgetCallbacks|callbacks}.
--
-- @field text (string) text displayed inside the Input
-- @field cornerRadius (number) radius for rounded corners
-- @table InputAttributes
--- Input constructor
-- @param args (@{InputAttributes}) widget attributes
function Input.new(args)
local self = setmetatable(args, Input)
self.text = self.text or ""
self.color = self.color or core.theme.color
self.cornerRadius = self.cornerRadius or core.theme.cornerRadius
self.cursor = math.max(1, math.min(utf8.len(self.text)+1,
self.cursor or utf8.len(self.text)+1))
self.candidate = { text = "", start = 0, length = 0 }
self.drawofs = 0
return self
end
-- NOTE: Input steals keyboard input on focus.
function Input:gainFocus()
love.keyboard.setTextInput(true, self.x,self.y,self.w,self.h)
love.keyboard.setKeyRepeat(true)
end
function Input:loseFocus()
if love.system.getOS() == 'Android' or love.system.getOS() == 'iOS' then
love.keyboard.setTextInput(false)
end
love.keyboard.setKeyRepeat(false)
end
function Input:onPointerInput(px,py, clicked)
if clicked then
self:grabFocus()
-- Schedule cursor reposition for next draw()
self.px = px
self.py = py
end
end
function Input:textedited(text, start, length)
self.candidate.text = text
self.candidate.start = start
self.candidate.length = length
end
function Input:textinput(text)
if text ~= "" then
local a,b = split(self.text, self.cursor)
self.text = table.concat {a, text, b}
self.cursor = self.cursor + utf8.len(text)
end
end
function Input:keypressed(key, code, isrepeat)
if isrepeat == nil then
-- LOVE sends 3 types of keypressed() events,
-- 1. with isrepeat = true
-- 2. with isrepeat = false
-- 3. with isrepeat = nil
--
-- We're only interested in the first 2.
return
end
if self.candidate.length == 0 then
if key == 'backspace' then
local a,b = split(self.text, self.cursor)
self.text = table.concat { split(a, utf8.len(a)), b }
self.cursor = math.max(1, self.cursor-1)
elseif key == 'delete' then
local a,b = split(self.text, self.cursor)
local _,b = split(b, 2)
self.text = table.concat { a, b }
elseif key == 'left' then
self.cursor = math.max(0, self.cursor-1)
elseif key == 'right' then
self.cursor = math.min(utf8.len(self.text)+1, self.cursor+1)
end
end
end
function Input:keyreleased(key)
if self.candidate.length == 0 then
if key == 'home' then
self.cursor = 1
elseif key == 'end' then
self.cursor = utf8.len(self.text)+1
elseif key == 'up' or key == 'down' then
self.ui:navigate(key)
elseif key == 'tab' or key == 'return' then
self.ui:navigate('right')
elseif key == 'escape' then
self.ui:navigate('cancel')
end
end
end
function Input:draw()
-- Cursor position is before the char (including EOS) i.e. in "hello":
-- position 1: |hello
-- position 2: h|ello
-- ...
-- position 6: hello|
local x,y,w,h = self.x,self.y,self.w,self.h
local font = self.font or love.graphics.getFont()
local th = font:getHeight()
local tw = font:getWidth(self.text)
-- Get size of text and cursor position
local cursor_pos = 0
if self.cursor > 1 then
local s = self.text:sub(1, utf8.offset(self.text, self.cursor)-1)
cursor_pos = font:getWidth(s)
end
-- Calculate initial text offset
local wm = math.max(w - 6, 0) -- width minus margin
if cursor_pos - self.drawofs < 0 then
-- cursor left of input box
self.drawofs = cursor_pos
end
if cursor_pos - self.drawofs > wm then
-- cursor right of input box
self.drawofs = cursor_pos - wm
end
if tw - self.drawofs < wm and tw > wm then
-- text bigger than input box, but doesn't fill it
self.drawofs = tw - wm
end
-- Handle cursor movement within the box
if self.px ~= nil then
-- Mouse movement
local mx = self.px - self.x + self.drawofs
self.cursor = utf8.len(self.text) + 1
for c = 1,self.cursor do
local s = self.text:sub(0, utf8.offset(self.text, c)-1)
if font:getWidth(s) >= mx then
self.cursor = c-1
break
end
end
self.px,self.py = nil,nil
end
-- Perform actual draw
core.drawBox(x,y,w,h, self.color.normal, self.cornerRadius)
-- Apply text margins
x = math.min(x + 3, x + w)
-- Set scissors
local sx, sy, sw, sh = love.graphics.getScissor()
love.graphics.setScissor(x-1,y,w+2,h)
-- Move to focused text box region
x = x - self.drawofs
-- Text
love.graphics.setColor(self.color.normal.fg)
love.graphics.setFont(font)
love.graphics.print(self.text, x, y + (h-th)/2)
if self.candidate.length > 0 then
-- Candidate text
local ctw = font:getWidth(self.candidate.text)
love.graphics.setColor(self.color.normal.fg)
love.graphics.print(self.candidate.text, x + tw, y + (h-th)/2)
-- Candidate text rectangle box
love.graphics.rectangle('line', x + tw, y + (h-th)/2, ctw, th)
self.candidate.text = ""
self.candidate.start = 0
self.candidate.length = 0
end
-- Cursor
if self:isFocused() and (love.timer.getTime() % 1) > .5 then
local ct = self.candidate
local ss = ct.text:sub(1, utf8.offset(ct.text, ct.start))
local ws = ct.start > 0 and font:getWidth(ss) or 0
love.graphics.setLineWidth(1)
love.graphics.setLineStyle('rough')
love.graphics.line(x + cursor_pos + ws, y + (h-th)/2,
x + cursor_pos + ws, y + (h+th)/2)
end
-- Reset scissor
love.graphics.setScissor(sx,sy,sw,sh)
end
return Input