Jump to content
  • entries
    943
  • comments
    5,899
  • views
    924,382

Widget Progress


Josh

3,733 views

 Share

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.

 

blogentry-1-0-15728200-1469119892.png

 

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

  • Upvote 8
 Share

11 Comments


Recommended Comments

Really awesome! Once the full release comes out, I'm totally gonna revamp my developer console with the new GUI system! :)

  • Upvote 3
Link to comment

The plan is to match the MaxGUI gadget types:

  • Panel
  • Slider
  • Menu
  • Toolbar
  • Tabber
  • Label
  • Button
  • Text Field
  • List Box
  • Combo Box
  • Tree View
  • Progress Bar

  • Upvote 5
Link to comment

Looks great so far, can't wait to switch over to the new Leadwerks Ui... finally I'll be able to give my client application a nice developer console. Using pdcurses for the server. :/

Link to comment

Are you going to use this UI for the Leadwerks editor?

That's the plan. Because of that, you'll probably get a result that is a lot more complete than a standard game GUI.

  • Upvote 1
Link to comment

I also forgot to add the behavior to make the whole box scroll to the left when your text overruns the width of the widget. You can see how complicated all those little behaviors are when you are trying to emulate a desktop UI.

Link to comment

Will there be a rich text multi line control of some kind? Thinking of a developer console or chat log with different colored text.

Link to comment

Will there be a rich text multi line control of some kind? Thinking of a developer console or chat log with different colored text.

I will probably rely on Scintilla for this.

  • Upvote 1
Link to comment
Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...