--[[============================================================
--=
--=  InputField v3.3.1 - text input handling library for LÖVE (0.10.2+)
--=  - Written by Marcus 'ReFreezed' Thunström
--=  - MIT License (See the bottom of this file)
--=  - https://github.com/ReFreezed/InputField
--=
--==============================================================

	1. Functions
	2. Enums
	3. Basic usage



	1. Functions
	----------------------------------------------------------------

	-- Search through the file for more info about each individual function.

	-- Settings:
	getAlignment, setAlignment
	getDimensions, getWidth, getHeight, setDimensions, setWidth, setHeight
	getDoubleClickMaxDelay, setDoubleClickMaxDelay
	getFilter, setFilter
	getFont, setFont
	getMaxHistory, setMaxHistory
	getMouseScrollSpeed, getMouseScrollSpeedX, getMouseScrollSpeedY, setMouseScrollSpeed, setMouseScrollSpeedX, setMouseScrollSpeedY
	getType, setType, isPassword, isMultiline
	getWheelScrollSpeed, getWheelScrollSpeedX, getWheelScrollSpeedY, setWheelScrollSpeed, setWheelScrollSpeedX, setWheelScrollSpeedY
	isEditable, setEditable
	isFontFilteringActive, setFontFilteringActive

	-- Events:
	update
	mousepressed, mousemoved, mousereleased, wheelmoved
	keypressed, textinput

	-- Other:
	canScroll, canScrollHorizontally, canScrollVertically
	clearHistory
	eachVisibleLine, eachSelection, eachSelectionOptimized
	getBlinkPhase, resetBlinking
	getCursor, setCursor, moveCursor, getCursorSelectionSide, getAnchorSelectionSide
	getCursorLayout
	getInfoAtCoords, getInfoAtCursor, getInfoAtCharacter
	getScroll, getScrollX, getScrollY, setScroll, setScrollX, setScrollY, scroll, scrollToCursor
	getScrollHandles, getScrollHandleHorizontal, getScrollHandleVertical
	getScrollLimits
	getSelection, setSelection, selectAll, getSelectedText, getSelectedVisibleText
	getText, setText, getVisibleText, insert
	getTextDimensions, getTextWidth, getTextHeight
	getTextLength
	getTextOffset
	getVisibleLine, getVisibleLineCount
	isBusy
	releaseMouse
	reset



	2. Enums
	----------------------------------------------------------------

	InputFieldType
		"normal"      -- Simple single-line input.
		"password"    -- Single-line input where all characters are obscured.
		"multiwrap"   -- Multi-line input where text wraps by the width of the field.
		"multinowrap" -- Multi-line input with no wrapping.

	SelectionSide
		"start" -- The start (left side) of the text selection.
		"end"   -- The end (right side) of the text selection.

	TextCursorAlignment
		"left"  -- Align cursor to the left.
		"right" -- Align cursor to the right.

	TextAlignment
		"left"   -- Align text to the left.
		"right"  -- Align text to the right.
		"center" -- Align text to the center.



	3. Basic usage
	----------------------------------------------------------------

	local InputField = require("InputField")
	local field      = InputField("Initial text.")

	local fieldX = 80
	local fieldY = 50

	love.keyboard.setKeyRepeat(true)

	function love.keypressed(key, scancode, isRepeat)
		field:keypressed(key, isRepeat)
	end
	function love.textinput(text)
		field:textinput(text)
	end

	function love.mousepressed(mx, my, mbutton, pressCount)
		field:mousepressed(mx-fieldX, my-fieldY, mbutton, pressCount)
	end
	function love.mousemoved(mx, my)
		field:mousemoved(mx-fieldX, my-fieldY)
	end
	function love.mousereleased(mx, my, mbutton)
		field:mousereleased(mx-fieldX, my-fieldY, mbutton)
	end
	function love.wheelmoved(dx, dy)
		field:wheelmoved(dx, dy)
	end

	function love.draw()
		love.graphics.setColor(0, 0, 1)
		for _, x, y, w, h in field:eachSelection() do
			love.graphics.rectangle("fill", fieldX+x, fieldY+y, w, h)
		end

		love.graphics.setColor(1, 1, 1)
		for _, text, x, y in field:eachVisibleLine() do
			love.graphics.print(text, fieldX+x, fieldY+y)
		end

		local x, y, h = field:getCursorLayout()
		love.graphics.rectangle("fill", fieldX+x, fieldY+y, 1, h)
	end



--============================================================]]

local InputField = {
	_VERSION = "InputField 3.3.1",
}



local LK   = require"love.keyboard"
local LS   = require"love.system"
local LT   = require"love.timer"
local utf8 = require"utf8"

local isMac = (LS.getOS() == "OS X")

local InputField   = {}
InputField.__index = InputField



--[[ @Debug macOS on Windows.
isMac         = true
local _isDown = LK.isDown

function LK.isDown(...)
	for i = 1, select("#", ...) do
		local key = select(i, ...)

		if     key == "lgui"  then  key = "lctrl"
		elseif key == "rgui"  then  key = "rctrl"
		elseif key == "lctrl" then  key = "application"
		elseif key == "rctrl" then  key = "application"
		end

		if _isDown(key) then  return true  end
	end

	return false
end
--]]



--==============================================================
--= Internal functions =========================================
--==============================================================



local function noop() end



local function clamp(n, min, max)
	return math.max(math.min(n, max), min)
end



local function nextCodepoint(s, i)
	if i > 0 then
		local b = s:byte(i)
		if not b then
			return
		elseif b >= 240 then
			i = i + 4
		elseif b >= 224 then
			i = i + 3
		elseif b >= 192 then
			i = i + 2
		else
			i = i + 1
		end
	elseif i == 0 then
		i = 1
	elseif i < 0 then
		return
	end
	if i > #s then  return  end

	return i, utf8.codepoint(s, i)
end

local function utf8Codes(s) -- We don't use utf8.codes() as it creates garbage!
	return nextCodepoint, s, 0
end

