248 lines
7.0 KiB
Lua
248 lines
7.0 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
|
|
-- @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
|