--- 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
-- @table InputAttributes
--- Input constructor
-- @param args (@{InputAttributes}) widget attributes
function Input : new ( args )
self = setmetatable ( args , self )
self.text = self.text or " "
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 )
self : onChange ( self.text )
end
end
function Input : keypressed ( key , _ , 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 ' and self.cursor > 1 then
local a , b = split ( self.text , self.cursor )
self.text = table.concat { split ( a , utf8.len ( a ) ) , b }
self.cursor = self.cursor - 1
self : onChange ( self.text )
elseif key == ' delete ' and self.cursor ~= utf8.len ( self.text ) + 1 then
local a , b = split ( self.text , self.cursor )
_ , b = split ( b , 2 )
self.text = table.concat { a , b }
self : onChange ( self.text )
elseif key == ' left ' then
self.cursor = math.max ( 1 , 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
local moveTo
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
moveTo = key
elseif key == ' tab ' or key == ' return ' then
moveTo = ' right '
elseif key == ' escape ' then
moveTo = ' cancel '
end
if moveTo then
self.cursor = 1 -- reset cursor position
self.ui : navigate ( moveTo )
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 color , font , cornerRadius = core.themeForWidget ( self )
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 , color.normal , 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 + 1 , h )
-- Move to focused text box region
x = x - self.drawofs
-- Text
love.graphics . setColor ( 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 ( 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