Files
love-luigi/luigi/widget/text.lua
2015-12-15 14:21:30 -05:00

310 lines
8.0 KiB
Lua

--[[--
A text entry area.
@widget text
--]]--
local ROOT = (...):gsub('[^.]*.[^.]*$', '')
local utf8 = require(ROOT .. 'utf8')
local Backend = require(ROOT .. 'backend')
local function scrollToCaret (self)
local x1, y1, w, h = self:getRectangle(true, true)
local x2, y2 = x1 + w, y1 + h
local oldX = self.endX
local newX
if oldX <= x1 then
self.scrollX = self.scrollX - (x1 - oldX)
newX = x1
elseif oldX >= x2 then
self.scrollX = self.scrollX + (oldX - x2 + 1)
newX = x2 - 1
end
if newX then
self.endX = newX
self.startX = self.startX + (newX - oldX)
end
end
local function findCaretFromText (self, text)
local font = self.fontData
local x = self:getRectangle(true, true)
return #text, font:getAdvance(text) + x - self.scrollX
end
local function setCaretFromText (self, text, mode)
local index, x = findCaretFromText(self, text)
if mode == 'start' or not mode then
self.startIndex, self.startX = index, x
end
if mode == 'end' or not mode then
self.endIndex, self.endX = index, x
end
scrollToCaret(self)
end
-- return caret index and x position
local function findCaretFromPoint (self, x, y)
local x1 = self:getRectangle(true, true)
local font = self.fontData
local width, lastWidth = 0
local lastPosition = 0
local function checkPosition (position)
local text = self.value:sub(1, position - 1)
lastWidth = width
width = font:getAdvance(text)
if width > x + self.scrollX - x1 then
if position == 1 then
return 0, x1 - self.scrollX
end
return lastPosition, lastWidth + x1 - self.scrollX
end
lastPosition = position - 1
end
for position in utf8.codes(self.value) do
local a, b = checkPosition(position)
if a then return a, b end
end
local a, b = checkPosition(#self.value + 1)
if a then return a, b end
return #self.value, width + x1 - self.scrollX
end
-- move the caret one character to the left
local function moveCaretLeft (self, alterRange)
local text, endIndex = self.value, self.endIndex
-- if cursor is at beginning, do nothing
if endIndex < 1 then
return false
end
-- move left
local mode = alterRange and 'end'
local offset = utf8.offset(text, -1, endIndex + 1) or 0
setCaretFromText(self, text:sub(1, offset - 1), mode)
end
-- move the caret one character to the right
local function moveCaretRight (self, alterRange)
local text, endIndex = self.value, self.endIndex
-- if cursor is at end, do nothing
if endIndex == #text then
return false
end
local mode = alterRange and 'end'
local offset = utf8.offset(text, 2, endIndex + 1) or #text
-- move right
setCaretFromText(self, text:sub(1, offset - 1), mode)
end
local function getRange (self)
if self.startIndex <= self.endIndex then
return self.startIndex, self.endIndex
end
return self.endIndex, self.startIndex
end
local function deleteRange (self)
local text = self.value
local first, last = getRange(self)
-- if expanded range is selected, delete text in range
if first ~= last then
local left = text:sub(1, first)
text = left .. text:sub(last + 1)
self.value = text
setCaretFromText(self, left)
return true
end
end
local function deleteCharacterLeft (self)
local text = self.value
local first, last = getRange(self)
-- if cursor is at beginning, do nothing
if first < 1 then
return
end
-- delete character to the left
local offset = utf8.offset(text, -1, last + 1) or 0
local left = text:sub(1, offset - 1)
text = left .. text:sub(first + 1)
self.value = text
setCaretFromText(self, left)
end
local function copyRangeToClipboard (self)
local text = self.value
local first, last = getRange(self)
if last >= first + 1 then
Backend.setClipboardText(text:sub(first + 1, last))
end
end
local function pasteFromClipboard (self)
local text = self.value
local pasted = Backend.getClipboardText() or ''
local first, last = getRange(self)
local left = text:sub(1, first) .. pasted
text = left .. text:sub(last + 1)
self.value = text
setCaretFromText(self, left)
end
local function insertText (self, newText)
local text = self.value
local first, last = getRange(self)
local left = text:sub(1, first) .. newText
self.value = left .. text:sub(last + 1)
setCaretFromText(self, left)
end
return function (self)
if not self.fontData then
self.fontData = Backend.Font(self.font, self.size)
end
self.value = tostring(self.value or self.text or '')
self.text = ''
--[[--
Special Attributes
@section special
--]]--
--[[--
Highlight color.
Should contain an array with 3 or 4 values (RGB or RGBA) from 0 to 255.
This color is used to indicate the selected range of text.
@attrib highlight
--]]--
self:defineAttribute('highlight')
--[[--
@section end
--]]--
if not self.highlight then
self.highlight = { 0x80, 0x80, 0x80 }
end
self.scrollX = 0
setCaretFromText(self, self.value)
self:onPressStart(function (event)
self.startIndex, self.startX = findCaretFromPoint(self, event.x)
self.endIndex, self.endX = self.startIndex, self.startX
end)
self:onPressDrag(function (event)
self.endIndex, self.endX = findCaretFromPoint(self, event.x)
scrollToCaret(self)
end)
self:onTextInput(function (event)
insertText(self, event.text)
end)
self:onKeyPress(function (event)
-- ignore tabs (keyboard navigation)
if event.key == 'tab' then
return
end
-- focus next widget on enter (keyboard navigation)
if event.key == 'return' then
self.layout:focusNextWidget()
-- if the next widget is a button, allow the event to propagate
-- so that the button is pressed (TODO: is this a good idea?)
return self.layout.focusedWidget.type ~= 'button' or nil
end
if event.key == 'backspace' then
if not deleteRange(self) then
deleteCharacterLeft(self)
end
elseif event.key == 'left' then
moveCaretLeft(self, Backend.isKeyDown('lshift', 'rshift'))
elseif event.key == 'right' then
moveCaretRight(self, Backend.isKeyDown('lshift', 'rshift'))
elseif event.key == 'x' and Backend.isKeyDown('lctrl', 'rctrl') then
copyRangeToClipboard(self)
deleteRange(self)
elseif event.key == 'c' and Backend.isKeyDown('lctrl', 'rctrl') then
copyRangeToClipboard(self)
elseif event.key == 'v' and Backend.isKeyDown('lctrl', 'rctrl') then
pasteFromClipboard(self)
end
return false
end)
self:onDisplay(function (event)
local startX, endX = self.startX or 0, self.endX or 0
local x, y, w, h = self:getRectangle(true, true)
local width, height = endX - startX, h
local font = self.fontData
local color = self.color or { 0, 0, 0, 255 }
local textTop = math.floor(y + (h - font:getLineHeight()) / 2)
Backend.push()
Backend.setScissor(x, y, w, h)
Backend.setFont(font)
if self.focused then
-- draw highlighted selection
Backend.setColor(self.highlight)
Backend.drawRectangle('fill', startX, y, width, height)
-- draw cursor selection
if Backend.getTime() % 2 < 1.75 then
Backend.setColor(color)
Backend.drawRectangle('fill', endX, y, 1, height)
end
else
Backend.setColor { color[1], color[2], color[3],
(color[4] or 256) / 8 }
Backend.drawRectangle('fill', startX, y, width, height)
end
-- draw text
Backend.setColor(color)
Backend.print(self.value, x - self.scrollX, textTop)
Backend.pop()
end)
end