diff --git a/game/engines/default/data/gfx/ui/button-left-sel.png b/game/engines/default/data/gfx/ui/button-left-sel.png
index 647e3e36c00305138552219ee38885bc5ec880b3..532f8c461f0ee3e525972c049655df5cab07ce48 100644
Binary files a/game/engines/default/data/gfx/ui/button-left-sel.png and b/game/engines/default/data/gfx/ui/button-left-sel.png differ
diff --git a/game/engines/default/data/gfx/ui/button-middle-sel.png b/game/engines/default/data/gfx/ui/button-middle-sel.png
index 8b32a9d0b3e0b784c34e1c1360cddb4befeeb94a..306f7cf1d62dd567a18886ce863286f842fe139e 100644
Binary files a/game/engines/default/data/gfx/ui/button-middle-sel.png and b/game/engines/default/data/gfx/ui/button-middle-sel.png differ
diff --git a/game/engines/default/data/gfx/ui/button-right-sel.png b/game/engines/default/data/gfx/ui/button-right-sel.png
index c504f1063aa2791b584275a46bae07dc7d0a2a05..8a2d303027939a896fda6fff73b25fc339732910 100644
Binary files a/game/engines/default/data/gfx/ui/button-right-sel.png and b/game/engines/default/data/gfx/ui/button-right-sel.png differ
diff --git a/game/engines/default/data/gfx/ui/scrollbar-sel.png b/game/engines/default/data/gfx/ui/scrollbar-sel.png
new file mode 100644
index 0000000000000000000000000000000000000000..21ebe87149a225adf15a59dd4d1edbd3b6020ae1
Binary files /dev/null and b/game/engines/default/data/gfx/ui/scrollbar-sel.png differ
diff --git a/game/engines/default/data/gfx/ui/scrollbar.png b/game/engines/default/data/gfx/ui/scrollbar.png
new file mode 100644
index 0000000000000000000000000000000000000000..c20574112f672353bab2e0f58fb4d66fbea4e908
Binary files /dev/null and b/game/engines/default/data/gfx/ui/scrollbar.png differ
diff --git a/game/engines/default/data/gfx/ui/selection-left-column.png b/game/engines/default/data/gfx/ui/selection-left-column.png
index a19defb42a04feb90f4be8dfcf29749d901ba9a3..2736bd608e340d4a7d7e4e79e85e28108d69095d 100644
Binary files a/game/engines/default/data/gfx/ui/selection-left-column.png and b/game/engines/default/data/gfx/ui/selection-left-column.png differ
diff --git a/game/engines/default/data/gfx/ui/selection-middle-column.png b/game/engines/default/data/gfx/ui/selection-middle-column.png
index 0ba88ec21c4f790e15f0b1c93faf1198967068cd..d0b9deacc678589f865cb10eb7c906c724c00c8a 100644
Binary files a/game/engines/default/data/gfx/ui/selection-middle-column.png and b/game/engines/default/data/gfx/ui/selection-middle-column.png differ
diff --git a/game/engines/default/data/gfx/ui/selection-right-column.png b/game/engines/default/data/gfx/ui/selection-right-column.png
index 0a87daeaf0f25a831463cf0957f929e09ccd3be3..12fb9dc2218317c6c466cf33bba1ca63879cc926 100644
Binary files a/game/engines/default/data/gfx/ui/selection-right-column.png and b/game/engines/default/data/gfx/ui/selection-right-column.png differ
diff --git a/game/engines/default/data/gfx/ui/textbox-cursor.png b/game/engines/default/data/gfx/ui/textbox-cursor.png
new file mode 100644
index 0000000000000000000000000000000000000000..ec01ddb954443f2762c76fbb92d568fccf89f762
Binary files /dev/null and b/game/engines/default/data/gfx/ui/textbox-cursor.png differ
diff --git a/game/engines/default/data/gfx/ui/textbox-left-sel.png b/game/engines/default/data/gfx/ui/textbox-left-sel.png
new file mode 100644
index 0000000000000000000000000000000000000000..6084377ca688ef4773c017077cf59839f2686741
Binary files /dev/null and b/game/engines/default/data/gfx/ui/textbox-left-sel.png differ
diff --git a/game/engines/default/data/gfx/ui/textbox-left.png b/game/engines/default/data/gfx/ui/textbox-left.png
new file mode 100644
index 0000000000000000000000000000000000000000..89fe2cdfdc2fdf4338e4172eebeb9d3bed7c0aec
Binary files /dev/null and b/game/engines/default/data/gfx/ui/textbox-left.png differ
diff --git a/game/engines/default/data/gfx/ui/textbox-middle-sel.png b/game/engines/default/data/gfx/ui/textbox-middle-sel.png
new file mode 100644
index 0000000000000000000000000000000000000000..cdc5a1b979d850afb785c6e41ce24a2c76cb6ba2
Binary files /dev/null and b/game/engines/default/data/gfx/ui/textbox-middle-sel.png differ
diff --git a/game/engines/default/data/gfx/ui/textbox-middle.png b/game/engines/default/data/gfx/ui/textbox-middle.png
new file mode 100644
index 0000000000000000000000000000000000000000..1df1da27dcbc40aa76acb816e68d676a8640b534
Binary files /dev/null and b/game/engines/default/data/gfx/ui/textbox-middle.png differ
diff --git a/game/engines/default/data/gfx/ui/textbox-right-sel.png b/game/engines/default/data/gfx/ui/textbox-right-sel.png
new file mode 100644
index 0000000000000000000000000000000000000000..1bb754d41584d646ef17c7f5a0f950eb8cc0799f
Binary files /dev/null and b/game/engines/default/data/gfx/ui/textbox-right-sel.png differ
diff --git a/game/engines/default/data/gfx/ui/textbox-right.png b/game/engines/default/data/gfx/ui/textbox-right.png
new file mode 100644
index 0000000000000000000000000000000000000000..2c8464028bfa4c3e9cdc11615de58c114e44b55f
Binary files /dev/null and b/game/engines/default/data/gfx/ui/textbox-right.png differ
diff --git a/game/engines/default/engine/ui/Base.lua b/game/engines/default/engine/ui/Base.lua
index 584ee68258b11123bf2f054ba8639dbbcaccfbcc..6c6f9a58fb2b3b1e3214da10b792ae2f54cc2892 100644
--- a/game/engines/default/engine/ui/Base.lua
+++ b/game/engines/default/engine/ui/Base.lua
@@ -30,6 +30,9 @@ local cache = {}
 -- Default font
 _M.font = core.display.newFont("/data/font/Vera.ttf", 12)
 _M.font_h = _M.font:lineSkip()
