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