local function utf8GetEndOffset(line, pos)
	return (utf8.offset(line, pos+1) or #line+1) - 1
end



local function cleanString(field, s)
	local isMultiline = field:isMultiline()
	s                 = s:gsub((isMultiline and "[%z\1-\9\11-\31]+" or "[%z\1-\31]+"), "") -- Should we allow horizontal tab?

	if field.fontFilteringIsActive then
		local font      = field.font
		local hasGlyphs = font.hasGlyphs

		s = s:gsub(utf8.charpattern, function(c)
			if not (hasGlyphs(font, c) or (c == "\n" and isMultiline)) then
				return ""
			end
		end)
	end

	return s
end



--
-- boundPosition = getNextWordBound    ( string, startPosition, direction )
-- boundPosition = getNextHardLineBound( string, startPosition, direction )
-- direction     = -1 | 1
--
-- Cursor behavior examples when navigating by word:
--   a|a bb  ->  aa| bb
--   aa| bb  ->  aa bb|
--   aa |bb  ->  aa bb|
--   cc| = dd+ee  ->  cc =| dd+ee
--   cc =| dd+ee  ->  cc = dd|+ee
--   cc = dd|+ee  ->  cc = dd+|ee
--   f|f(-88  ->  ff|(-88
--   ff|(-88  ->  ff(-|88
--   ff(|-88  ->  ff(-|88
--
local getNextWordBound, getNextHardLineBound
do
	local function newSet(values)
		local set = {}
		for _, v in ipairs(values) do
			set[v] = true
		end
		return set
	end

	local ASCII_PUNCTUATION = "!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~"; ASCII_PUNCTUATION = newSet{ ASCII_PUNCTUATION:byte(1, #ASCII_PUNCTUATION) }
	local ASCII_WHITESPACE  = newSet{ 9--[[,10]],11,12,13,32 } -- Horizontal tab, line feed, vertical tab, form feed, carriage return, space.

	-- Generated by tools/generateUnicodeInfo.lua:

	-- PUNCTUATION (8330, 97+230)
	local UNICODE_PUNCTUATION = newSet{180,187,191,215,247,749,885,894,903,1014,1154,1470,1472,1475,1478,1563,1748,1758,1769,2142,2416,2557,2678,2928,3191,3199,3204,3407,3449,
		3572,3647,3663,3892,3894,3896,3973,4347,5120,6464,7379,8125,8468,8485,8487,8489,8494,8527,11632,12336,12448,12539,12880,42611,42622,43260,43359,43867,44011,64297,
		65952,66463,66512,66927,67671,67871,67903,68223,68296,69293,70093,70107,70313,70749,70854,71739,72162,73727,92917,94178,113820,113823,119365,120513,120539,120571,120597,120629,120655,120687,
		120713,120745,120771,123215,123647,126124,126128,126254}
	local ranges = {161,169,171,172,174,177,182,184,706,709,722,735,741,747,751,767,900,901,1370,1375,1417,1418,
		1421,1423,1523,1524,1542,1551,1566,1567,1642,1645,1789,1790,1792,1805,2038,2041,2046,2047,2096,2110,2404,2405,2546,2547,
		2554,2555,2800,2801,3059,3066,3674,3675,3841,3863,3866,3871,3898,3901,4030,4037,4039,4044,4046,4058,4170,4175,4254,4255,
		4960,4968,5008,5017,5741,5742,5787,5788,5867,5869,5941,5942,6100,6102,6104,6107,6144,6154,6468,6469,6622,6655,6686,6687,
		6816,6822,6824,6829,7002,7018,7028,7036,7164,7167,7227,7231,7294,7295,7360,7367,8127,8129,8141,8143,8157,8159,8173,8175,
		8189,8190,8208,8231,8240,8286,8314,8318,8330,8334,8352,8383,8448,8449,8451,8454,8456,8457,8470,8472,8478,8483,8506,8507,
		8512,8516,8522,8525,8586,8587,8592,9254,9280,9290,9372,9449,9472,10101,10132,11123,11126,11157,11159,11263,11493,11498,11513,11516,
		11518,11519,11776,11822,11824,11858,11904,11929,11931,12019,12032,12245,12272,12283,12289,12292,12296,12320,12342,12343,12349,12351,12443,12444,
		12688,12689,12694,12703,12736,12771,12800,12830,12842,12871,12896,12927,12938,12976,12992,13311,19904,19967,42128,42182,42238,42239,42509,42511,
		42738,42743,42752,42774,42784,42785,42889,42890,43048,43051,43062,43065,43124,43127,43214,43215,43256,43258,43310,43311,43457,43469,43486,43487,
		43612,43615,43639,43641,43742,43743,43760,43761,43882,43883,64434,64449,64830,64831,65020,65021,65040,65049,65072,65106,65108,65126,65128,65131,
		65281,65295,65306,65312,65339,65344,65371,65381,65504,65510,65512,65518,65532,65533,65792,65794,65847,65855,65913,65929,65932,65934,65936,65948,
		66000,66044,67703,67704,68176,68184,68336,68342,68409,68415,68505,68508,69461,69465,69703,69709,69819,69820,69822,69825,69952,69955,70004,70005,
		70085,70088,70109,70111,70200,70205,70731,70735,70746,70747,71105,71127,71233,71235,71264,71276,71484,71487,72004,72006,72255,72262,72346,72348,
		72350,72354,72769,72773,72816,72817,73463,73464,73685,73713,74864,74868,92782,92783,92983,92991,92996,92997,93847,93850,118784,119029,119040,119078,
		119081,119140,119146,119148,119171,119172,119180,119209,119214,119272,119296,119361,119552,119638,120832,121343,121399,121402,121453,121460,121462,121475,121477,121483,
		125278,125279,126704,126705,126976,127019,127024,127123,127136,127150,127153,127167,127169,127183,127185,127221,127245,127405,127462,127490,127504,127547,127552,127560,
		127568,127569,127584,127589,127744,128727,128736,128748,128752,128764,128768,128883,128896,128984,128992,129003,129024,129035,129040,129095,129104,129113,129120,129159,
		129168,129197,129200,129201,129280,129400,129402,129483,129485,129619,129632,129645,129648,129652,129656,129658,129664,129670,129680,129704,129712,129718,129728,129730,
		129744,129750,129792,129938,129940,129994}
	for i = 1, #ranges, 2 do  for cp = ranges[i], ranges[i+1] do UNICODE_PUNCTUATION[cp] = true end  end

	-- WHITESPACE (18, 5+2)
	local UNICODE_WHITESPACE = newSet{160,5760,8239,8287,12288}
	local ranges = {8192,8202,8232,8233}
	for i = 1, #ranges, 2 do  for cp = ranges[i], ranges[i+1] do UNICODE_WHITESPACE[cp] = true end  end



	local function getCodepointCharType(cp)
		return ASCII_PUNCTUATION[cp]   and "punctuation"
		    or cp == 10                and "newline"
		    or ASCII_WHITESPACE[cp]    and "whitespace"
		    or cp <= 127               and "word"
		    or UNICODE_PUNCTUATION[cp] and "punctuation"
		    or UNICODE_WHITESPACE[cp]  and "whitespace"
		    or                             "word"
	end

	local codepoints = {}

	function getNextWordBound(s, pos, direction)
		local len = 0

		for _, cp in utf8Codes(s) do
			len             = len + 1
			codepoints[len] = cp
		end
		for i = len+1, #codepoints do
			codepoints[i] = nil
		end

		pos = clamp(pos, 0, len)
		if direction < 0 then  pos = pos+1  end

		while true do
			pos = pos + direction

			-- Check for end of string.
			local prevCp = codepoints[pos]
			local nextCp = codepoints[pos+direction]

			if not (prevCp and nextCp) then
				pos = pos + direction
				break
			end

			-- Check for word bound.
			local prevType = getCodepointCharType(prevCp)
			local nextType = getCodepointCharType(nextCp)

			if (nextType ~= prevType and not (nextType ~= "whitespace" and prevType == "whitespace")) or nextType == "newline" then
				if direction < 0 then  pos = pos-1  end
				break
			end
		end

		return clamp(pos, 0, len)
	end

	function getNextHardLineBound(s, pos, direction)
		local len = 0

		for _, cp in utf8Codes(s) do
			len             = len + 1
			codepoints[len] = cp
		end
		for i = len+1, #codepoints do
			codepoints[i] = nil
		end

		while codepoints[pos] or codepoints[pos+direction] do
			pos = pos + direction

			if (codepoints[pos] or 10) == 10 then
				if direction > 0 then  pos = pos-1  end
				break
			end
		end

		return clamp(pos, 0, len)
	end
end



local function updateWrap(field)
	local text = field:getVisibleText()
	if field.lastWrappedText == text then  return  end

	field.lastWrappedText = text

	if field:isMultiline() then
		field.wrappedWidth = 0

		local wrapWidth = (field.type == "multiwrap") and field.width or 1/0
		local lineCount = 0
		local processed = 0

		for line, i in text:gmatch"([^\n]*)()" do
			if i > processed then
				processed = i

				if line == "" then
					lineCount                    = lineCount + 1
					field.wrappedText[lineCount] = ""
					field.softBreak[lineCount]   = false

				else
					local w, subLines  = field.font:getWrap(line, wrapWidth)
					local subLineCount = #subLines

					for subLineI, subLine in ipairs(subLines) do
						lineCount                    = lineCount + 1
						field.wrappedText[lineCount] = subLine
						field.softBreak[lineCount]   = subLineI < subLineCount
					end

					field.wrappedWidth = math.max(field.wrappedWidth, w)
				end
			end
		end

		for lineI = lineCount+1, #field.wrappedText do
			field.wrappedText[lineI] = nil
			field.softBreak[lineI]   = nil
		end

	else
		field.wrappedText[1] = text
		field.softBreak[1]   = false
		field.wrappedWidth   = field.font:getWidth(text)

		for lineI = 2, #field.wrappedText do
			field.wrappedText[lineI] = nil
			field.softBreak[lineI]   = nil
		end
	end

	--[[ @Debug
	print("--------------------------------")
	for i = 1, #field.wrappedText do
		print(i, field.softBreak[i], field.wrappedText[i])
	end
	--]]
end



local function getLineAlignmentOffset(field, line)
	if     field.alignment == "left"        then  return 0
	elseif field.width     == 1/0           then  return 0
	elseif field.type      == "multinowrap" then  return 0
	elseif field.alignment == "right"       then  return math.max(field.width-field.font:getWidth(line), 0)
	elseif field.alignment == "center"      then  return math.floor(.5 * math.max(field.width-field.font:getWidth(line), 0))
	else
		error(field.alignment)
	end
end

local function alignOnLine(field, line, x)
	return x + getLineAlignmentOffset(field, line)
end

local function unalignOnLine(field, line, x)
	return x - getLineAlignmentOffset(field, line)
end

-- cursorPosition, side = getCursorPositionAtX(field, line, x)
-- side                 = -1 | 1
local function getCursorPositionAtX(field, line, x)
	x = unalignOnLine(field, line, x)

	if line == "" or x <= 0 then
		return 0
	elseif x <= field.font:getWidth(line:sub(1, utf8GetEndOffset(line, 1)))/2 then
		return 0
	end

	local lineW = field.font:getWidth(line)
	if x >= lineW then
		return utf8.len(line)
	end

	-- Binary search.
	local posL = 1
	local posR = utf8.len(line)

	local closestDist = math.abs(x - lineW)
	local closestPos  = posR

	while posL < posR do
		local pos      = math.floor((posL+posR)/2)
		local linePart = line:sub(1, utf8GetEndOffset(line, pos)) -- @Memory (We could maybe use font:getKerning() in LÖVE 11.4+ to traverse one character at a time. Might be slower though.)

		local dx = x - field.font:getWidth(linePart)
		if dx == 0 then  return pos  end -- x is exactly at the cursor's position.

		local dist = math.abs(dx)

		if dist < closestDist then
			closestDist = dist
			closestPos  = pos
		end

		if dx < 0 then
			posR = pos
		else
			posL = pos
			if posL == posR-1 then  break  end -- Because pos is rounded down we'd get stuck without this (as pos would be posL again and again).
		end
	end

	return closestPos
end

local function getLineStartPosition(field, targetLineI)
	local linePos1 = 1

	for lineI = 1, targetLineI-1 do
		linePos1 = linePos1 + utf8.len(field.wrappedText[lineI])

		if not field.softBreak[lineI] then
			linePos1 = linePos1 + 1
		end
	end

	return linePos1
end

-- cursorPosition, visualLineIndex, visualLineIndexUnclamped = getCursorPositionAtCoordinates( field, x, y )
local function getCursorPositionAtCoordinates(field, x, y)
	updateWrap(field)

	if not field:isMultiline() then
		return getCursorPositionAtX(field, field.wrappedText[1], x), 1, 1
	end

	local fontH          = field.font:getHeight()
	local lineDist       = math.ceil(fontH*field.font:getLineHeight())
	local lineSpace      = lineDist - fontH
	local lineIUnclamped = math.floor(1 + (y+lineSpace/2) / lineDist)
	local lineI          = clamp(lineIUnclamped, 1, #field.wrappedText)
	local line           = field.wrappedText[lineI]

	if not line then  return 0, 0, lineIUnclamped  end

	local linePos1  = getLineStartPosition(field, lineI)
	local posOnLine = getCursorPositionAtX(field, line, x)

	return linePos1+posOnLine-1, lineI, lineIUnclamped
end

-- line, positionOnLine, lineIndex, linePosition1, linePosition2 = getLineInfoAtPosition( field, position )
local function getLineInfoAtPosition(field, pos)
	updateWrap(field)

	pos            = math.min(pos, utf8.len(field.text))
	local linePos1 = 1

	for lineI, line in ipairs(field.wrappedText) do
		local linePos2  = linePos1 + utf8.len(line) - 1
		local softBreak = field.softBreak[lineI]

		if pos <= (softBreak and linePos2-1 or linePos2) then -- Any trailing newline counts as being on the next line.
			return line, pos-linePos1+1, lineI, linePos1, linePos2
		end

		linePos1 = linePos2 + (softBreak and 1 or 2) -- Jump over any newline.
	end

	-- We should never get here!
	return getLineInfoAtPosition(field, 0)
end



local LCTRL = isMac and "lgui" or "lctrl"
local RCTRL = isMac and "rgui" or "rctrl"

-- modKeys = getModKeys( )
-- modKeys = "cas" | "ca" | "cs" | "as" | "c" | "a" | "s" | "" | "^cas" | "^ca" | "^cs" | "^as" | "^c" | "^a" | "^s" | "^"
local function getModKeys()
	local c = LK.isDown(LCTRL,    RCTRL   )
	local a = LK.isDown("lalt",   "ralt"  )
	local s = LK.isDown("lshift", "rshift")

	if isMac and LK.isDown("lctrl", "rctrl") then
		if     c and a and s then  return "^cas"
		elseif c and a       then  return "^ca"
		elseif c and s       then  return "^cs"
		elseif a and s       then  return "^as"
		elseif c             then  return "^c"
		elseif a             then  return "^a"
		elseif s             then  return "^s"
		else                       return "^"  end
	end

	if     c and a and s then  return "cas"
	elseif c and a       then  return "ca"
	elseif c and s       then  return "cs"
	elseif a and s       then  return "as"
	elseif c             then  return "c"
	elseif a             then  return "a"
	elseif s             then  return "s"
	else                       return ""  end
end



local function limitScroll(field)
	local limitX, limitY = field:getScrollLimits()
	field.scrollX        = clamp(field.scrollX, 0, limitX)
	field.scrollY        = clamp(field.scrollY, 0, limitY)
end



local function applyFilter(field, s)
	local filterFunc = field.filterFunction
	if not filterFunc then  return s  end

	s = s:gsub(utf8.charpattern, function(c)
		if filterFunc(c) then  return ""  end
	end)

	return s
end



-- pushHistory( field, group|nil )
local function pushHistory(field, group)
	local history = field.editHistory
	local i, state

	-- Never save history for password fields.
	if field.type == "password" then
		i     = 1
		state = history[1]

	-- Update last entry if group matches.
	elseif group and group == field.editHistoryGroup then
		i     = field.editHistoryIndex
		state = history[i]

	-- History limit reached.
	elseif field.editHistoryIndex >= field.editHistoryMax then
		i          = field.editHistoryIndex
		state      = table.remove(history, 1)
		history[i] = state

	-- Expand history.
	else
		i          = field.editHistoryIndex + 1
		state      = {}
		history[i] = state
	end

	for i = i+1, #history do
		history[i] = nil
	end

	state.text           = field.text
	state.cursorPosition = field.cursorPosition
	state.selectionStart = field.selectionStart
	state.selectionEnd   = field.selectionEnd

	field.editHistoryIndex = i
	field.editHistoryGroup = group

	field.navigationTargetX = nil
end



local function finilizeHistoryGroup(field)
	field.editHistoryGroup = nil
end



local function applyHistoryState(field, offset)
	field.editHistoryIndex = field.editHistoryIndex + offset

	local state = field.editHistory[field.editHistoryIndex] or assert(false)

	field.text           = state.text
	field.cursorPosition = state.cursorPosition
	field.selectionStart = state.selectionStart
	field.selectionEnd   = state.selectionEnd

	field.navigationTargetX = nil
end

-- @UX: Improve how the cursor and selection are restored on undo.
local function undoEdit(field)
	if field.editHistoryIndex == 1 then  return false  end

	finilizeHistoryGroup(field)
	applyHistoryState(field, -1)

	field:resetBlinking()
	field:scrollToCursor()

	return true
end

local function redoEdit(field)
	if field.editHistoryIndex == #field.editHistory then  return false  end

	finilizeHistoryGroup(field)
	applyHistoryState(field, 1)

	field:resetBlinking()
	field:scrollToCursor()

	return true
end



--==============================================================
--= Exported functions =========================================
--==============================================================



-- InputField( [ initialText="", type:InputFieldType="normal" ] )
local function newInputField(text, fieldType)
	if not (fieldType == nil or fieldType == "normal" or fieldType == "password" or fieldType == "multiwrap" or fieldType == "multinowrap") then
		error("[InputField] Invalid field type '"..tostring(fieldType).."'.", 2)
	end
	fieldType = fieldType or "normal"

	local field = setmetatable({
		type = fieldType,

		blinkTimer = LT.getTime(),

		cursorPosition = 0,
		selectionStart = 0,
		selectionEnd   = 0,

		clickCount               = 1, -- This and multiClickMaxDelay are only used if the 'pressCount' argument isn't supplied to mousepressed().
		multiClickMaxDelay       = 0.40,
		multiClickExpirationTime = 0.00,
		multiClickLastX          = 0.0,
		multiClickLastY          = 0.0,

		editHistoryMax   = 100,
		editHistory      = {},
		editHistoryIndex = 1,
		editHistoryGroup = nil, -- nil | "insert" | "remove"

		font                  = require"love.graphics".getFont(),
		fontFilteringIsActive = false,

		filterFunction = nil,

		mouseScrollSpeedX = 6.0, -- Per pixel per second.
		mouseScrollSpeedY = 8.0,

		wheelScrollSpeedX = 1.0, -- Per second. Relative to the font size.
		wheelScrollSpeedY = 1.0,

		dragMode           = "", -- "" | "character" | "word" | "line"
		dragStartPosition1 = 0,
		dragStartPosition2 = 0,
		dragLastX          = 0,
		dragLastY          = 0,

		editingEnabled = true,

		scrollX = 0.0,
		scrollY = 0.0,

		text = "",

		width  = 1/0,
		height = 1/0,

		alignment = "left",

		navigationTargetX = nil, -- Used when navigating vertically. Nil means need recalculation.

		-- These are updated by updateWrap():
		lastWrappedText = "",
		wrappedText     = {""}, -- []line
		softBreak       = {false}, -- []bool
		wrappedWidth    = 0,
	}, InputField)

	text       = cleanString(field, tostring(text == nil and "" or text))
	field.text = text
	local len  = utf8.len(text)

	field.editHistory[1] = {
		text           = text,
		cursorPosition = len,
		selectionStart = 0,
		selectionEnd   = len,
	}

	return field
end



-- phase = field:getBlinkPhase( )
-- Get current phase for an animated cursor.
-- The value is the time since the last blink reset.
function InputField.getBlinkPhase(field)
	return LT.getTime() - field.blinkTimer
end

-- field:resetBlinking( )
function InputField.resetBlinking(field)
	field.blinkTimer = LT.getTime()
end



-- position = field:getCursor( )
-- Position 0 is before first character, position 1 is between first and second etc.
function InputField.getCursor(field)
	return field.cursorPosition
end

-- field:setCursor( position [, selectionSideToAnchor:SelectionSide=none ] )
-- Position 0 is before first character, position 1 is between first and second etc.
function InputField.setCursor(field, pos, selSideAnchor)
	finilizeHistoryGroup(field)

	pos                  = clamp(pos, 0, field:getTextLength())
	field.cursorPosition = pos

	local selStart       = (selSideAnchor == "start" and field.selectionStart or pos)
	local selEnd         = (selSideAnchor == "end"   and field.selectionEnd   or pos)
	field.selectionStart = math.min(selStart, selEnd)
	field.selectionEnd   = math.max(selStart, selEnd)

	field:resetBlinking()
	field:scrollToCursor()
end

local function setCursorPosition(field, pos, selSideAnchor, eraseNavTargetX)
	field:setCursor(pos, selSideAnchor)
	if eraseNavTargetX then
		field.navigationTargetX = nil
	end
end

-- field:moveCursor( steps [, selectionSideToAnchor:SelectionSide=none ] )
-- 'steps' may be positive or negative.
function InputField.moveCursor(field, steps, selSideAnchor)
	setCursorPosition(field, field.cursorPosition+steps, selSideAnchor, true)
end

-- side:SelectionSide = field:getCursorSelectionSide( )
function InputField.getCursorSelectionSide(field)
	return (field.cursorPosition < field.selectionEnd and "start" or "end")
end

-- side:SelectionSide = field:getAnchorSelectionSide( )
function InputField.getAnchorSelectionSide(field)
	return (field.cursorPosition < field.selectionEnd and "end" or "start")
end



-- font = field:getFont( )
function InputField.getFont(field)
	return field.font
end

-- field:setFont( font )
-- The font is used for text measurements.
function InputField.setFont(field, font)
	if field.font == font then  return  end

	if not font then  error("Missing font argument.", 2)  end

	field.font            = font
	field.lastWrappedText = "\0" -- Make sure wrappedText updates.

	limitScroll(field)
end



-- scrollX, scrollY = field:getScroll( )
-- scrollX = field:getScrollX( )
-- scrollY = field:getScrollY( )
function InputField.getScroll(field)
	return field.scrollX, field.scrollY
end
function InputField.getScrollX(field)
	return field.scrollX
end
function InputField.getScrollY(field)
	return field.scrollY
end

-- field:setScroll( scrollX, scrollY )
-- field:setScrollX( scrollX )
-- field:setScrollY( scrollY )
function InputField.setScroll(field, scrollX, scrollY)
	field.scrollX = scrollX
	field.scrollY = scrollY
	limitScroll(field)
end
function InputField.setScrollX(field, scrollX)
	field.scrollX = scrollX
	limitScroll(field)
end
function InputField.setScrollY(field, scrollY)
	field.scrollY = scrollY
	limitScroll(field)
end

-- scrolledX, scrolledY = field:scroll( deltaX, deltaY )
-- Returned values are how much was actually scrolled.
function InputField.scroll(field, dx, dy)
	local oldScrollX = field.scrollX
	local oldScrollY = field.scrollY
	field.scrollX    = oldScrollX + dx
	field.scrollY    = oldScrollY + dy

	limitScroll(field)

	return field.scrollX - oldScrollX,
	       field.scrollY - oldScrollY
end

-- field:scrollToCursor( )
function InputField.scrollToCursor(field)
	local line, posOnLine, lineI = getLineInfoAtPosition(field, field.cursorPosition)

	do
		local fontH    = field.font:getHeight()
		local lineDist = math.ceil(fontH*field.font:getLineHeight())
		local y        = (lineI - 1) * lineDist
		field.scrollY  = clamp(field.scrollY, y-field.height+fontH, y)
	end

	if not field:isMultiline() then
		local visibleText = field:getVisibleText()
		local preText     = visibleText:sub(1, utf8GetEndOffset(visibleText, field.cursorPosition))
		local x           = field.font:getWidth(preText)
		field.scrollX     = clamp(field.scrollX, x-field.width, x)

	elseif field.type == "multinowrap" then
		local preText = line:sub(1, utf8GetEndOffset(line, posOnLine))
		local x       = field.font:getWidth(preText)
		field.scrollX = clamp(field.scrollX, x-field.width, x)
	end

	limitScroll(field)
end



-- limitX, limitY = field:getScrollLimits( )
function InputField.getScrollLimits(field)
	updateWrap(field)

	local fontH    = field.font:getHeight()
	local lineDist = math.ceil(fontH*field.font:getLineHeight())

	return (field.type == "multiwrap") and 0 or math.max((field.wrappedWidth                   ) - field.width,  0),
	       (not field:isMultiline()  ) and 0 or math.max(((#field.wrappedText-1)*lineDist+fontH) - field.height, 0)
end



-- speedX, speedY = field:getMouseScrollSpeed( )
-- speedX = field:getMouseScrollSpeedX( )
-- speedY = field:getMouseScrollSpeedY( )
function InputField.getMouseScrollSpeed(field)
	return field.mouseScrollSpeedX, field.mouseScrollSpeedY
end
function InputField.getMouseScrollSpeedX(field)
	return field.mouseScrollSpeedX
end
function InputField.getMouseScrollSpeedY(field)
	return field.mouseScrollSpeedY
end

--
-- field:setMouseScrollSpeed( speedX [, speedY=speedX ] )
-- field:setMouseScrollSpeedX( speedX )
-- field:setMouseScrollSpeedY( speedY )
--
-- Also see setWheelScrollSpeed().
--
function InputField.setMouseScrollSpeed(field, speedX, speedY)
	speedY                  = speedY or speedX
	field.mouseScrollSpeedX = math.max(speedX, 0)
	field.mouseScrollSpeedY = math.max(speedY, 0)
end
function InputField.setMouseScrollSpeedX(field, speedX)
	field.mouseScrollSpeedX = math.max(speedX, 0)
end
function InputField.setMouseScrollSpeedY(field, speedY)
	field.mouseScrollSpeedY = math.max(speedY, 0)
end



-- speedX, speedY = field:getWheelScrollSpeed( )
-- speedX = field:getWheelScrollSpeedX( )
-- speedY = field:getWheelScrollSpeedY( )
function InputField.getWheelScrollSpeed(field)
	return field.wheelScrollSpeedX, field.wheelScrollSpeedY
end
function InputField.getWheelScrollSpeedX(field)
	return field.wheelScrollSpeedX
end
function InputField.getWheelScrollSpeedY(field)
	return field.wheelScrollSpeedY
end

-- field:setWheelScrollSpeed( speedX [, speedY=speedX ] )
-- field:setWheelScrollSpeedX( speedX )
-- field:setWheelScrollSpeedY( speedY )
function InputField.setWheelScrollSpeed(field, speedX, speedY)
	speedY                  = speedY or speedX
	field.wheelScrollSpeedX = math.max(speedX, 0)
	field.wheelScrollSpeedY = math.max(speedY, 0)
end
function InputField.setWheelScrollSpeedX(field, speedX)
	field.wheelScrollSpeedX = math.max(speedX, 0)
end
function InputField.setWheelScrollSpeedY(field, speedY)
	field.wheelScrollSpeedY = math.max(speedY, 0)
end



-- delay = field:getDoubleClickMaxDelay( )
function InputField.getDoubleClickMaxDelay(field)
	return field.multiClickMaxDelay
end

-- field:setDoubleClickMaxDelay( delay )
-- Note: This value is only used if the 'pressCount' argument isn't supplied to mousepressed().
function InputField.setDoubleClickMaxDelay(field, delay)
	field.multiClickMaxDelay = math.max(delay, 0)
end



-- fromPosition, toPosition = field:getSelection( )
-- Position 0 is before first character, position 1 is between first and second etc.
function InputField.getSelection(field)
	return field.selectionStart, field.selectionEnd
end

-- field:setSelection( fromPosition, toPosition [, cursorAlign:TextCursorAlignment="right" ] )
-- Position 0 is before first character, position 1 is between first and second etc.
function InputField.setSelection(field, from, to, cursorAlign)
	finilizeHistoryGroup(field)

	local len = field:getTextLength()
	from = clamp(from, 0, len)
	to   = clamp(to,   0, len)

	from, to = math.min(from, to), math.max(from, to)

	field.selectionStart = from
	field.selectionEnd   = to
	field.cursorPosition = (cursorAlign == "left") and from or to

	field:resetBlinking()
	field:scrollToCursor()
end

-- field:selectAll( )
function InputField.selectAll(field)
	field:setSelection(0, field:getTextLength())
end

-- text = field:getSelectedText( )
function InputField.getSelectedText(field)
	local text = field.text
	return text:sub(
		utf8.offset(text, field.selectionStart+1),
		utf8GetEndOffset(text, field.selectionEnd)
	)
end

-- text = field:getSelectedVisibleText( )
function InputField.getSelectedVisibleText(field)
	return (field.type == "password") and ("*"):rep(field.selectionEnd-field.selectionStart) or field:getSelectedText()
end



-- text = field:getText( )
-- The text is the value of the field.
-- Also see field:getVisibleText().
function InputField.getText(field)
	return field.text
end

-- text = field:getVisibleText( )
-- Returns asterisks for password fields instead of the actual text value.
-- Also see field:getText().
function InputField.getVisibleText(field)
	return (field.type == "password") and ("*"):rep(field:getTextLength()) or field.text -- @Speed: Maybe cache the repeated text.
end

-- field:setText( text )
function InputField.setText(field, text)
	text = cleanString(field, tostring(text))
	if field.text == text then  return  end

	local len = utf8.len(text)

	field.text           = text
	field.cursorPosition = math.min(len, field.cursorPosition)
	field.selectionStart = math.min(len, field.selectionStart)
	field.selectionEnd   = math.min(len, field.selectionEnd)

	pushHistory(field, nil)
end



-- length = field:getTextLength( )
-- Returns the number of characters in the UTF-8 text string.
function InputField.getTextLength(field)
	return utf8.len(field.text)
end



-- offsetX, offsetY = field:getTextOffset( )
-- Note: The coordinates are relative to the field's position.
function InputField.getTextOffset(field)
	return -math.floor(field.scrollX),
	       -math.floor(field.scrollY)
end



-- x, y, height = field:getCursorLayout( )
-- Note: The coordinates are relative to the field's position.
function InputField.getCursorLayout(field)
	local line, posOnLine, lineI = getLineInfoAtPosition(field, field.cursorPosition)

	local preText  = line:sub(1, utf8GetEndOffset(line, posOnLine))
	local fontH    = field.font:getHeight()
	local lineDist = math.ceil(fontH*field.font:getLineHeight())

	return alignOnLine(field, line, field.font:getWidth(preText)) - math.floor(field.scrollX),
	       (lineI-1)*lineDist                                     - math.floor(field.scrollY),
	       fontH
end



-- width, height = field:getDimensions( )
-- width  = field:getWidth( )
-- height = field:getHeight( )
function InputField.getDimensions(field)
	return field.width, field.height
end
function InputField.getWidth(field)
	return field.width
end
function InputField.getHeight(field)
	return field.height
end

-- field:setDimensions( width, height )
-- field:setDimensions( nil, nil ) -- Disable scrolling on both axes.
-- Enable/disable horizontal/vertical scrolling.
function InputField.setDimensions(field, w, h)
	w = math.max((w or 1/0), 0)
	h = math.max((h or 1/0), 0)
	if field.width == w and field.height == h then  return  end

	field.width           = w
	field.height          = h
	field.lastWrappedText = "\0" -- Make sure wrappedText updates.

	limitScroll(field)
end

-- field:setWidth( width )
-- field:setWidth( nil ) -- Disable scrolling on the x axis.
-- Enable/disable horizontal scrolling.
function InputField.setWidth(field, w)
	w = math.max((w or 1/0), 0)
	if field.width == w then  return  end

	field.width           = w
	field.lastWrappedText = "\0" -- Make sure wrappedText updates.

	limitScroll(field)
end

-- field:setHeight( height )
-- field:setHeight( nil ) -- Disable scrolling on the y axis.
-- Enable/disable vertical scrolling.
function InputField.setHeight(field, h)
	h = math.max((h or 1/0), 0)
	if field.height == h then  return  end

	field.height = h -- Note: wrappedText does not need to update because of this change.

	limitScroll(field)
end




-- width, height = field:getTextDimensions( )
function InputField.getTextDimensions(field)
	updateWrap(field)

	local fontH    = field.font:getHeight()
	local lineDist = math.ceil(fontH*field.font:getLineHeight())

	return field.wrappedWidth, (#field.wrappedText-1)*lineDist+fontH
end

-- width = field:getTextWidth( )
function InputField.getTextWidth(field)
	updateWrap(field)
	return field.wrappedWidth
end

-- height = field:getTextHeight( )
function InputField.getTextHeight(field)
	updateWrap(field)

	local fontH    = field.font:getHeight()
	local lineDist = math.ceil(fontH*field.font:getLineHeight())

	return (#field.wrappedText - 1) * lineDist + fontH
end



-- field:insert( text )
-- Insert text at cursor, or replace selected text.
function InputField.insert(field, newText)
	newText = cleanString(field, tostring(newText))

	local text     = field.text
	local selStart = field.selectionStart
	local iLeft    = utf8GetEndOffset(text, selStart)
	local iRight   = utf8.offset(text, field.selectionEnd+1)

	if newText == "" then
		field.text           = text:sub(1, iLeft) .. text:sub(iRight)
		field.cursorPosition = selStart
		field.selectionEnd   = selStart

		pushHistory(field, "remove")

	else
		field.text           = text:sub(1, iLeft) .. newText .. text:sub(iRight)
		field.cursorPosition = selStart + utf8.len(newText)
		field.selectionStart = field.cursorPosition
		field.selectionEnd   = field.cursorPosition

		pushHistory(field, "insert")
	end

	field:resetBlinking()
	field:scrollToCursor()
end



-- field:reset( [ initialText="" ] )
function InputField.reset(field, text)
	field:releaseMouse()

	field.cursorPosition = 0
	field.selectionStart = 0
	field.selectionEnd   = 0

	field.clickCount               = 1
	field.multiClickExpirationTime = 0

	field.navigationTargetX = nil

	field:setText(text == nil and "" or text)
	field:clearHistory()
end



--
-- bool = field:isFontFilteringActive( )
-- field:setFontFilteringActive( bool )
--
-- Filter out characters that the font doesn't have glyphs for.
--
function InputField.isFontFilteringActive(field)          return field.fontFilteringIsActive   end
function InputField.setFontFilteringActive(field, state)  field.fontFilteringIsActive = state  end

-- bool = field:isEditable( )
-- field:setEditable( bool )
-- Enable/disable read-only mode.
function InputField.isEditable(field)          return field.editingEnabled   end
function InputField.setEditable(field, state)  field.editingEnabled = state  end



-- type:InputFieldType = field:getType( )
function InputField.getType(field)
	return field.type
end

-- field:setType( type:InputFieldType )
function InputField.setType(field, fieldType)
	if field.type == fieldType then  return  end

	if not (fieldType == "normal" or fieldType == "password" or fieldType == "multiwrap" or fieldType == "multinowrap") then
		error("[InputField] Invalid field type '"..tostring(fieldType).."'.", 2)
	end

	local wasMultiline = field:isMultiline()

	field.type            = fieldType
	field.lastWrappedText = "\0" -- Make sure wrappedText updates.

	if wasMultiline and not field:isMultiline() then
		field:reset(cleanString(field, field.text)) -- Removes newlines (including from history).
	end
end

-- bool = field:isPassword( )
-- bool = field:isMultiline( )
function InputField.isPassword(field)   return field.type == "password"  end
function InputField.isMultiline(field)  return field.type == "multiwrap" or field.type == "multinowrap"  end



-- filterFunction = field:getFilter( )
function InputField.getFilter(field)
	return field.filterFunction
end

--
-- setFilter( filterFunction )
-- setFilter( nil ) -- Remove filter.
-- removeCharacter:bool = filterFunction( utf8Character )
--
-- Filter out entered characters.
--
-- Note: The filter is only applied in text input events, like textinput().
-- Functions like setText() are unaffected (unlike with font filtering).
--
function InputField.setFilter(field, filterFunc)
	field.filterFunction = filterFunc
end



-- field:clearHistory( )
function InputField.clearHistory(field)
	local history = field.editHistory

	history[1] = history[#history]
	for i = 2, #history do  history[i] = nil  end

	field.editHistoryGroup = nil
	field.editHistoryIndex = 1
end



-- maxHistory = field:getMaxHistory( )
function InputField.getMaxHistory(field)
	return field.editHistoryMax
end

-- field:setMaxHistory( maxHistory )
-- Limit entry count for the undo/redo history.
function InputField.setMaxHistory(field, maxHistory)
	maxHistory = math.max(maxHistory, 1)
	if maxHistory == field.editHistoryMax then  return  end

	field:clearHistory()
	field.editHistoryMax = maxHistory
end



-- horizontally, vertically = field:canScroll( )
-- Note: Scrolling is enabled/disabled by setDimensions() and company.
function InputField.canScroll(field)
	return field:canScrollHorizontally(), field:canScrollVertically()
end

-- horizontally = field:canScrollHorizontally( )
-- Note: Scrolling is enabled/disabled by setDimensions() or setWidth().
function InputField.canScrollHorizontally(field)
	return field.type ~= "multiwrap"
end

-- vertically = field:canScrollVertically( )
-- Note: Scrolling is enabled/disabled by setDimensions() or setHeight().
function InputField.canScrollVertically(field)
	return field.type == "multiwrap" or field.type == "multinowrap"
end



-- horizontalOffset, horizontalCoverage, verticalOffset, verticalCoverage = field:getScrollHandles( )
-- Values are between 0 and 1.
function InputField.getScrollHandles(field)
	local textW,      textH      = field:getTextDimensions()
	local scrollX,    scrollY    = field:getScroll()
	local maxScrollX, maxScrollY = field:getScrollLimits()

	local hCoverage = math.min(field.width  / textW, 1)
	local vCoverage = math.min(field.height / textH, 1)
	local hOffset   = (maxScrollX == 0) and 0 or (scrollX / maxScrollX) * (1 - hCoverage)
	local vOffset   = (maxScrollY == 0) and 0 or (scrollY / maxScrollY) * (1 - vCoverage)

	return hOffset, hCoverage, vOffset, vCoverage
end

-- offset, coverage = field:getScrollHandleHorizontal( )
-- The values (and their sum) are between 0 and 1.
function InputField.getScrollHandleHorizontal(field)
	local hOffset, hCoverage, vOffset, vCoverage = field:getScrollHandles()
	return hOffset, hCoverage
end

-- offset, coverage = field:getScrollHandleVertical( )
-- The values (and their sum) are between 0 and 1.
function InputField.getScrollHandleVertical(field)
	local hOffset, hCoverage, vOffset, vCoverage = field:getScrollHandles()
	return vOffset, vCoverage
end



-- alignment:TextAlignment = field:getAlignment( )
function InputField.getAlignment(field)
	return field.alignment
end

-- field:setAlignment( alignment:TextAlignment )
-- Note: The field's width must be set for alignment to work. (See field:setDimensions() or field:setWidth().)
-- Note: Alignment doesn't work for fields of type "multinowrap".
function InputField.setAlignment(field, alignment)
	field.alignment = alignment
end




-- line = field:getVisibleLine( lineIndex )
-- Returns nil if lineIndex is invalid.
function InputField.getVisibleLine(field, lineI)
	updateWrap(field)
	return field.wrappedText[lineI]
end

-- count = field:getVisibleLineCount( )
function InputField.getVisibleLineCount(field)
	updateWrap(field)
	return #field.wrappedText
end



--
-- info = field:getInfoAtCoords( x, y [, info={} ] )
--
-- Info table fields:
--   cursorPosition    -- integer  Cursor position.
--   characterPosition -- integer  Character position. (Is set even if hasText is false.)
--   hasText           -- boolean  Whether there's text directly at the coordinates.
--   lineIndex         -- integer  Visible line index.
--   linePosition      -- integer  Visible line start position.
--
-- Note: The coordinates must be relative to the field's position on the screen.
--
function InputField.getInfoAtCoords(field, x, y, info)
	updateWrap(field)

	info = info or {}

	if field.text == "" then
		info.cursorPosition    = 0
		info.characterPosition = 1
		info.hasText           = false
		info.lineIndex         = 1
		info.linePosition      = 1

	else
		local curPos, visualLineI, visualLineIUnclamped = getCursorPositionAtCoordinates(field, x+field.scrollX, y+field.scrollY)

		local line, curPosOnLine, lineI, linePos1, linePos2 = getLineInfoAtPosition(field, curPos)

		local preText              = line:sub(1, utf8GetEndOffset(line, curPosOnLine)) -- @Speed @Memory
		local preTextW             = field.font:getWidth(preText)
		local charStartIsLeftOfCur = (alignOnLine(field, line, preTextW-field.scrollX) <= x)
		local charPosOnLine        = clamp(curPosOnLine+(charStartIsLeftOfCur and 1 or 0), 1, linePos2-linePos1+1)

		if lineI > visualLineI then
			-- We're right before a soft wrap.
			line, posOnLine, lineI, linePos1, linePos2 = getLineInfoAtPosition(field, curPos-1)
			charPosOnLine                              = linePos2 - linePos1 + 1
		end

		local xInText = unalignOnLine(field, line, x+field.scrollX)

		info.cursorPosition    = curPos
		info.characterPosition = math.min(linePos1+charPosOnLine-1, utf8.len(field.text))
		info.hasText           = field.wrappedText[visualLineIUnclamped] ~= nil and xInText >= 0 and xInText < field.font:getWidth(line)
		info.lineIndex         = lineI
		info.linePosition      = linePos1
	end

	return info
end

--
-- info = field:getInfoAtCursor( cursorPosition [, info={} ] )
--
-- Info table fields:
--   x            -- integer  X position.
--   y            -- integer  Y/top position.
--   height       -- integer  Line height.
--   lineIndex    -- integer  Visible line index.
--   linePosition -- integer  Visible line start position.
--
-- Note: The coordinates are relative to the field's position.
--
function InputField.getInfoAtCursor(field, pos, info)
	updateWrap(field)

	info = info or {}

	if field.text == "" then
		info.x            = alignOnLine(field, "", 0)
		info.y            = 0
		info.height       = field.font:getHeight()
		info.lineIndex    = 1
		info.linePosition = 1

	else
		pos = clamp(pos, 0, utf8.len(field.text))

		local line, curPosOnLine, lineI, linePos1, linePos2 = getLineInfoAtPosition(field, pos)

		local savedCursorPosition = field.cursorPosition
		field.cursorPosition      = pos
		local x, y, h             = field:getCursorLayout()
		field.cursorPosition      = savedCursorPosition

		info.x            = x
		info.y            = y
		info.height       = h
		info.lineIndex    = lineI
		info.linePosition = linePos1
	end

	return info
end

--
-- info = field:getInfoAtCharacter( characterPosition [, info={} ] )
--
-- Info table fields:
--   character    -- string   The character (unobfuscated if field type is "password").
--   x            -- integer  Character x/left position.
--   y            -- integer  Character y/top position.
--   width        -- integer  Character width.
--   height       -- integer  Character/line height.
--   lineIndex    -- integer  Visible line index.
--   linePosition -- integer  Visible line start position.
--
-- Returns nil if characterPosition is invalid or points at a newline.
-- Note: The coordinates are relative to the field's position.
--
function InputField.getInfoAtCharacter(field, pos, info)
	updateWrap(field)

	local text = field.text

	if
		text == "" or pos < 1 or pos > utf8.len(text) -- Error: Out-of-range!
		or utf8.codepoint(text, utf8.offset(text, pos)) == 10 -- Don't return info about newlines.
	then
		return nil
	end

	local line, curPosOnLine, lineI, linePos1, linePos2 = getLineInfoAtPosition(field, pos-1)

	local i1          = utf8.offset(line, curPosOnLine+1)
	local i2          = utf8GetEndOffset(line, curPosOnLine+1)
	local visibleChar = line:sub(i1, i2)

	local preText = line:sub(1, i1-1) -- @Speed @Memory
	local x       = alignOnLine(field, line, field.font:getWidth(preText))

	local fontH    = field.font:getHeight()
	local lineDist = math.ceil(fontH*field.font:getLineHeight())
	local y        = (lineI-1) * lineDist

	info              = info or {}
	info.character    = (field.type == "password") and text:sub(utf8.offset(text, pos), utf8GetEndOffset(text, pos)) or visibleChar
	info.x            = x
	info.y            = y
	info.width        = field.font:getWidth(visibleChar)
	info.height       = fontH
	info.lineIndex    = lineI
	info.linePosition = linePos1

	return info
end



----------------------------------------------------------------



-- field:update( deltaTime )
function InputField.update(field, dt)
	if field.dragMode ~= "" then
		local mx         = field.dragLastX
		local my         = field.dragLastY
		local oldScrollX = field.scrollX
		local oldScrollY = field.scrollY
		local scrollX    = oldScrollX
		local scrollY    = oldScrollY
		local w          = field.width
		local h          = field.height

		field.scrollX = (mx < 0 and scrollX+field.mouseScrollSpeedX*mx*dt) or (mx > w and scrollX+field.mouseScrollSpeedX*(mx-w)*dt) or (scrollX)
		field.scrollY = (my < 0 and scrollY+field.mouseScrollSpeedY*my*dt) or (my > h and scrollY+field.mouseScrollSpeedY*(my-h)*dt) or (scrollY)
		limitScroll(field)

		if not (field.scrollX == oldScrollX and field.scrollY == oldScrollY) then
			field:mousemoved(mx, my) -- This should only update selection stuff.
		end
	end
end



-- eventWasHandled = field:mousepressed( mouseX, mouseY, mouseButton [, pressCount=auto ] )
-- Note: The coordinates must be relative to the field's position on the screen.
function InputField.mousepressed(field, mx, my, mbutton, pressCount)
	if mbutton ~= 1 then  return false  end

	-- Check if double click.
	if mbutton == 1 then
		local time = LT.getTime()

		if pressCount then
			-- void
		elseif
			time < field.multiClickExpirationTime
			and math.abs(field.multiClickLastX-mx) <= 1 -- @Incomplete: Make max distance into a setting? Also, shoud we use max distance even if pressCount is supplied?
			and math.abs(field.multiClickLastY-my) <= 1
		then
			pressCount = field.clickCount + 1
		else
			pressCount = 1
		end

		field.clickCount               = (pressCount == 1) and 1 or 2+(pressCount%2)
		field.multiClickExpirationTime = time + field.multiClickMaxDelay
		field.multiClickLastX          = mx
		field.multiClickLastY          = my

	else
		field.clickCount               = 1
		field.multiClickExpirationTime = 0
	end

	-- Handle mouse press.
	local pos = getCursorPositionAtCoordinates(field, mx+field.scrollX, my+field.scrollY)

	if field.clickCount == 3 then
		local pos1 = getNextHardLineBound(field.text, pos+1, -1)
		local pos2 = getNextHardLineBound(field.text, pos1,   1)

		field:setSelection(pos1, pos2)

		field.dragMode           = "line"
		field.dragStartPosition1 = pos1
		field.dragStartPosition2 = pos2
		field.dragLastX          = mx
		field.dragLastY          = my

	elseif field.clickCount == 2 then
		local visibleText = field:getVisibleText()
		local pos1        = getNextWordBound(visibleText, pos+1, -1)
		local pos2        = getNextWordBound(visibleText, pos1,   1)

		field:setSelection(pos1, pos2)

		field.dragMode           = "word"
		field.dragStartPosition1 = pos1
		field.dragStartPosition2 = pos2
		field.dragLastX          = mx
		field.dragLastY          = my

	elseif LK.isDown("lshift","rshift") then
		local anchorPos = (field:getAnchorSelectionSide() == "start" and field.selectionStart or field.selectionEnd)

		field:setSelection(pos, anchorPos, (pos < anchorPos and "left" or "right"))

		field.dragMode           = "character"
		field.dragStartPosition1 = anchorPos
		field.dragLastX          = mx
		field.dragLastY          = my

	else
		setCursorPosition(field, pos, nil, true)

		field.dragMode           = "character"
		field.dragStartPosition1 = pos
		field.dragLastX          = mx
		field.dragLastY          = my
	end

	return true
end

-- eventWasHandled = field:mousemoved( mouseX, mouseY )
-- Note: The coordinates must be relative to the field's position on the screen.
function InputField.mousemoved(field, mx, my)
	if field.dragMode == "" then  return false  end

	local scrollX = field.scrollX
	local scrollY = field.scrollY
	local pos     = getCursorPositionAtCoordinates(field, mx+field.scrollX, my+field.scrollY)

	if field.dragMode == "line" then
		-- Note: Visibility doesn't matter when selecting lines.
		local pos1 = getNextHardLineBound(field.text, pos+1, -1)
		local pos2 = getNextHardLineBound(field.text, pos1,   1)

		if pos1 < field.dragStartPosition1 then
			field:setSelection(pos1, field.dragStartPosition2, "left")
		else
			field:setSelection(field.dragStartPosition1, pos2, "right")
		end

	elseif field.dragMode == "word" then
		local visibleText = field:getVisibleText()
		local pos1        = getNextWordBound(visibleText, pos+1, -1)
		local pos2        = getNextWordBound(visibleText, pos1,   1)

		if pos1 < field.dragStartPosition1 then
			field:setSelection(pos1, field.dragStartPosition2, "left")
		else
			field:setSelection(field.dragStartPosition1, pos2, "right")
		end

	else
		field:setSelection(field.dragStartPosition1, pos, (pos < field.dragStartPosition1 and "left" or "right"))
	end

	field.scrollX = scrollX -- Prevent scrolling to cursor while dragging.
	field.scrollY = scrollY

	field.dragLastX = mx
	field.dragLastY = my
	return true
end

-- eventWasHandled = field:mousereleased( mouseX, mouseY, mouseButton )
-- Note: The coordinates must be relative to the field's position on the screen.
function InputField.mousereleased(field, mx, my, mbutton)
	if mbutton ~= 1         then  return false  end
	if field.dragMode == "" then  return false  end

	field.dragMode = ""

	field:scrollToCursor()
	return true
end

-- eventWasHandled = field:wheelmoved( deltaX, deltaY )
function InputField.wheelmoved(field, dx, dy)
	if LK.isDown("lshift","rshift") then
		dx, dy = dy, dx
	end

	if not ((dx ~= 0 and field:canScrollHorizontally()) or (dy ~= 0 and field:canScrollVertically())) then
		return false
	end

	local fontH = field.font:getHeight()
	local scrolledX, scrolledY = field:scroll(
		-dx * field.wheelScrollSpeedX*fontH,
		-dy * field.wheelScrollSpeedY*fontH
	)

	return scrolledX ~= 0 or scrolledY ~= 0
end



-- field:releaseMouse( )
-- Release mouse buttons that are held down (i.e. stop drag-selecting).
function InputField.releaseMouse(field)
	field.dragMode = ""
end



-- bool = field:isBusy( )
-- Returns true if the user is currently drag-selecting.
function InputField.isBusy(field)
	return field.dragMode ~= ""
end



local function action_moveCursorCharacterLeft(field, isRepeat)
	if field.selectionStart ~= field.selectionEnd then
		setCursorPosition(field, field.selectionStart, nil, true)
	else
		field:moveCursor(-1)
	end
	return true, false
end
local function action_moveCursorCharacterLeftAnchored(field, isRepeat)
	field:moveCursor(-1, field:getAnchorSelectionSide())
	return true, false
end
local function action_moveCursorWordLeft(field, isRepeat)
	setCursorPosition(field, getNextWordBound(field:getVisibleText(), field.cursorPosition, -1), nil, true)
	return true, false
end
local function action_moveCursorWordLeftAnchored(field, isRepeat)
	setCursorPosition(field, getNextWordBound(field:getVisibleText(), field.cursorPosition, -1), field:getAnchorSelectionSide(), true)
	return true, false
end

local function action_moveCursorCharacterRight(field, isRepeat)
	if field.selectionStart ~= field.selectionEnd then
		setCursorPosition(field, field.selectionEnd, nil, true)
	else
		field:moveCursor(1)
	end
	return true, false
end
local function action_moveCursorCharacterRightAnchored(field, isRepeat)
	field:moveCursor(1, field:getAnchorSelectionSide())
	return true, false
end
local function action_moveCursorWordRight(field, isRepeat)
	setCursorPosition(field, getNextWordBound(field:getVisibleText(), field.cursorPosition, 1), nil, true)
	return true, false
end
local function action_moveCursorWordRightAnchored(field, isRepeat)
	setCursorPosition(field, getNextWordBound(field:getVisibleText(), field.cursorPosition, 1), field:getAnchorSelectionSide(), true)
	return true, false
end

local function action_moveCursorLineStart(field, isRepeat)
	local line, posOnLine, lineI, linePos1, linePos2 = getLineInfoAtPosition(field, field.cursorPosition)
	if field.cursorPosition == linePos1-1 and field.softBreak[lineI-1] then
		line, posOnLine, lineI, linePos1, linePos2 = getLineInfoAtPosition(field, field.cursorPosition-1)
	end
	setCursorPosition(field, linePos1-1, nil, true)
	return true, false
end
local function action_moveCursorLineStartAnchored(field, isRepeat)
	local line, posOnLine, lineI, linePos1, linePos2 = getLineInfoAtPosition(field, field.cursorPosition)
	if field.cursorPosition == linePos1-1 and field.softBreak[lineI-1] then
		line, posOnLine, lineI, linePos1, linePos2 = getLineInfoAtPosition(field, field.cursorPosition-1)
	end
	setCursorPosition(field, linePos1-1, field:getAnchorSelectionSide(), true)
	return true, false
end
local function action_moveCursorDocumentStart(field, isRepeat)
	setCursorPosition(field, 0, nil, true)
	return true, false
end
local function action_moveCursorDocumentStartAnchored(field, isRepeat)
	setCursorPosition(field, 0, field:getAnchorSelectionSide(), true)
	return true, false
end

local function action_moveCursorLineEnd(field, isRepeat)
	local line, posOnLine, lineI, linePos1, linePos2 = getLineInfoAtPosition(field, field.cursorPosition)
	setCursorPosition(field, linePos2, nil, true)
	return true, false
end
local function action_moveCursorLineEndAnchored(field, isRepeat)
	local line, posOnLine, lineI, linePos1, linePos2 = getLineInfoAtPosition(field, field.cursorPosition)
	setCursorPosition(field, linePos2, field:getAnchorSelectionSide(), true)
	return true, false
end
local function action_moveCursorDocumentEnd(field, isRepeat)
	setCursorPosition(field, field:getTextLength(), nil, true)
	return true, false
end
local function action_moveCursorDocumentEndAnchored(field, isRepeat)
	setCursorPosition(field, field:getTextLength(), field:getAnchorSelectionSide(), true)
	return true, false
end

local function navigateByLine(field, dirY, doAnchor)
	if not field:isMultiline() then  return false  end

	local anchorSide = (doAnchor and field:getAnchorSelectionSide() or nil)

	-- Get info about the current line.
	local oldLine, oldPosOnLine, oldLineI, oldLinePos1, oldLinePos2 = getLineInfoAtPosition(field, field.cursorPosition)

	if dirY < 0 and oldLineI == 1 then
		-- setCursorPosition(field, 0, anchorSide, false) -- Eh, not sure it's good to navigate horizontally during vertical navigation.
		return true
	elseif dirY > 0 and oldLineI >= #field.wrappedText then
		-- setCursorPosition(field, utf8.len(field.text), anchorSide, false)
		return true
	end

	if not field.navigationTargetX then
		local linePart          = oldLine:sub(1, utf8GetEndOffset(oldLine, oldPosOnLine))
		field.navigationTargetX = alignOnLine(field, oldLine, field.font:getWidth(linePart))
	end

	-- Get info about the target line.
	local newLine, newPosOnLine, newLineI, newLinePos1, newLinePos2
	if dirY < 0 then
		newLine, newPosOnLine, newLineI, newLinePos1, newLinePos2 = getLineInfoAtPosition(field, oldLinePos1-2)
	else
		newLine, newPosOnLine, newLineI, newLinePos1, newLinePos2 = getLineInfoAtPosition(field, oldLinePos2+1)
	end

	local posOnLine = getCursorPositionAtX(field, newLine, field.navigationTargetX)

	-- Going from the end of a long line to the end of a short soft-wrapped line would put
	-- the cursor at the start of the previous long line or two lines down. No good!
	if field.softBreak[newLineI] and posOnLine == utf8.len(newLine) then
		posOnLine = posOnLine - 1
	end

	setCursorPosition(field, newLinePos1+posOnLine-1, anchorSide, false)
	return true
end

local function navigateByPage(field, dirY, doAnchor)
	local fontH      = field.font:getHeight()
	local lineDist   = math.ceil(fontH*field.font:getLineHeight())
	local walkCount  = math.max(math.floor(field.height/lineDist), 1)
	local anyHandled = false

	for i = 1, walkCount do
		local handled, _targetX = navigateByLine(field, dirY, doAnchor) -- @Speed @Memory
		if not handled then  break  end
		anyHandled = true
	end

	return anyHandled
end

local function action_moveCursorLineUp          (field, isRepeat)  return navigateByLine(field, -1, false), false  end
local function action_moveCursorLineDown        (field, isRepeat)  return navigateByLine(field,  1, false), false  end
local function action_moveCursorLineUpAnchored  (field, isRepeat)  return navigateByLine(field, -1, true ), false  end
local function action_moveCursorLineDownAnchored(field, isRepeat)  return navigateByLine(field,  1, true ), false  end

local function action_moveCursorPageUp          (field, isRepeat)  return navigateByPage(field, -1, false), false  end
local function action_moveCursorPageDown        (field, isRepeat)  return navigateByPage(field,  1, false), false  end
local function action_moveCursorPageUpAnchored  (field, isRepeat)  return navigateByPage(field, -1, true ), false  end
local function action_moveCursorPageDownAnchored(field, isRepeat)  return navigateByPage(field,  1, true ), false  end

local function action_insertNewline(field, isRepeat)
	if not field.editingEnabled then  return false, false  end
	if not field:isMultiline()  then  return false, false  end

	field:insert("\n")
	return true, true
end

local function action_deleteCharacterLeft(field, isRepeat)
	if not field.editingEnabled then
		return false, false

	elseif field.selectionStart ~= field.selectionEnd then
		-- void

	elseif field.cursorPosition == 0 then
		field:resetBlinking()
		field:scrollToCursor()
		return true, false

	else
		field.selectionStart = field.cursorPosition - 1
		field.selectionEnd   = field.cursorPosition
	end

	field:insert("")
	return true, true
end
local function action_deleteWordLeft(field, isRepeat)
	if not field.editingEnabled then
		return false, false
	elseif field.selectionStart ~= field.selectionEnd then
		-- void
	else
		field.cursorPosition = getNextWordBound(field:getVisibleText(), field.cursorPosition, -1)
		field.selectionStart = field.cursorPosition
	end
	field:insert("")
	return true, true
end

local function action_deleteCharacterRight(field, isRepeat)
	if not field.editingEnabled then
		return false, false

	elseif field.selectionStart ~= field.selectionEnd then
		-- void

	elseif field.cursorPosition == field:getTextLength() then
		field:resetBlinking()
		field:scrollToCursor()
		return true, false

	else
		field.selectionStart = field.cursorPosition
		field.selectionEnd   = field.cursorPosition + 1
	end

	field:insert("")
	return true, true
end
local function action_deleteWordRight(field, isRepeat)
	if not field.editingEnabled then
		return false, false
	elseif field.selectionStart ~= field.selectionEnd then
		-- void
	else
		field.cursorPosition = getNextWordBound(field:getVisibleText(), field.cursorPosition, 1)
		field.selectionEnd   = field.cursorPosition
	end
	field:insert("")
	return true, true
end

local function action_selectAll(field, isRepeat)
	field:selectAll()
	return true, false
end

local function action_copy(field, isRepeat)
	local text = field:getSelectedVisibleText()

	if text ~= "" then
		LS.setClipboardText(text)
		field:resetBlinking()
	end

	return true, false
end

local function action_cut(field, isRepeat)
	local text = field:getSelectedVisibleText()
	if text == "" then  return true, false  end

	LS.setClipboardText(text)

	if field.editingEnabled then
		field:insert("")
		return true, true
	else
		field:resetBlinking()
		return true, false
	end
end

local function action_paste(field, isRepeat)
	if not field.editingEnabled then  return false, false  end

	local text = cleanString(field, LS.getClipboardText())
	if text ~= "" then
		field:insert(applyFilter(field, text))
	end

	field:resetBlinking()
	return true, true
end

local function action_undo(field, isRepeat)
	if not field.editingEnabled then  return false, false  end

	-- @Robustness: Filter and/or font filter could have changed after the last edit.
	return true, (field.type ~= "password" and undoEdit(field))
end
local function action_redo(field, isRepeat)
	if not field.editingEnabled then  return false, false  end

	-- @Robustness: Filter and/or font filter could have changed after the last edit.
	return true, (field.type ~= "password" and redoEdit(field))
end



local keyHandlers = {
	[ "cas"]={}, [ "ca"]={}, [ "cs"]={}, [ "as"]={}, [ "c"]={}, [ "a"]={}, [ "s"]={}, [ ""]={},
	["^cas"]={}, ["^ca"]={}, ["^cs"]={}, ["^as"]={}, ["^c"]={}, ["^a"]={}, ["^s"]={}, ["^"]={}, -- macOS only.
}

local function bind(system, modKeys, key, action)
	if system == "all" or (system == "macos") == isMac then
		keyHandlers[modKeys][key] = action
	end
end

-- (Ctrl means Cmd and Alt means Option in macOS.)

bind("all"    , ""   , "left"     , action_moveCursorCharacterLeft)
bind("all"    , "s"  , "left"     , action_moveCursorCharacterLeftAnchored)
bind("all"    , ""   , "right"    , action_moveCursorCharacterRight)
bind("all"    , "s"  , "right"    , action_moveCursorCharacterRightAnchored)
bind("macos"  , "^"  , "b"        , action_moveCursorCharacterLeft)
bind("macos"  , "^"  , "f"        , action_moveCursorCharacterRight)

bind("windows", "c"  , "left"     , action_moveCursorWordLeft)
bind("windows", "cs" , "left"     , action_moveCursorWordLeftAnchored)
bind("windows", "c"  , "right"    , action_moveCursorWordRight)
bind("windows", "cs" , "right"    , action_moveCursorWordRightAnchored)
bind("macos"  , "a"  , "left"     , action_moveCursorWordLeft)
bind("macos"  , "as" , "left"     , action_moveCursorWordLeftAnchored)
bind("macos"  , "a"  , "right"    , action_moveCursorWordRight)
bind("macos"  , "as" , "right"    , action_moveCursorWordRightAnchored)

bind("all"    , ""   , "home"     , action_moveCursorLineStart)
bind("all"    , "s"  , "home"     , action_moveCursorLineStartAnchored)
bind("all"    , ""   , "end"      , action_moveCursorLineEnd)
bind("all"    , "s"  , "end"      , action_moveCursorLineEndAnchored)
bind("macos"  , "c"  , "left"     , action_moveCursorLineStart)
bind("macos"  , "cs" , "left"     , action_moveCursorLineStartAnchored)
bind("macos"  , "c"  , "right"    , action_moveCursorLineEnd)
bind("macos"  , "cs" , "right"    , action_moveCursorLineEndAnchored)

bind("all"    , "c"  , "home"     , action_moveCursorDocumentStart)
bind("all"    , "cs" , "home"     , action_moveCursorDocumentStartAnchored)
bind("all"    , "c"  , "end"      , action_moveCursorDocumentEnd)
bind("all"    , "cs" , "end"      , action_moveCursorDocumentEndAnchored)
bind("macos"  , "c"  , "up"       , action_moveCursorDocumentStart)
bind("macos"  , "cs" , "up"       , action_moveCursorDocumentStartAnchored)
bind("macos"  , "c"  , "down"     , action_moveCursorDocumentEnd)
bind("macos"  , "cs" , "down"     , action_moveCursorDocumentEndAnchored)

bind("all"    , ""   , "up"       , action_moveCursorLineUp)
bind("all"    , "s"  , "up"       , action_moveCursorLineUpAnchored)
bind("all"    , ""   , "down"     , action_moveCursorLineDown)
bind("all"    , "s"  , "down"     , action_moveCursorLineDownAnchored)

bind("all"    , ""   , "pageup"   , action_moveCursorPageUp)
bind("all"    , "s"  , "pageup"   , action_moveCursorPageUpAnchored)
bind("all"    , ""   , "pagedown" , action_moveCursorPageDown)
bind("all"    , "s"  , "pagedown" , action_moveCursorPageDownAnchored)

bind("all"    , ""   , "return"   , action_insertNewline)
bind("all"    , ""   , "kpenter"  , action_insertNewline)
bind("macos"  , "^"  , "o"        , action_insertNewline)

bind("all"    , ""   , "backspace", action_deleteCharacterLeft)
bind("all"    , ""   , "delete"   , action_deleteCharacterRight)
bind("macos"  , "^"  , "h"        , action_deleteCharacterLeft)
bind("macos"  , "^"  , "d"        , action_deleteCharacterRight)

bind("windows", "c"  , "backspace", action_deleteWordLeft)
bind("windows", "c"  , "delete"   , action_deleteWordRight)
bind("macos"  , "a"  , "backspace", action_deleteWordLeft)
bind("macos"  , "a"  , "delete"   , action_deleteWordRight) -- Guessed macOS equivalent.  @Incomplete: Handle keyboards with no 'right-delete' key.

bind("all"    , "c"  , "a"        , action_selectAll)

bind("all"    , "c"  , "c"        , action_copy)
bind("windows", "c"  , "insert"   , action_copy)

bind("all"    , "c"  , "x"        , action_cut)
bind("windows", "s"  , "delete"   , action_cut)

bind("all"    , "c"  , "v"        , action_paste)
bind("windows", "s"  , "insert"   , action_paste)
bind("macos"  , "cas", "v"        , action_paste)

bind("all"    , "c"  , "z"        , action_undo)
bind("all"    , "cs" , "z"        , action_redo)
bind("windows", "c"  , "y"        , action_redo)

-- @Incomplete: Bind 'delete from line start'. windows=ctrl+shift+backspace
-- @Incomplete: Bind 'delete to line end'. windows=ctrl+shift+delete



-- eventWasHandled, textWasEdited = field:keypressed( key, isRepeat )
function InputField.keypressed(field, key, isRepeat)
	if field.dragMode ~= "" then
		-- Escape while dragging: Stop dragging.
		if key == "escape" then  field:releaseMouse()  end

		return true, false
	end

	local keyHandler = keyHandlers[getModKeys()][key]

	if keyHandler then
		return keyHandler(field, isRepeat)
	else
		return false, false
	end
end

-- eventWasHandled, textWasEdited = field:textinput( text )
function InputField.textinput(field, text)
	if field.dragMode ~= ""     then  return true, false  end
	if not field.editingEnabled then  return true, false  end
	field:insert(applyFilter(field, text))
	return true, true
end



local function nextLine(field, lineI)
	lineI      = lineI + 1
	local line = field.wrappedText[lineI]

	if not line then  return nil  end

	local fontH    = field.font:getHeight()
	local lineDist = math.ceil(fontH*field.font:getLineHeight())

	return lineI,
		line,
		alignOnLine(field, line, 0) - math.floor(field.scrollX),
		(lineI - 1) * lineDist      - math.floor(field.scrollY),
		field.font:getWidth(line),
		fontH
end

-- for index, line, lineX, lineY, lineWidth, lineHeight in field:eachVisibleLine( )
-- Note: The coordinates are relative to the field's position.
function InputField.eachVisibleLine(field)
	updateWrap(field)
	return nextLine, field, 0
end



local versionMajor, versionMinor = love.getVersion()
local hasGetKerningMethod        = false--(versionMajor > 11) or (versionMajor == 11 and versionMinor >= 4)
local cache                      = {}

local getSelectionLayoutOnLine = (
	hasGetKerningMethod and function(font, line, posOnLine1, posOnLine2) -- Currently worse than the fallback!
		local pos    = 0
		local x      = 0
		local x1     = 0
		local lastCp = 0

		for _, cp in utf8Codes(line) do
			pos = pos + 1

			if lastCp > 0 then
				x = x + font:getKerning(lastCp, cp)
			end
			cache[cp] = cache[cp] or utf8.char(cp) ; x = x + font:getWidth(cache[cp]) -- Not sure if the cache is doing anything...
			-- x = x + font:getWidth(utf8.char(cp))

			if pos == posOnLine1-1 then
				x1 = x
			end
			if pos == posOnLine2 then
				return x1, x -- @Polish: Handle kerning on the right end of the selection.
			end

			lastCp = cp
		end

		return x, x
	end

	or function(font, line, posOnLine1, posOnLine2)
		local preText       = line:sub(1, utf8.offset     (line, posOnLine1)-1) -- @Speed @Memory
		local preAndMidText = line:sub(1, utf8GetEndOffset(line, posOnLine2)  ) -- @Speed @Memory
		local x1            = font:getWidth(preText)
		local x2            = font:getWidth(preAndMidText) -- @Polish: Handle kerning on the right end of the selection.
		return x1, x2
	end
)

local function nextSelection(selections, i)
	i          = i + 1
	local line = selections[3*i-2]

	if not line then  return nil  end

	local field = selections.field
	local font  = field.font

	local posOnLine1 = selections[3*i-1]
	local posOnLine2 = selections[3*i  ]
	local x1, x2     = getSelectionLayoutOnLine(font, line, posOnLine1, posOnLine2)

	local fontH    = font:getHeight()
	local lineDist = math.ceil(fontH*font:getLineHeight())

	if selections[3*(i+1)] then
		x2 = x2 + font:getWidth(" ")
		--x2 = math.min(x2, field.width) -- Eh, this is a bad idea. Any scissoring should be done by the user.
	end

	return i,
		alignOnLine(field, line, x1)           - math.floor(field.scrollX),
		(selections.lineOffset + i) * lineDist - math.floor(field.scrollY),
		x2 - x1,
		fontH
end

local selectionsReused = {field=nil, lineOffset=0, --[[ line1, startPositionOnLine1, endPositionOnLine1, ... ]]}

local function _eachSelection(field, selections)
	-- updateWrap(field) -- getLineInfoAtPosition() calls this.

	local startLine, startPosOnLine, startLineI, startLinePos1, startLinePos2 = getLineInfoAtPosition(field, field.selectionStart)
	local   endLine,   endPosOnLine,   endLineI,   endLinePos1,   endLinePos2 = getLineInfoAtPosition(field, field.selectionEnd)

	-- Note: We include selections that are empty.
	selections.field      = field
	selections.lineOffset = startLineI - 2

	if startLineI == endLineI then
		selections[1] = startLine
		selections[2] = startPosOnLine + 1
		selections[3] = endPosOnLine

	else
		selections[1] = startLine
		selections[2] = startPosOnLine + 1
		selections[3] = startLinePos2 - startLinePos1 + 1

		for lineI = startLineI+1, endLineI-1 do
			local line = field.wrappedText[lineI]
			table.insert(selections, line)
			table.insert(selections, 1)
			table.insert(selections, utf8.len(line))
		end

		table.insert(selections, endLine)
		table.insert(selections, 1)
		table.insert(selections, endPosOnLine)
	end

	return nextSelection, selections, 0
end



--
-- for index, selectionX, selectionY, selectionWidth, selectionHeight in field:eachSelection( )
-- for index, selectionX, selectionY, selectionWidth, selectionHeight in field:eachSelectionOptimized( )
--
-- Note: The coordinates are relative to the field's position.
--
-- Note: It's possible for the right side of selections to extend slightly
-- past the specified width of the field in multi-line fields.
--
-- Warning: field:eachSelection() may chew through lots of memory if many
-- lines are selected! Consider using field:eachSelectionOptimized() instead.
--
-- Warning: field:eachSelectionOptimized() must not be called recursively!
--

function InputField.eachSelection(field)
	if field.selectionStart == field.selectionEnd then  return noop  end

	return _eachSelection(field, {}) -- @Speed @Memory: Don't create a new table every call!
end

function InputField.eachSelectionOptimized(field)
	if field.selectionStart == field.selectionEnd then  return noop  end

	for i = 4, #selectionsReused do -- selectionsReused will always have at least 3 items, so just ignore them here.
		selectionsReused[i] = nil
	end

	return _eachSelection(field, selectionsReused)
end



--==============================================================
--==============================================================
--==============================================================

return setmetatable(InputField, {

	-- InputField( [ initialText="", type:InputFieldType="normal" ] )
	__call = function(InputField, text, fieldType)
		return newInputField(text, fieldType)
	end,

})

--==============================================================
--=
--=  MIT License
--=
--=  Copyright © 2017-2022 Marcus 'ReFreezed' Thunström
--=
--=  Permission is hereby granted, free of charge, to any person obtaining a copy
--=  of this software and associated documentation files (the "Software"), to deal
--=  in the Software without restriction, including without limitation the rights
--=  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
--=  copies of the Software, and to permit persons to whom the Software is
--=  furnished to do so, subject to the following conditions:
--=
--=  The above copyright notice and this permission notice shall be included in all
--=  copies or substantial portions of the Software.
--=
--=  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
--=  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
--=  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
--=  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
--=  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
--=  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
--=  SOFTWARE.
--=
--==============================================================