+_M.font_mono = core.display.newFont("/data/font/VeraMono.ttf", 12)
+_M.font_mono_w = _M.font_mono:size(" ")
+_M.font_mono_h = _M.font_mono:lineSkip()
 
 function _M:init(t, no_gen)
 	self.mouse = Mouse.new()
diff --git a/game/engines/default/engine/ui/Dialog.lua b/game/engines/default/engine/ui/Dialog.lua
index 9372ffe8e543bd5a0e7f63340aaf7691ee64ffd8..5682f154f774be600912eb925aee0db20e84a560 100644
--- a/game/engines/default/engine/ui/Dialog.lua
+++ b/game/engines/default/engine/ui/Dialog.lua
@@ -88,7 +88,7 @@ function _M:generate()
 	self.mouse:registerZone(0, 0, gamew, gameh, function() self.key:triggerVirtual("EXIT") end)
 	self.mouse:registerZone(self.display_x, self.display_y, self.w, self.h, function(...) self:mouseEvent(...) end)
 	self.key.receiveKey = function(_, ...) self:keyEvent(...) end
-	self.key:addCommand("__TAB", function(...) self.key:triggerVirtual("MOVE_DOWN") end)
+	self.key:addCommand("_TAB", function(...) self.key:triggerVirtual("MOVE_DOWN") end)
 	self.key:addBinds{
 		MOVE_UP = function() self:setFocus(util.boundWrap(self.focus_ui_id - 1, 1, #self.uis)) end,
 		MOVE_DOWN = function() self:setFocus(util.boundWrap(self.focus_ui_id + 1, 1, #self.uis)) end,
diff --git a/game/engines/default/engine/ui/List.lua b/game/engines/default/engine/ui/List.lua
index 5351bb3197d629cf173ecb58b9ee6e4da6cf246c..7358d286dfe14dc92eb492adaef1ca38abf082af 100644
--- a/game/engines/default/engine/ui/List.lua
+++ b/game/engines/default/engine/ui/List.lua
@@ -78,7 +78,7 @@ function _M:generate()
 
 	-- Add UI controls
 	self.mouse:registerZone(0, 0, self.w, self.h, function(button, x, y, xrel, yrel, bx, by, event)
-		self.sel = util.bound(self.scroll + math.floor(by / self.fh), 1, self.max)
+		if button ~= "wheelup" and button ~= "wheeldown" then self.sel = util.bound(self.scroll + math.floor(by / self.fh), 1, self.max) end
 		if button == "left" and event == "button" then self:onUse()
 		elseif button == "wheelup" and event == "button" then self.key:triggerVirtual("MOVE_UP")
 		elseif button == "wheeldown" and event == "button" then self.key:triggerVirtual("MOVE_DOWN")
diff --git a/game/engines/default/engine/ui/ListColumns.lua b/game/engines/default/engine/ui/ListColumns.lua
index 055cc2b91377bce22a8d76f2e99371fd8a56e0d6..335c3e216d6a341ead0694b203ca752ebf599e1d 100644
--- a/game/engines/default/engine/ui/ListColumns.lua
+++ b/game/engines/default/engine/ui/ListColumns.lua
@@ -29,8 +29,15 @@ function _M:init(t)
 	self.columns = assert(t.columns, "no list columns")
 	self.w = assert(t.width, "no list width")
 	self.h = assert(t.height, "no list height")
+	self.sortable = t.sortable
+	self.scrollbar = t.scrollbar
 	self.fct = t.fct
-	self.display_prop = t.display_prop or "name"
+
+	local w = self.w
+	if self.scrollbar then w = w - 10 end
+	for j, col in ipairs(self.columns) do
+		col.width = w * col.width / 100
+	end
 
 	Base.init(self, t)
 end
@@ -39,6 +46,7 @@ function _M:generate()
 	self.sel = 1
 	self.scroll = 1
 	self.max = #self.list
+	self:selectColumn(1, true)
 
 	local ls, ls_w, ls_h = self:getImage("ui/selection-left-sel.png")
 	local ms, ms_w, ms_h = self:getImage("ui/selection-middle-sel.png")
@@ -54,17 +62,35 @@ function _M:generate()
 	local cm, cm_w, cm_h = self:getImage("ui/selection-middle-column.png")
 	local cr, cr_w, cr_h = self:getImage("ui/selection-right-column.png")
 
-	local fw, fh = self.w, ls_h
-	self.fw, self.fh = fw, fh
+
+	local fh = ls_h
+	self.fh = fh
 
 	self.max_display = math.floor(self.h / fh) - 1
 
+	-- Draw the scrollbar
+	if self.scrollbar then
+		local sb, sb_w, sb_h = self:getImage("ui/scrollbar.png")
+		local ssb, ssb_w, ssb_h = self:getImage("ui/scrollbar-sel.png")
+
+		self.scrollbar = { bar = {}, sel = {} }
+		self.scrollbar.sel.w, self.scrollbar.sel.h, self.scrollbar.sel.tex, self.scrollbar.sel.texw, self.scrollbar.sel.texh = ssb_w, ssb_h, ssb:glTexture()
+		local s = core.display.newSurface(sb_w, self.h - fh)
+		s:erase(200,0,0)
+		for i = 0, self.h - fh do s:merge(sb, 0, i) end
+		self.scrollbar.bar.w, self.scrollbar.bar.h, self.scrollbar.bar.tex, self.scrollbar.bar.texw, self.scrollbar.bar.texh = ssb_w, self.h - fh, s:glTexture()
+	end
+
 	-- Draw the list columns
-	for i, col in ipairs(self.columns) do
+	local colx = 0
+	for j, col in ipairs(self.columns) do
+		local fw = col.width
+		col.fw = fw
 		local text = col.name
 		local ss = core.display.newSurface(fw, fh)
 		local s = core.display.newSurface(fw, fh)
 
+		self.font:setStyle("bold")
 		ss:merge(cls, 0, 0)
 		for i = cls_w, fw - crs_w do ss:merge(cms, i, 0) end
 		ss:merge(crs, fw - crs_w, 0)
@@ -74,49 +100,76 @@ function _M:generate()
 		for i = cl_w, fw - cr_w do s:merge(cm, i, 0) end
 		s:merge(cr, fw - cr_w, 0)
 		s:drawColorStringBlended(self.font, text, cl_w, (fh - self.font_h) / 2, 255, 255, 255, nil, fw - cl_w - cr_w)
+		self.font:setStyle("normal")
 
 		col._tex, col._tex_w, col._tex_h = s:glTexture()
 		col._stex = ss:glTexture()
-	end
 
-	-- Draw the list items
-	for i, item in ipairs(self.list) do
-		local text = item[self.display_prop]
-		local ss = core.display.newSurface(fw, fh)
-		local sus = core.display.newSurface(fw, fh)
-		local s = core.display.newSurface(fw, fh)
-
-		ss:merge(ls, 0, 0)
-		for i = ls_w, fw - rs_w do ss:merge(ms, i, 0) end
-		ss:merge(rs, fw - rs_w, 0)
-		ss:drawColorStringBlended(self.font, text, ls_w, (fh - self.font_h) / 2, 255, 255, 255, nil, fw - ls_w - rs_w)
-
-		s:erase(0, 0, 0)
-		s:drawColorStringBlended(self.font, text, ls_w, (fh - self.font_h) / 2, 255, 255, 255, nil, fw - ls_w - rs_w)
-
-		sus:merge(l, 0, 0)
-		for i = l_w, fw - r_w do sus:merge(m, i, 0) end
-		sus:merge(r, fw - r_w, 0)
-		sus:drawColorStringBlended(self.font, text, ls_w, (fh - self.font_h) / 2, 255, 255, 255, nil, fw - ls_w - rs_w)
+		-- Draw the list items
+		for i, item in ipairs(self.list) do
+			local text = tostring(item[col.display_prop or col.sort])
+			local ss = core.display.newSurface(fw, fh)
+			local sus = core.display.newSurface(fw, fh)
+			local s = core.display.newSurface(fw, fh)
+
+			ss:merge(ls, 0, 0)
+			for i = ls_w, fw - rs_w do ss:merge(ms, i, 0) end
+			ss:merge(rs, fw - rs_w, 0)
+			ss:drawColorStringBlended(self.font, text, ls_w, (fh - self.font_h) / 2, 255, 255, 255, nil, fw - ls_w - rs_w)
+
+			s:erase(0, 0, 0)
+			s:drawColorStringBlended(self.font, text, ls_w, (fh - self.font_h) / 2, 255, 255, 255, nil, fw - ls_w - rs_w)
+
+			sus:merge(l, 0, 0)
+			for i = l_w, fw - r_w do sus:merge(m, i, 0) end
+			sus:merge(r, fw - r_w, 0)
+			sus:drawColorStringBlended(self.font, text, ls_w, (fh - self.font_h) / 2, 255, 255, 255, nil, fw - ls_w - rs_w)
+
+			item._tex = item._tex or {}
+			item._stex = item._stex or {}
+			item._sustex = item._sustex or {}
+			item._tex[j] = {s:glTexture()}
+			item._stex[j] = {ss:glTexture()}
+			item._sustex[j] = {sus:glTexture()}
+		end
 
-		item._tex, item._tex_w, item._tex_h = s:glTexture()
-		item._stex = ss:glTexture()
-		item._sustex = sus:glTexture()
+		self.mouse:registerZone(colx, 0, col.width, self.fh, function(button, x, y, xrel, yrel, bx, by, event)
+			if button == "left" and event == "button" then self:selectColumn(j) end
+		end)
+		colx = colx + col.width
 	end
 
 	-- Add UI controls
-	self.mouse:registerZone(0, 0, self.w, self.h, function(button, x, y, xrel, yrel, bx, by, event)
+	self.mouse:registerZone(0, self.fh, self.w, self.h - self.fh, function(button, x, y, xrel, yrel, bx, by, event)
+		if button == "wheelup" and event == "button" then self.scroll = util.bound(self.scroll - 1, 1, self.max - self.max_display + 1)
+		elseif button == "wheeldown" and event == "button" then self.scroll = util.bound(self.scroll + 1, 1, self.max - self.max_display + 1) end
+
 		self.sel = util.bound(self.scroll + math.floor(by / self.fh), 1, self.max)
-		if button == "left" and event == "button" then self:onUse()
-		elseif button == "wheelup" and event == "button" then self.key:triggerVirtual("MOVE_UP")
-		elseif button == "wheeldown" and event == "button" then self.key:triggerVirtual("MOVE_DOWN")
-		end
+		if button == "left" and event == "button" then self:onUse() end
 	end)
 	self.key:addBinds{
 		ACCEPT = function() self:onUse() end,
 		MOVE_UP = function() self.sel = util.boundWrap(self.sel - 1, 1, self.max) self.scroll = util.scroll(self.sel, self.scroll, self.max_display) end,
 		MOVE_DOWN = function() self.sel = util.boundWrap(self.sel + 1, 1, self.max) self.scroll = util.scroll(self.sel, self.scroll, self.max_display) end,
 	}
+	self.key:addCommands{
+		_HOME = function()
+			self.sel = 1
+			self.scroll = util.scroll(self.sel, self.scroll, self.max_display)
+		end,
+		_END = function()
+			self.sel = self.max
+			self.scroll = util.scroll(self.sel, self.scroll, self.max_display)
+		end,
+		_PAGEUP = function()
+			self.sel = util.bound(self.sel - self.max_display, 1, self.max)
+			self.scroll = util.scroll(self.sel, self.scroll, self.max_display)
+		end,
+		_PAGEDOWN = function()
+			self.sel = util.bound(self.sel + self.max_display, 1, self.max)
+			self.scroll = util.scroll(self.sel, self.scroll, self.max_display)
+		end,
+	}
 end
 
 function _M:onUse()
@@ -126,27 +179,58 @@ function _M:onUse()
 	else self.fct(item) end
 end
 
-function _M:selectColumn(i)
+function _M:selectColumn(i, force)
+	if not self.sortable and not force then return end
 	local col = self.columns[i]
-	self.cur_col = i
+	if not col then return end
+
+	if self.cur_col ~= i then
+		self.cur_col = i
+		self.sort_reverse = false
+	else
+		self.sort_reverse = not self.sort_reverse
+	end
+
+	local fct = col.sort
+	if type(fct) == "string" then fct = function(a, b) return a[col.sort] < b[col.sort] end end
+	if self.sort_reverse then local old=fct fct = function(a, b) return old(b, a) end end
+	table.sort(self.list, fct)
 end
 
 function _M:display(x, y)
+	local bx, by = x, y
+
 	for j = 1, #self.columns do
-		local y = y
+		local col = self.columns[j]
+		if self.cur_col == j then
+			col._stex:toScreenFull(x, y, col.fw, self.fh, col._tex_w, col._tex_h)
+		else
+			col._tex:toScreenFull(x, y, col.fw, self.fh, col._tex_w, col._tex_h)
+		end
+
+		local y = y + self.fh
 		local max = math.min(self.scroll + self.max_display - 1, self.max)
 		for i = self.scroll, max do
 			local item = self.list[i]
 			if self.sel == i then
 				if self.focused then
-					item._stex:toScreenFull(x, y, self.fw, self.fh, item._tex_w, item._tex_h)
+					item._stex[j][1]:toScreenFull(x, y, col.fw, self.fh, item._stex[j][2], item._stex[j][3])
 				else
-					item._sustex:toScreenFull(x, y, self.fw, self.fh, item._tex_w, item._tex_h)
+					item._sustex[j][1]:toScreenFull(x, y, col.fw, self.fh, item._sustex[j][2], item._sustex[j][3])
 				end
 			else
-				item._tex:toScreenFull(x, y, self.fw, self.fh, item._tex_w, item._tex_h)
+				item._tex[j][1]:toScreenFull(x, y, col.fw, self.fh, item._tex[j][2], item._tex[j][3])
 			end
 			y = y + self.fh
 		end
+
+		x = x + col.width
+	end
+
+	if self.focused and self.scrollbar then
+		local pos = self.sel * (self.h - self.fh) / self.max
+
+		self.scrollbar.bar.tex:toScreenFull(bx + self.w - self.scrollbar.bar.w, by + self.fh, self.scrollbar.bar.w, self.scrollbar.bar.h, self.scrollbar.bar.texw, self.scrollbar.bar.texh)
+		self.scrollbar.sel.tex:toScreenFull(bx + self.w - self.scrollbar.sel.w, by + self.fh + pos, self.scrollbar.sel.w, self.scrollbar.sel.h, self.scrollbar.sel.texw, self.scrollbar.sel.texh)
 	end
 end
diff --git a/game/engines/default/engine/ui/Textbox.lua b/game/engines/default/engine/ui/Textbox.lua
new file mode 100644
index 0000000000000000000000000000000000000000..b7652d29e8346486e4238556e91731a246d53d0f
--- /dev/null
+++ b/game/engines/default/engine/ui/Textbox.lua
@@ -0,0 +1,156 @@
+-- TE4 - T-Engine 4
+-- Copyright (C) 2009, 2010 Nicolas Casalini
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see <http://www.gnu.org/licenses/>.
+--
+-- Nicolas Casalini "DarkGod"
+-- darkgod@te4.org
+
+require "engine.class"
+local Base = require "engine.ui.Base"
+local Focusable = require "engine.ui.Focusable"
+
+--- A generic UI textbox
+module(..., package.seeall, class.inherit(Base, Focusable))
+
+function _M:init(t)
+	self.title = assert(t.title, "no textbox title")
+	self.text = t.text or ""
+	self.hide = t.hide
+	self.max_len = t.max_len or 999
+	self.fct = assert(t.fct, "no textbox fct")
+	self.chars = assert(t.chars, "no textbox chars")
+
+	self.tmp = {}
+	for i = 1, #self.text do self.tmp[#self.tmp+1] = self.text:sub(i, i) end
+	self.cursor = #self.tmp + 1
+	self.scroll = 1
+
+	Base.init(self, t)
+end
+
+function _M:generate()
+	local ls, ls_w, ls_h = self:getImage("ui/textbox-left-sel.png")
+	local ms, ms_w, ms_h = self:getImage("ui/textbox-middle-sel.png")
+	local rs, rs_w, rs_h = self:getImage("ui/textbox-right-sel.png")
+	local l, l_w, l_h = self:getImage("ui/textbox-left.png")
+	local m, m_w, m_h = self:getImage("ui/textbox-middle.png")
+	local r, r_w, r_h = self:getImage("ui/textbox-right.png")
+	local c, c_w, c_h = self:getImage("ui/textbox-cursor.png")
+
+	self.h = r_h
+
+	-- Draw UI
+	local title_w = self.font:size(self.title:removeColorCodes())
+	self.w = title_w + self.chars * self.font_mono_w + ls_w + rs_w
+	print("titl", self.title, title_w, self.w)
+	local w, h = self.w, r_h
+	local fw, fh = w - title_w - ls_w - rs_w, self.font_h
+	self.fw, self.fh = fw, fh
+	self.text_x = ls_w + title_w
+	self.text_y = (h - fh) / 2
+	self.max_display = math.floor(fw / self.font_mono_w)
+	local ss = core.display.newSurface(w, h)
+	local s = core.display.newSurface(w, h)
+	self.text_surf = core.display.newSurface(fw, fh)
+	self.text_tex, self.text_tex_w, self.text_tex_h = s:glTexture()
+	self:updateText()
+
+	ss:merge(ls, title_w, 0)
+	for i = title_w + ls_w, w - rs_w do ss:merge(ms, i, 0) end
+	ss:merge(rs, w - rs_w, 0)
+	ss:drawColorStringBlended(self.font, self.title, 0, (h - fh) / 2, 255, 255, 255, true)
+
+	s:merge(l, title_w, 0)
+	for i = title_w + l_w, w - r_w do s:merge(m, i, 0) end
+	s:merge(r, w - r_w, 0)
+	s:drawColorStringBlended(self.font, self.title, 0, (h - fh) / 2, 255, 255, 255, true)
+
+	local cursor = core.display.newSurface(c_w, fh)
+	for i = 0, fh - 1 do cursor:merge(c, 0, i) end
+	self.cursor_tex, self.cursor_tex_w, self.cursor_tex_h = cursor:glTexture()
+	self.cursor_w, self.cursor_h = c_w, fh
+
+	-- Add UI controls
+	self.mouse:registerZone(title_w + ls_w, 0, fw, h, function(button, x, y, xrel, yrel, bx, by, event)
+		if event == "button" then
+			self.cursor = util.bound(math.floor(bx / self.font_mono_w) + self.scroll, 1, #self.tmp+1)
+			self:updateText()
+		end
+	end)
+	self.key:addBind("ACCEPT", function() self.fct(self.text) end)
+	self.key:addBind("MOVE_LEFT", function() self.cursor = util.bound(self.cursor - 1, 1, #self.tmp+1) self.scroll = util.scroll(self.cursor, self.scroll, self.max_display) self:updateText() end)
+	self.key:addBind("MOVE_RIGHT", function() self.cursor = util.bound(self.cursor + 1, 1, #self.tmp+1) self.scroll = util.scroll(self.cursor, self.scroll, self.max_display) self:updateText() end)
+	self.key:addCommands{
+		_DELETE = function()
+			if self.cursor <= #self.tmp then
+				table.remove(self.tmp, self.cursor)
+				self:updateText()
+			end
+		end,
+		_BACKSPACE = function()
+			if self.cursor > 1 then
+				table.remove(self.tmp, self.cursor - 1)
+				self.cursor = self.cursor - 1
+				self.scroll = util.scroll(self.cursor, self.scroll, self.max_display)
+				self:updateText()
+			end
+		end,
+		_HOME = function()
+			self.cursor = 1
+			self.scroll = util.scroll(self.cursor, self.scroll, self.max_display)
+			self:updateText()
+		end,
+		_END = function()
+			self.cursor = #self.tmp + 1
+			self.scroll = util.scroll(self.cursor, self.scroll, self.max_display)
+			self:updateText()
+		end,
+		__TEXTINPUT = function(c)
+			if #self.tmp < self.max_len then
+				table.insert(self.tmp, self.cursor, c)
+				self.cursor = self.cursor + 1
+				self.scroll = util.scroll(self.cursor, self.scroll, self.max_display)
+				self:updateText()
+			end
+		end,
+	}
+
+	self.tex, self.tex_w, self.tex_h = s:glTexture()
+	self.stex = ss:glTexture()
+end
+
+function _M:updateText()
+	self.text = table.concat(self.tmp)
+	local text = ""
+	for i = self.scroll, self.scroll + self.max_display - 1 do
+		if not self.tmp[i] then break end
+		if not self.hide then text = text .. self.tmp[i]
+		else text = text .. "*" end
+	end
+
+	self.text_surf:erase(0, 0, 0, 0)
+	self.text_surf:drawStringBlended(self.font_mono, text, 0, 0, 255, 255, 255, true)
+	self.text_surf:updateTexture(self.text_tex)
+end
+
+function _M:display(x, y)
+	if self.focused then
+		self.stex:toScreenFull(x, y, self.w, self.h, self.tex_w, self.tex_h)
+	else
+		self.tex:toScreenFull(x, y, self.w, self.h, self.tex_w, self.tex_h)
+	end
+	self.text_tex:toScreenFull(x + self.text_x, y + self.text_y, self.fw, self.fh, self.text_tex_w, self.text_tex_h)
+	self.cursor_tex:toScreenFull(x + self.text_x + (self.cursor-self.scroll) * self.font_mono_w, y + self.text_y, self.cursor_w, self.cursor_h, self.cursor_tex_w, self.cursor_tex_h)
+end
diff --git a/game/engines/default/engine/ui/Textzone.lua b/game/engines/default/engine/ui/Textzone.lua
new file mode 100644
index 0000000000000000000000000000000000000000..74699b58adb185cc6c628bead4b4066cfda491af
--- /dev/null
+++ b/game/engines/default/engine/ui/Textzone.lua
@@ -0,0 +1,99 @@
+-- TE4 - T-Engine 4
+-- Copyright (C) 2009, 2010 Nicolas Casalini
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see <http://www.gnu.org/licenses/>.
+--
+-- Nicolas Casalini "DarkGod"
+-- darkgod@te4.org
+
+require "engine.class"
+local Base = require "engine.ui.Base"
+local Focusable = require "engine.ui.Focusable"
+
+--- A generic UI list
+module(..., package.seeall, class.inherit(Base, Focusable))
+
+function _M:init(t)
+	self.text = assert(t.text, "no textzone text")
+	self.w = assert(t.width, "no list width")
+	self.h = assert(t.height, "no list height")
+	self.scrollbar = t.scrollbar
+
+	Base.init(self, t)
+end
+
+function _M:generate()
+	local list = self.text:splitLines(self.w - 10, self.font)
+	self.scroll = 1
+	self.max = #list
+	self.max_display = math.floor(self.h / self.font_h)
+	self.can_focus = self.scrollbar and (self.max_display < self.max)
+
+	local fw, fh = self.w, self.font_h
+	self.fw, self.fh = fw, fh
+
+	-- Draw the list items
+	self.list = {}
+	local r, g, b = 255, 255, 255
+	for i, l in ipairs(list) do
+		local s = core.display.newSurface(fw, fh)
+		r, g, b = s:drawColorStringBlended(self.font, l, 0, 0, r, g, b, true)
+
+		local item = {}
+		item._tex, item._tex_w, item._tex_h = s:glTexture()
+		self.list[#self.list+1] = item
+	end
+
+	-- Draw the scrollbar
+	if self.scrollbar then
+		local sb, sb_w, sb_h = self:getImage("ui/scrollbar.png")
+		local ssb, ssb_w, ssb_h = self:getImage("ui/scrollbar-sel.png")
+
+		self.scrollbar = { bar = {}, sel = {} }
+		self.scrollbar.sel.w, self.scrollbar.sel.h, self.scrollbar.sel.tex, self.scrollbar.sel.texw, self.scrollbar.sel.texh = ssb_w, ssb_h, ssb:glTexture()
+		local s = core.display.newSurface(sb_w, self.h - fh)
+		s:erase(200,0,0)
+		for i = 0, self.h - fh do s:merge(sb, 0, i) end
+		self.scrollbar.bar.w, self.scrollbar.bar.h, self.scrollbar.bar.tex, self.scrollbar.bar.texw, self.scrollbar.bar.texh = ssb_w, self.h - fh, s:glTexture()
+	end
+
+	-- Add UI controls
+	self.mouse:registerZone(0, 0, self.w, self.h, function(button, x, y, xrel, yrel, bx, by, event)
+		if button == "wheelup" and event == "button" then self.key:triggerVirtual("MOVE_UP")
+		elseif button == "wheeldown" and event == "button" then self.key:triggerVirtual("MOVE_DOWN")
+		end
+	end)
+	self.key:addBinds{
+		MOVE_UP = function() self.scroll = util.bound(self.scroll - 1, 1, self.max - self.max_display + 1) end,
+		MOVE_DOWN = function() self.scroll = util.bound(self.scroll + 1, 1, self.max - self.max_display + 1) end,
+	}
+end
+
+
+function _M:display(x, y)
+	local bx, by = x, y
+	local max = math.min(self.scroll + self.max_display - 1, self.max)
+	for i = self.scroll, max do
+		local item = self.list[i]
+		item._tex:toScreenFull(x, y, self.fw, self.fh, item._tex_w, item._tex_h)
+		y = y + self.fh
+	end
+
+	if self.focused and self.scrollbar then
+		local pos = self.scroll * (self.h - self.fh) / (self.max - self.max_display + 1)
+
+		self.scrollbar.bar.tex:toScreenFull(bx + self.w - self.scrollbar.bar.w, by + self.fh, self.scrollbar.bar.w, self.scrollbar.bar.h, self.scrollbar.bar.texw, self.scrollbar.bar.texh)
+		self.scrollbar.sel.tex:toScreenFull(bx + self.w - self.scrollbar.sel.w, by + self.fh + pos, self.scrollbar.sel.w, self.scrollbar.sel.h, self.scrollbar.sel.texw, self.scrollbar.sel.texh)
+	end
+end
diff --git a/game/engines/default/special/mainmenu/class/TestUI.lua b/game/engines/default/special/mainmenu/class/TestUI.lua
index 419c0a2b92f8b5b55b3cfaebaab784dadad9abdb..0c7d51c88b4ee177492e23179f230f53cb955226 100644
--- a/game/engines/default/special/mainmenu/class/TestUI.lua
+++ b/game/engines/default/special/mainmenu/class/TestUI.lua
@@ -29,6 +29,8 @@ local DownloadDialog = require "engine.dialogs.DownloadDialog"
 
 local ListColumns = require "engine.ui.ListColumns"
 local Button = require "engine.ui.Button"
+local Textzone = require "engine.ui.Textzone"
+local Textbox = require "engine.ui.Textbox"
 local Dialog = require "engine.ui.Dialog"
 
 module(..., package.seeall, class.inherit(engine.Game, engine.interface.GameMusic))
@@ -38,34 +40,78 @@ function _M:init()
 
 	self.refuse_threads = true
 
+	local text = Textzone.new{width=390, height=200, scrollbar=true, text=[[Pplopppo
+zejkfzejkfh #RED#zmkfhzekhfkjfhkjqerhfgq#LAST# jfh qjzfh qzejfh #BLUE#qjzfh
+flkzerjflm qzfj #WHITE#zeklfj qlfjkql ql
+zf ze
+fze fqefqzfjhjqzkef
+fqzef
+
+
+zef qzekjfhqjkzfh
+
+ze fzef zejkhzfekjh ze
+fze
+ fze
+  zefze fze zef
+
+  ze fzef zefz e
+zlfjklhzqfjkqhzefkhqzefjkqh z
+
+
+zef qzekjfhqjkzfh
+
+ze fzef zejkhzfekjh ze
+fze
+ fze
+  zefze fze zef
+
+  ze fzef zefz e
+zlfjklhzqfjkqhzefkhqzefjkqh z
+
+
+zef qzekjfhqjkzfh
+
+ze fzef zejkhzfekjh ze
+fze
+ fze
+  zefze fze zef
+
+  ze fzef zefz e
+zlfjklhzqfjkqhzefkhqzefjkqh z
+]]}
+	local box = Textbox.new{title="Name: ", text="", chars=10, max_len=30, fct=function(text) print("got", text) end}
 	local b1 = Button.new{text="Ok", fct=function() print"OK" end}
 	local b2 = Button.new{text="Cancel", fct=function() print"KO" end}
-	local list = ListColumns.new{width=200, height=200, columns={
-		{name="Name", width=150}, {name="Encumber", width=50},
+	local list = ListColumns.new{width=390, height=200, sortable=true, scrollbar=true, columns={
+		{name="Name", width=90, display_prop="name", sort="name"},
+		{name="Encumber", width=10, display_prop="encumberance", sort="encumberance"},
 	}, list={
-		{name="toto", encumberance="20"},
-		{name="tutu", encumberance="50"},
-		{name="plopzor #GOLD#Robe of the Archmage#WHITE#!", encumberance="20"},
-		{name="MOUHAHAHAHAH!", encumberance="20"},
-		{name="toto", encumberance="20"},
-		{name="tutu", encumberance="50"},
-		{name="plopzor #GOLD#Robe of the Archmage#WHITE#!", encumberance="20"},
-		{name="MOUHAHAHAHAH!", encumberance="20"},
-		{name="toto", encumberance="20"},
-		{name="tutu", encumberance="50"},
-		{name="plopzor #GOLD#Robe of the Archmage#WHITE#!", encumberance="20"},
-		{name="MOUHAHAHAHAH!", encumberance="20"},
-		{name="toto", encumberance="20"},
-		{name="tutu", encumberance="50"},
-		{name="plopzor #GOLD#Robe of the Archmage#WHITE#!", encumberance="20"},
-		{name="MOUHAHAHAHAH!", encumberance="20"},
+		{name="toto", encumberance=20},
+		{name="tutu", encumberance=50},
+		{name="plopzor #GOLD#Robe of the Archmage#WHITE#!", encumberance=20},
+		{name="MOUHAHAHAHAH!", encumberance=20},
+		{name="toto", encumberance=20},
+		{name="tutu", encumberance=50},
+		{name="plopzor #GOLD#Robe of the Archmage#WHITE#!", encumberance=20},
+		{name="MOUHAHAHAHAH!", encumberance=20},
+		{name="toto", encumberance=20},
+		{name="tutu", encumberance=50},
+		{name="plopzor #GOLD#Robe of the Archmage#WHITE#!", encumberance=20},
+		{name="MOUHAHAHAHAH!", encumberance=20},
+		{name="toto", encumberance=20},
+		{name="tutu", encumberance=50},
+		{name="plopzor #GOLD#Robe of the Archmage#WHITE#!", encumberance=20},
+		{name="MOUHAHAHAHAH!", encumberance=20},
 	}, fct=function(item) print(item.name) end}
 
-	local d = Dialog.new("Test UI", 400, 300)
+	local d = Dialog.new("Test UI", 800, 300)
 	d:loadUI{
 		{left=0, top=0, ui=list},
-		{left=10, bottom=10, ui=b1},
-		{right=10, bottom=10, ui=b2},
+		{left=400, top=0, ui=text},
+		{left=5, bottom=10 + b1.h, ui=box},
+		{left=5, bottom=5, ui=b1},
+		{right=5, bottom=5, ui=b2},
 	}
 	self:registerDialog(d)
 end