Widget Progress
I've added a textfield widget script to the beta branch, and a new build, for (Lua interpreter, Windows only, at this time). The textfield widget allows editing of a single line of text. It's actually one of the more difficult widgets to implement due to all the user interaction features. Text is entered from the keyboard and may be selected with arrow keys or by clicking the mouse. A range of text can be selected by clicking and dragging the mouse, or by pressing an arrow key while the shift key is pressed.
I had to implement an additional keyboard event. KeyDown and KeyEvents work for all keys, but KeyChar events are called when typing results in an actual character. The ASCII code of the typed character is sent in the data parameter of the event function:
function Script:KeyChar( charcode ) end
Making the caret indicator flash on an off goes against the event-driven nature of this system, but I think it's an important visual indicator and I wanted to include it. I went through a few ideas including a really over-engineered timer system. Finally I just decided to make the GUI call a function on the focused widget every 500 milliseconds (if the function is present in the widget's script):
--Blink the caret cursor on and off
function Script:CursorBlink()
if self.cursorblinkmode == nil then
self.cursorblinkmode = false
end
self.cursorblinkmode = not self.cursorblinkmode
self.widget:Redraw()
end
All in all, the script weighs in at 270 lines of code. It does not handle cut, copy, and paste yet, and double-clicking to select the entire text does not yet consider spaces in the clicked word. The drawing function is actually quite simple, so you could easily skin this to get a different appearance and keep the same behavior.
Script.caretposition=0
Script.sellen=0
Script.doubleclickrange = 1
Script.doubleclicktime = 500
function Script:Draw(x,y,width,height)
local gui = self.widget:GetGUI()
local pos = self.widget:GetPosition(true)
local sz = self.widget:GetSize(true)
local scale = gui:GetScale()
local item = self.widget:GetSelectedItem()
local text = self.widget:GetText()
--Draw the widget background
gui:SetColor(0.2,0.2,0.2)
gui:DrawRect(pos.x,pos.y,sz.width,sz.height,0)
--Draw the widget outline
if self.hovered==true then
gui:SetColor(51/255/4,151/255/4,1/4)
else
gui:SetColor(0,0,0)
end
gui:DrawRect(pos.x,pos.y,sz.width,sz.height,1)
--Draw text selection background
if self.sellen~=0 then
local n
local w
local x = gui:GetScale()*8
local px = x
local c1 = math.min(self.caretposition,self.caretposition+self.sellen)
local c2 = math.max(self.caretposition,self.caretposition+self.sellen)
for n=0,c2-1 do
if n==c1 then
px = x
end
c = String:Mid(text,n,1)
x = x + gui:GetTextWidth(c)
if n==c2-1 then
w = x-px
end
end
gui:SetColor(0.4,0.4,0.4)
gui:DrawRect(pos.x + px,pos.y+2*scale,w,sz.height-4*scale,0)
end
--Draw text
gui:SetColor(0.75,0.75,0.75)
if text~="" then
gui:DrawText(text,scale*8+pos.x,pos.y,sz.width,sz.height,Text.Left+Text.VCenter)
end
--Draw the caret
if self.cursorblinkmode then
if self.focused then
local x = self:GetCaretCoord(text)
gui:DrawLine(scale*8+pos.x + x,pos.y+2*scale,scale*8+pos.x + x,pos.y + sz.height-4*scale)
end
end
end
--Find the character position for the given x coordinate
function Script:GetCharAtPosition(pos)
local text = self.widget:GetText()
local gui = self.widget:GetGUI()
local n
local c
local x = gui:GetScale()*8
local count = String:Length(text)
local lastcharwidth=0
for n=0,count-1 do
c = String:Mid(text,n,1)
lastcharwidth = gui:GetTextWidth(c)
if x >= pos - lastcharwidth/2 then return n end
x = x + lastcharwidth
end
return count
end
--Get the x coordinate of the current caret position
function Script:GetCaretCoord()
local text = self.widget:GetText()
local gui = self.widget:GetGUI()
local n
local c
local x=0
local count = math.min(self.caretposition-1,(String:Length(text)-1))
for n=0,count do
c = String:Mid(text,n,1)
x = x + gui:GetTextWidth(c)
end
return x
end
--Blink the caret cursor on and off
function Script:CursorBlink()
if self.cursorblinkmode == nil then
self.cursorblinkmode = false
end
self.cursorblinkmode = not self.cursorblinkmode
self.widget:Redraw()
end
function Script:MouseDown(button,x,y)
self.focused=true
if button==Mouse.Left then
--Detect double-click and select entire text
local currenttime = Time:Millisecs()
if self.lastmousehittime~=nil then
if math.abs(self.lastmouseposition.x-x)<=self.doubleclickrange and math.abs(self.lastmouseposition.y-y)<=self.doubleclickrange then
if currenttime - self.lastmousehittime < self.doubleclicktime then
self.lastmousehittime = currenttime
local l = String:Length(self.widget:GetText())
self.caretposition = l
self.sellen = -l
self.widget:GetGUI():ResetCursorBlink()
self.cursorblinkmode=true
self.pressed=false
self.widget:Redraw()
return
end
end
end
self.lastmouseposition = {}
self.lastmouseposition.x = x
self.lastmouseposition.y = y
self.lastmousehittime = currenttime
--Position caret under mouse click
self.cursorblinkmode=true
self.caretposition = self:GetCharAtPosition(x)
self.widget:GetGUI():ResetCursorBlink()
self.cursorblinkmode=true
self.pressed=true
self.sellen=0
self.widget:Redraw()
end
end
function Script:MouseUp(button,x,y)
if button==Mouse.Left then
self.pressed=false
end
end
function Script:MouseMove(x,y)
if self.pressed then
--Select range of characters
local currentcaretpos = self.caretposition
local prevcaretpos = self.caretposition + self.sellen
self.cursorblinkmode=true
self.caretposition = self:GetCharAtPosition(x)
if self.caretposition ~= currentcaretpos then
self.widget:GetGUI():ResetCursorBlink()
self.cursorblinkmode=true
self.sellen = prevcaretpos - self.caretposition
self.widget:Redraw()
end
end
end
function Script:LoseFocus()
self.focused=false
self.widget:Redraw()
end
function Script:MouseEnter(x,y)
self.hovered = true
self.widget:Redraw()
end
function Script:MouseLeave(x,y)
self.hovered = false
self.widget:Redraw()
end
function Script:KeyUp(keycode)
if keycode==Key.Shift then
self.shiftpressed=false
end
end
function Script:KeyDown(keycode)
if keycode==Key.Shift then
self.shiftpressed=true
end
if keycode==Key.Up or keycode==Key.Left then
--Move the caret one character left
local text = self.widget:GetText()
if self.caretposition>0 then
self.caretposition = self.caretposition - 1
self.widget:GetGUI():ResetCursorBlink()
self.cursorblinkmode=true
if self.shiftpressed then
self.sellen = self.sellen + 1
else
self.sellen = 0
end
self.widget:Redraw()
end
elseif keycode==Key.Down or keycode==Key.Right then
--Move the caret one character right
local text = self.widget:GetText()
if self.caretposition<String:Length(text) then
self.caretposition = self.caretposition + 1
self.widget:GetGUI():ResetCursorBlink()
self.cursorblinkmode=true
if self.shiftpressed then
self.sellen = self.sellen - 1
else
self.sellen = 0
end
self.widget:Redraw()
end
end
end
function Script:KeyChar(charcode)
local s = self.widget:GetText()
local c = String:Chr(charcode)
if c=="\b" then
--Backspace
if String:Length(s)>0 then
if self.sellen==0 then
if self.caretposition==String:Length(s) then
s = String:Left(s,String:Length(s)-1)
elseif self.caretposition>0 then
s = String:Left(s,self.caretposition-1)..String:Right(s,String:Length(s)-self.caretposition)
end
self.caretposition = self.caretposition - 1
self.caretposition = math.max(0,self.caretposition)
else
local c1 = math.min(self.caretposition,self.caretposition+self.sellen)
local c2 = math.max(self.caretposition,self.caretposition+self.sellen)
s = String:Left(s,c1)..String:Right(s,String:Length(s) - c2)
self.caretposition = c1
self.sellen = 0
end
self.widget:GetGUI():ResetCursorBlink()
self.cursorblinkmode=true
self.widget:SetText(s)
EventQueue:Emit(Event.WidgetAction,self.widget)
end
elseif c~="\r" and c~="" then
--Insert a new character
local c1 = math.min(self.caretposition,self.caretposition+self.sellen)
local c2 = math.max(self.caretposition,self.caretposition+self.sellen)
s = String:Left(s,c1)..c..String:Right(s,String:Length(s) - c2)
self.caretposition = self.caretposition + 1
if self.sellen<0 then self.caretposition = self.caretposition + self.sellen end
self.sellen=0
self.widget:GetGUI():ResetCursorBlink()
self.cursorblinkmode=true
self.widget:SetText(s)
EventQueue:Emit(Event.WidgetAction,self.widget)
end
end
-
8

11 Comments
Recommended Comments