summary refs log tree commit diff
path: root/src/Widgets.c
diff options
context:
space:
mode:
authorWlodekM <[email protected]>2024-06-16 10:35:45 +0300
committerWlodekM <[email protected]>2024-06-16 10:35:45 +0300
commitabef6da56913f1c55528103e60a50451a39628b1 (patch)
treeb3c8092471ecbb73e568cd0d336efa0e7871ee8d /src/Widgets.c
initial commit
Diffstat (limited to 'src/Widgets.c')
-rw-r--r--src/Widgets.c2821
1 files changed, 2821 insertions, 0 deletions
diff --git a/src/Widgets.c b/src/Widgets.c
new file mode 100644
index 0000000..7f20b95
--- /dev/null
+++ b/src/Widgets.c
@@ -0,0 +1,2821 @@
+#include "Widgets.h"
+#include "Graphics.h"
+#include "Drawer2D.h"
+#include "ExtMath.h"
+#include "Funcs.h"
+#include "Window.h"
+#include "Utils.h"
+#include "Model.h"
+#include "Screens.h"
+#include "Platform.h"
+#include "Server.h"
+#include "Event.h"
+#include "Chat.h"
+#include "Game.h"
+#include "Logger.h"
+#include "Bitmap.h"
+#include "Block.h"
+#include "Input.h"
+
+static void Widget_NullFunc(void* widget) { }
+static int  Widget_Pointer(void* elem, int id, int x, int y) { return false; }
+static void Widget_InputUp(void* elem, int key)   { }
+static int  Widget_InputDown(void* elem, int key) { return false; }
+static void Widget_PointerUp(void* elem, int id, int x, int y) { }
+static int  Widget_PointerMove(void* elem, int id, int x, int y) { return false; }
+static int  Widget_MouseScroll(void* elem, float delta) { return false; }
+
+static void AddWidget(void* screen, void* w) {
+	struct Screen* s = (struct Screen*)screen;
+
+	if (s->numWidgets >= s->maxWidgets) Logger_Abort("Tried to add too many widgets to screen");
+	s->widgets[s->numWidgets++] = (struct Widget*)w;
+}
+
+/*########################################################################################################################*
+*-------------------------------------------------------TextWidget--------------------------------------------------------*
+*#########################################################################################################################*/
+static void TextWidget_Render(void* widget, float delta) {
+	struct TextWidget* w = (struct TextWidget*)widget;
+	if (w->tex.ID) Texture_RenderShaded(&w->tex, w->color);
+}
+
+static void TextWidget_Free(void* widget) {
+	struct TextWidget* w = (struct TextWidget*)widget;
+	Gfx_DeleteTexture(&w->tex.ID);
+}
+
+static void TextWidget_Reposition(void* widget) {
+	struct TextWidget* w = (struct TextWidget*)widget;
+	Widget_CalcPosition(w);
+	w->tex.x = w->x; w->tex.y = w->y;
+}
+
+static void TextWidget_BuildMesh(void* widget, struct VertexTextured** vertices) {
+	struct TextWidget* w = (struct TextWidget*)widget;
+	Gfx_Make2DQuad(&w->tex, w->color, vertices);
+}
+
+static int TextWidget_Render2(void* widget, int offset) {
+	struct TextWidget* w = (struct TextWidget*)widget;
+	if (w->tex.ID) {
+		Gfx_BindTexture(w->tex.ID);
+		Gfx_DrawVb_IndexedTris_Range(4, offset);
+	}
+	return offset + 4;
+}
+
+static int TextWidget_MaxVertices(void* widget) { return TEXTWIDGET_MAX; }
+
+static const struct WidgetVTABLE TextWidget_VTABLE = {
+	TextWidget_Render, TextWidget_Free,  TextWidget_Reposition,
+	Widget_InputDown,  Widget_InputUp,   Widget_MouseScroll,
+	Widget_Pointer,    Widget_PointerUp, Widget_PointerMove,
+	TextWidget_BuildMesh, TextWidget_Render2, TextWidget_MaxVertices
+};
+void TextWidget_Init(struct TextWidget* w) {
+	Widget_Reset(w);
+	w->VTABLE = &TextWidget_VTABLE;
+	w->color  = PACKEDCOL_WHITE;
+}
+
+void TextWidget_Add(void* screen, struct TextWidget* w) {
+	TextWidget_Init(w);
+	AddWidget(screen, w);
+}
+
+void TextWidget_Set(struct TextWidget* w, const cc_string* text, struct FontDesc* font) {
+	struct DrawTextArgs args;
+	Gfx_DeleteTexture(&w->tex.ID);
+	DrawTextArgs_Make(&args, text, font, true);
+	Drawer2D_MakeTextTexture(&w->tex, &args);
+
+	/* Give text widget default height when text is empty */
+	if (!w->tex.height) {
+		w->tex.height = Font_CalcHeight(font, true);
+	}
+
+	w->width = w->tex.width; w->height = w->tex.height;
+	Widget_Layout(w);
+}
+
+void TextWidget_SetConst(struct TextWidget* w, const char* text, struct FontDesc* font) {
+	cc_string str = String_FromReadonly(text);
+	TextWidget_Set(w, &str, font);
+}
+
+
+/*########################################################################################################################*
+*------------------------------------------------------ButtonWidget-------------------------------------------------------*
+*#########################################################################################################################*/
+#define BUTTON_uWIDTH (200.0f / 256.0f)
+/* Only top half of gui.png is used */
+#define Button_UV(u1,v1, u2,v2) Tex_UV(u1/256.0f,v1/128.0f, u2/256.0f,v2/128.0f)
+
+static struct Texture btnShadowTex   = { 0, Tex_Rect(0,0, 0,0), Button_UV(0,66, 200,86)  };
+static struct Texture btnSelectedTex = { 0, Tex_Rect(0,0, 0,0), Button_UV(0,86, 200,106) };
+static struct Texture btnDisabledTex = { 0, Tex_Rect(0,0, 0,0), Button_UV(0,46, 200,66)  };
+
+static void ButtonWidget_Free(void* widget) {
+	struct ButtonWidget* w = (struct ButtonWidget*)widget;
+	Gfx_DeleteTexture(&w->tex.ID);
+}
+
+static void ButtonWidget_Reposition(void* widget) {
+	struct ButtonWidget* w = (struct ButtonWidget*)widget;
+	w->width  = max(w->tex.width,  w->minWidth);
+	w->height = max(w->tex.height, w->minHeight);
+
+	Widget_CalcPosition(w);
+	w->tex.x = w->x + (w->width  / 2 - w->tex.width  / 2);
+	w->tex.y = w->y + (w->height / 2 - w->tex.height / 2);
+}
+
+static void ButtonWidget_Render(void* widget, float delta) {
+	PackedCol normColor     = PackedCol_Make(224, 224, 224, 255);
+	PackedCol activeColor   = PackedCol_Make(255, 255, 160, 255);
+	PackedCol disabledColor = PackedCol_Make(160, 160, 160, 255);
+	PackedCol color;
+
+	struct ButtonWidget* w = (struct ButtonWidget*)widget;
+	struct Texture back;	
+	float scale;
+		
+	back = w->active ? btnSelectedTex : btnShadowTex;
+	if (w->flags & WIDGET_FLAG_DISABLED) back = btnDisabledTex;
+
+	back.ID = Gui.ClassicTexture ? Gui.GuiClassicTex : Gui.GuiTex;
+	back.x = w->x; back.width  = w->width;
+	back.y = w->y; back.height = w->height;
+
+	/* TODO: Does this 400 need to take DPI into account */
+	if (w->width >= 400) {
+		/* Button can be drawn normally */
+		Texture_Render(&back);
+	} else {
+		/* Split button down the middle */
+		scale = (w->width / 400.0f) / (2 * DisplayInfo.ScaleX);
+		Gfx_BindTexture(back.ID); /* avoid bind twice */
+
+		back.width = (w->width / 2);
+		back.uv.u1 = 0.0f; back.uv.u2 = BUTTON_uWIDTH * scale;
+		Gfx_Draw2DTexture(&back, w->color);
+
+		back.x += (w->width / 2);
+		back.uv.u1 = BUTTON_uWIDTH * (1.0f - scale); back.uv.u2 = BUTTON_uWIDTH;
+		Gfx_Draw2DTexture(&back, w->color);
+	}
+
+	if (!w->tex.ID) return;
+	color = (w->flags & WIDGET_FLAG_DISABLED) ? disabledColor 
+											: (w->active ? activeColor : normColor);
+	Texture_RenderShaded(&w->tex, color);
+}
+
+static void ButtonWidget_BuildMesh(void* widget, struct VertexTextured** vertices) {
+	PackedCol normColor     = PackedCol_Make(224, 224, 224, 255);
+	PackedCol activeColor   = PackedCol_Make(255, 255, 160, 255);
+	PackedCol disabledColor = PackedCol_Make(160, 160, 160, 255);
+	PackedCol color;
+
+	struct ButtonWidget* w = (struct ButtonWidget*)widget;
+	struct Texture back;	
+	float scale;
+		
+	back = w->active ? btnSelectedTex : btnShadowTex;
+	if (w->flags & WIDGET_FLAG_DISABLED) back = btnDisabledTex;
+
+	back.x = w->x; back.width  = w->width;
+	back.y = w->y; back.height = w->height;
+
+	/* TODO: Does this 400 need to take DPI into account */
+	if (w->width >= 400) {
+		/* Button can be drawn normally */
+		Gfx_Make2DQuad(&back, w->color, vertices);
+		*vertices += 4; /* always use up 8 vertices for body */
+	} else {
+		/* Split button down the middle */
+		scale = (w->width / 400.0f) / (2 * DisplayInfo.ScaleX);
+
+		back.width = (w->width / 2);
+		back.uv.u1 = 0.0f; back.uv.u2 = BUTTON_uWIDTH * scale;
+		Gfx_Make2DQuad(&back, w->color, vertices);
+
+		back.x += (w->width / 2);
+		back.uv.u1 = BUTTON_uWIDTH * (1.0f - scale); back.uv.u2 = BUTTON_uWIDTH;
+		Gfx_Make2DQuad(&back, w->color, vertices);
+	}
+
+	color = (w->flags & WIDGET_FLAG_DISABLED) ? disabledColor 
+											: (w->active ? activeColor : normColor);
+	Gfx_Make2DQuad(&w->tex, color, vertices);
+}
+
+static int ButtonWidget_Render2(void* widget, int offset) {
+	struct ButtonWidget* w = (struct ButtonWidget*)widget;	
+	Gfx_BindTexture(Gui.ClassicTexture ? Gui.GuiClassicTex : Gui.GuiTex);
+	/* TODO: Does this 400 need to take DPI into account */
+	Gfx_DrawVb_IndexedTris_Range(w->width >= 400 ? 4 : 8, offset);
+
+	if (w->tex.ID) {
+		Gfx_BindTexture(w->tex.ID);
+		Gfx_DrawVb_IndexedTris_Range(4, offset + 8);
+	}
+	return offset + 12;
+}
+
+static int ButtonWidget_MaxVertices(void* widget) { return BUTTONWIDGET_MAX; }
+
+static const struct WidgetVTABLE ButtonWidget_VTABLE = {
+	ButtonWidget_Render, ButtonWidget_Free, ButtonWidget_Reposition,
+	Widget_InputDown,    Widget_InputUp,    Widget_MouseScroll,
+	Widget_Pointer,      Widget_PointerUp,  Widget_PointerMove,
+	ButtonWidget_BuildMesh, ButtonWidget_Render2, ButtonWidget_MaxVertices
+};
+
+void ButtonWidget_Init(struct ButtonWidget* w, int minWidth, Widget_LeftClick onClick) {
+	Widget_Reset(w);
+	w->VTABLE    = &ButtonWidget_VTABLE;
+	w->color     = PACKEDCOL_WHITE;
+	w->optName   = NULL;
+	w->flags     = WIDGET_FLAG_SELECTABLE;
+	w->minWidth  = Display_ScaleX(minWidth);
+	w->minHeight = Display_ScaleY(40);
+	w->MenuClick = onClick;
+}
+
+void ButtonWidget_Add(void* screen, struct ButtonWidget* w, int minWidth, Widget_LeftClick onClick) {
+	ButtonWidget_Init(w, minWidth, onClick);
+	AddWidget(screen, w);
+}
+
+void ButtonWidget_Set(struct ButtonWidget* w, const cc_string* text, struct FontDesc* font) {
+	struct DrawTextArgs args;
+	Gfx_DeleteTexture(&w->tex.ID);
+	DrawTextArgs_Make(&args, text, font, true);
+	Drawer2D_MakeTextTexture(&w->tex, &args);
+
+	/* Give button default height when text is empty */
+	if (!w->tex.height) {
+		w->tex.height = Font_CalcHeight(font, true);
+	}
+	Widget_Layout(w);
+}
+
+void ButtonWidget_SetConst(struct ButtonWidget* w, const char* text, struct FontDesc* font) {
+	cc_string str = String_FromReadonly(text);
+	ButtonWidget_Set(w, &str, font);
+}
+
+
+/*########################################################################################################################*
+*-----------------------------------------------------ScrollbarWidget-----------------------------------------------------*
+*#########################################################################################################################*/
+#define SCROLL_BACK_COL  PackedCol_Make( 10,  10,  10, 220)
+#define SCROLL_BAR_COL   PackedCol_Make(100, 100, 100, 220)
+#define SCROLL_HOVER_COL PackedCol_Make(122, 122, 122, 220)
+
+static void ScrollbarWidget_ClampTopRow(struct ScrollbarWidget* w) {
+	int maxTop = w->rowsTotal - w->rowsVisible;
+	if (w->topRow >= maxTop) w->topRow = maxTop;
+	if (w->topRow < 0) w->topRow = 0;
+}
+
+static float ScrollbarWidget_GetScale(struct ScrollbarWidget* w) {
+	float rows = (float)w->rowsTotal;
+	return (w->height - w->borderY * 2) / rows;
+}
+
+static void ScrollbarWidget_GetScrollbarCoords(struct ScrollbarWidget* w, int* y, int* height) {
+	float scale = ScrollbarWidget_GetScale(w);
+	*y = Math_Ceil(w->topRow * scale) + w->borderY;
+	*height = Math_Ceil(w->rowsVisible * scale);
+	*height = min(*y + *height, w->height - w->borderY) - *y;
+}
+
+static void ScrollbarWidget_Render(void* widget, float delta) {
+	struct ScrollbarWidget* w = (struct ScrollbarWidget*)widget;
+	int x, y, width, height;
+	PackedCol barCol;
+	cc_bool hovered;
+
+	x = w->x; width = w->width;
+	Gfx_Draw2DFlat(x, w->y, width, w->height, SCROLL_BACK_COL);
+
+	ScrollbarWidget_GetScrollbarCoords(w, &y, &height);
+	x += w->borderX; y += w->y;
+	width -= w->borderX * 2; 
+
+	hovered = Gui_ContainsPointers(x, y, width, height);
+	barCol  = hovered ? SCROLL_HOVER_COL : SCROLL_BAR_COL;
+	Gfx_Draw2DFlat(x, y, width, height, barCol);
+
+	if (height < 20) return;
+	x += w->nubsWidth; y += (height / 2);
+	width -= w->nubsWidth * 2;
+
+	Gfx_Draw2DFlat(x, y + w->offsets[0], width, w->borderY, SCROLL_BACK_COL);
+	Gfx_Draw2DFlat(x, y + w->offsets[1], width, w->borderY, SCROLL_BACK_COL);
+	Gfx_Draw2DFlat(x, y + w->offsets[2], width, w->borderY, SCROLL_BACK_COL);
+}
+
+static int ScrollbarWidget_PointerDown(void* widget, int id, int x, int y) {
+	struct ScrollbarWidget* w = (struct ScrollbarWidget*)widget;
+	int posY, height;
+
+	if (w->draggingId == id) return TOUCH_TYPE_GUI;
+	if (x < w->x || x >= w->x + w->width + w->padding) return false;
+	/* only intercept pointer that's dragging scrollbar */
+	if (w->draggingId) return false;
+
+	y -= w->y;
+	ScrollbarWidget_GetScrollbarCoords(w, &posY, &height);
+
+	if (y < posY) {
+		w->topRow -= w->rowsVisible;
+	} else if (y >= posY + height) {
+		w->topRow += w->rowsVisible;
+	} else {
+		w->draggingId = id;
+		w->dragOffset = y - posY;
+	}
+	ScrollbarWidget_ClampTopRow(w);
+	return TOUCH_TYPE_GUI;
+}
+
+static void ScrollbarWidget_PointerUp(void* widget, int id, int x, int y) {
+	struct ScrollbarWidget* w = (struct ScrollbarWidget*)widget;
+	if (w->draggingId != id) return;
+	w->draggingId = 0;
+	w->dragOffset = 0;
+}
+
+static int ScrollbarWidget_MouseScroll(void* widget, float delta) {
+	struct ScrollbarWidget* w = (struct ScrollbarWidget*)widget;
+	int steps = Utils_AccumulateWheelDelta(&w->scrollingAcc, delta);
+
+	w->topRow -= steps;
+	ScrollbarWidget_ClampTopRow(w);
+	return true;
+}
+
+static int ScrollbarWidget_PointerMove(void* widget, int id, int x, int y) {
+	struct ScrollbarWidget* w = (struct ScrollbarWidget*)widget;
+	float scale;
+
+	if (w->draggingId == id) {
+		y -= w->y;
+		scale = ScrollbarWidget_GetScale(w);
+		w->topRow = (int)((y - w->dragOffset) / scale);
+		ScrollbarWidget_ClampTopRow(w);
+		return true;
+	}
+	return false;
+}
+
+static const struct WidgetVTABLE ScrollbarWidget_VTABLE = {
+	ScrollbarWidget_Render,      Widget_NullFunc,           Widget_CalcPosition,
+	Widget_InputDown,            Widget_InputUp,            ScrollbarWidget_MouseScroll,
+	ScrollbarWidget_PointerDown, ScrollbarWidget_PointerUp, ScrollbarWidget_PointerMove
+};
+void ScrollbarWidget_Create(struct ScrollbarWidget* w, int width) {
+	Widget_Reset(w);
+	w->VTABLE    = &ScrollbarWidget_VTABLE;
+	w->width     = Display_ScaleX(width);
+	w->borderX   = Display_ScaleX(2);
+	w->borderY   = Display_ScaleY(2);
+	w->nubsWidth = Display_ScaleX(3);
+
+	w->offsets[0] = Display_ScaleY(-1 - 4);
+	w->offsets[1] = Display_ScaleY(-1);
+	w->offsets[2] = Display_ScaleY(-1 + 4);
+
+	w->rowsTotal    = 0;
+	w->rowsVisible  = 0;
+	w->topRow       = 0;
+	w->scrollingAcc = 0.0f;
+	w->draggingId   = 0;
+	w->dragOffset   = 0;
+
+	/* It's easy to accidentally touch a bit to the right of the */
+	/* scrollbar with your finger, so just add some padding */
+	if (!Gui_TouchUI) return;
+	w->padding = Display_ScaleX(15);
+}
+
+
+/*########################################################################################################################*
+*------------------------------------------------------HotbarWidget-------------------------------------------------------*
+*#########################################################################################################################*/
+#define HotbarWidget_TileX(w, idx) (int)(w->x + w->slotXOffset + w->slotWidth * (idx))
+
+static void HotbarWidget_BuildOutlineMesh(struct HotbarWidget* w, struct VertexTextured** vertices) {
+	int x;
+	Gfx_Make2DQuad(&w->backTex, PACKEDCOL_WHITE, vertices);
+
+	x = HotbarWidget_TileX(w, Inventory.SelectedIndex);
+	w->selTex.x = (int)(x - w->selWidth / 2);
+	Gfx_Make2DQuad(&w->selTex, PACKEDCOL_WHITE, vertices);
+}
+
+static void HotbarWidget_BuildEntriesMesh(struct HotbarWidget* w, struct VertexTextured** vertices) {
+	int i, x, y;
+	float scale;
+
+	IsometricDrawer_BeginBatch(*vertices, w->state);
+	scale = w->elemSize / 2.0f;
+
+	for (i = 0; i < INVENTORY_BLOCKS_PER_HOTBAR; i++) {
+		x = HotbarWidget_TileX(w, i);
+		y = w->y + (w->height / 2);
+
+		if (i == HOTBAR_MAX_INDEX && Gui_TouchUI) continue;
+
+		IsometricDrawer_AddBatch(Inventory_Get(i), scale, x, y);
+	}
+	w->verticesCount = IsometricDrawer_EndBatch();
+}
+
+static void HotbarWidget_BuildMesh(void* widget, struct VertexTextured** vertices) {
+	struct HotbarWidget* w = (struct HotbarWidget*)widget;
+	struct VertexTextured* data = *vertices;
+
+	HotbarWidget_BuildOutlineMesh(w, vertices);
+	HotbarWidget_BuildEntriesMesh(w, vertices);
+	*vertices = data + HOTBAR_MAX_VERTICES;
+}
+
+
+static void HotbarWidget_RenderOutline(struct HotbarWidget* w, int offset) {
+	GfxResourceID tex;
+	tex = Gui.ClassicTexture ? Gui.GuiClassicTex : Gui.GuiTex;
+
+	Gfx_BindTexture(tex);
+	Gfx_DrawVb_IndexedTris_Range(8, offset);
+}
+
+static void HotbarWidget_RenderEntries(struct HotbarWidget* w, int offset) {
+	if (w->verticesCount == 0) return;
+	IsometricDrawer_Render(w->verticesCount, offset, w->state);
+}
+
+static int HotbarWidget_Render2(void* widget, int offset) {
+	Gfx_3DS_SetRenderScreen(BOTTOM_SCREEN);
+
+	struct HotbarWidget* w = (struct HotbarWidget*)widget;
+	HotbarWidget_RenderOutline(w, offset    );
+	HotbarWidget_RenderEntries(w, offset + 8);
+
+	if (Gui_TouchUI) {
+		w->ellipsisTex.x = HotbarWidget_TileX(w, HOTBAR_MAX_INDEX) - w->ellipsisTex.width / 2;
+		w->ellipsisTex.y = w->y + (w->height / 2) - w->ellipsisTex.height / 2;
+		Texture_Render(&w->ellipsisTex);
+	}
+
+	Gfx_3DS_SetRenderScreen(TOP_SCREEN);
+	return HOTBAR_MAX_VERTICES;
+}
+
+static int HotbarWidget_MaxVertices(void* w) { return HOTBAR_MAX_VERTICES; }
+
+void HotbarWidget_Update(struct HotbarWidget* w, float delta) {
+	int i;
+	if (!Gui_TouchUI) return;
+
+	for (i = 0; i < HOTBAR_MAX_INDEX; i++) 
+	{
+		if (w->touchId[i] < 0) continue;
+		
+		w->touchTime[i] += delta;
+		if (w->touchTime[i] <= 1.0f) continue;
+		
+		w->touchId[i]   = -1;
+		w->touchTime[i] =  0;
+		Inventory_Set(i, 0);
+	}
+}
+
+static int HotbarWidget_ScrolledIndex(struct HotbarWidget* w, float delta, int index, int dir) {
+	int steps = Utils_AccumulateWheelDelta(&w->scrollAcc, delta);
+	index += (dir * steps) % INVENTORY_BLOCKS_PER_HOTBAR;
+
+	if (index < 0) index += INVENTORY_BLOCKS_PER_HOTBAR;
+	if (index >= INVENTORY_BLOCKS_PER_HOTBAR) {
+		index -= INVENTORY_BLOCKS_PER_HOTBAR;
+	}
+	return index;
+}
+
+static void HotbarWidget_Reposition(void* widget) {
+	struct HotbarWidget* w = (struct HotbarWidget*)widget;
+	float scaleX = w->scale * DisplayInfo.ScaleX;
+	float scaleY = w->scale * DisplayInfo.ScaleY;
+	int y;
+
+	w->width  = (int)(182 * scaleX);
+	w->height = Math_Floor(22.0f * scaleY);
+	Widget_CalcPosition(w);
+
+	w->selWidth    = (float)Math_Ceil(24.0f * scaleX);
+	w->elemSize    = 13.5f * scaleX;
+	w->slotXOffset = 11.1f * scaleX;
+	w->slotWidth   = 20.0f * scaleX;
+
+	Tex_SetRect(w->backTex, w->x,w->y, w->width,w->height);
+	/* Only top half of gui png is used */
+	Tex_SetUV(w->backTex,   0,0, 182/256.0f,22/128.0f);
+
+	y = w->y + (w->height - (int)(23.0f * scaleY));
+	Tex_SetRect(w->selTex, 0,y, (int)w->selWidth,w->height);
+	/* Only top half of gui png is used */
+	Tex_SetUV(w->selTex,   0,22/128.0f, 24/256.0f,44/128.0f);
+}
+
+static int HotbarWidget_MapKey(int key) {
+	int i;
+	for (i = 0; i < INVENTORY_BLOCKS_PER_HOTBAR; i++)
+	{
+		if (InputBind_Claims(BIND_HOTBAR_1 + i, key)) return i;
+	}
+	return -1;
+}
+
+static int HotbarWidget_CycleIndex(int dir) {
+	Inventory.SelectedIndex += dir;
+	if (Inventory.SelectedIndex < 0) 
+		Inventory.SelectedIndex += INVENTORY_BLOCKS_PER_HOTBAR;
+	if (Inventory.SelectedIndex >= INVENTORY_BLOCKS_PER_HOTBAR)
+		Inventory.SelectedIndex -= INVENTORY_BLOCKS_PER_HOTBAR;
+
+	return true;
+}
+
+static int HotbarWidget_KeyDown(void* widget, int key) {
+	struct HotbarWidget* w = (struct HotbarWidget*)widget;
+	int index = HotbarWidget_MapKey(key);
+
+	if (index == -1) {
+		if (InputBind_Claims(BIND_HOTBAR_LEFT, key))
+			return HotbarWidget_CycleIndex(-1);
+		if (InputBind_Claims(BIND_HOTBAR_RIGHT, key))
+			return HotbarWidget_CycleIndex(+1);
+		return false;
+	}
+
+	if (InputBind_IsPressed(BIND_HOTBAR_SWITCH)) {
+		/* Pick from first to ninth row */
+		Inventory_SetHotbarIndex(index);
+		w->altHandled = true;
+	} else {
+		Inventory_SetSelectedIndex(index);
+	}
+	return true;
+}
+
+static void HotbarWidget_InputUp(void* widget, int key) {
+	struct HotbarWidget* w = (struct HotbarWidget*)widget;
+	/* Need to handle these cases:
+	     a) user presses alt then number
+	     b) user presses alt
+	   We only do case b) if case a) did not happen */
+	if (!InputBind_Claims(BIND_HOTBAR_SWITCH, key)) return;
+	if (w->altHandled) { w->altHandled = false; return; } /* handled already */
+
+	/* Don't switch hotbar when alt+tabbing to another window */
+	if (Window_Main.Focused) Inventory_SwitchHotbar();
+}
+
+static int HotbarWidget_PointerDown(void* widget, int id, int x, int y) {
+	struct HotbarWidget* w = (struct HotbarWidget*)widget;
+	int width, height;
+	int i, cellX, cellY;
+
+	if (!Widget_Contains(w, x, y)) return false;
+	width  = (int)w->slotWidth;
+	height = w->height;
+
+	for (i = 0; i < INVENTORY_BLOCKS_PER_HOTBAR; i++) {
+		cellX = (int)(w->x + width * i);
+		cellY = w->y;
+		if (!Gui_Contains(cellX, cellY, width, height, x, y)) continue;
+
+		if (Gui_TouchUI) {
+			if (i == HOTBAR_MAX_INDEX) {
+				InventoryScreen_Show(); return TOUCH_TYPE_GUI;
+			} else {
+				w->touchId[i]   = id;
+				w->touchTime[i] = 0;
+			}
+		}
+
+		Inventory_SetSelectedIndex(i);
+		return TOUCH_TYPE_GUI;
+	}
+	return false;
+}
+
+static void HotbarWidget_PointerUp(void* widget, int id, int x, int y) {
+#ifdef CC_BUILD_TOUCH
+	struct HotbarWidget* w = (struct HotbarWidget*)widget;
+	int i;
+
+	for (i = 0; i < HOTBAR_MAX_INDEX; i++) {
+		if (w->touchId[i] == id) {
+			w->touchId[i]   = -1;
+			w->touchTime[i] = 0;
+		}
+	}
+#endif
+}
+
+static int HotbarWidget_PointerMove(void* widget, int id, int x, int y) {
+#ifdef CC_BUILD_TOUCH
+	struct HotbarWidget* w = (struct HotbarWidget*)widget;
+	int i;
+
+	for (i = 0; i < HOTBAR_MAX_INDEX; i++) 
+	{
+		if (w->touchId[i] == id && !Widget_Contains(w, x, y)) {
+			w->touchId[i]   = -1;
+			w->touchTime[i] = 0;
+			return true;
+		}
+	}
+#endif
+	return false;
+}
+
+static int HotbarWidget_MouseScroll(void* widget, float delta) {
+	struct HotbarWidget* w = (struct HotbarWidget*)widget;
+	int index;
+
+	if (InputBind_IsPressed(BIND_HOTBAR_SWITCH)) {
+		index = Inventory.Offset / INVENTORY_BLOCKS_PER_HOTBAR;
+		index = HotbarWidget_ScrolledIndex(w, delta, index, 1);
+		Inventory_SetHotbarIndex(index);
+		w->altHandled = true;
+	} else {
+		index = HotbarWidget_ScrolledIndex(w, delta, Inventory.SelectedIndex, -1);
+		Inventory_SetSelectedIndex(index);
+	}
+	return true;
+}
+
+static void HotbarWidget_Free(void* widget) {
+	struct HotbarWidget* w = (struct HotbarWidget*)widget;
+	if (!Gui_TouchUI) return;
+
+	Gfx_DeleteTexture(&w->ellipsisTex.ID);
+}
+
+static const struct WidgetVTABLE HotbarWidget_VTABLE = {
+	NULL,                     HotbarWidget_Free,      HotbarWidget_Reposition,
+	HotbarWidget_KeyDown,     HotbarWidget_InputUp,   HotbarWidget_MouseScroll,
+	HotbarWidget_PointerDown, HotbarWidget_PointerUp, HotbarWidget_PointerMove,
+	HotbarWidget_BuildMesh,   HotbarWidget_Render2,   HotbarWidget_MaxVertices
+};
+void HotbarWidget_Create(struct HotbarWidget* w) {
+	Widget_Reset(w);
+	w->VTABLE    = &HotbarWidget_VTABLE;
+	w->horAnchor = ANCHOR_CENTRE;
+	w->verAnchor = ANCHOR_MAX;
+	w->scale     = 1;
+	w->verticesCount = 0;
+
+#ifdef CC_BUILD_TOUCH
+	int i;
+	for (i = 0; i < INVENTORY_BLOCKS_PER_HOTBAR - 1; i++) {
+		w->touchId[i] = -1;
+	}
+#endif
+}
+
+void HotbarWidget_SetFont(struct HotbarWidget* w, struct FontDesc* font) {
+	static const cc_string dots = String_FromConst("...");
+	struct DrawTextArgs args;
+	if (!Gui_TouchUI) return;
+
+	DrawTextArgs_Make(&args, &dots, font, true);
+	Drawer2D_MakeTextTexture(&w->ellipsisTex, &args);
+}
+
+
+/*########################################################################################################################*
+*-------------------------------------------------------TableWidget-------------------------------------------------------*
+*#########################################################################################################################*/
+static int Table_X(struct TableWidget* w)      { return w->x - w->paddingL; }
+static int Table_Y(struct TableWidget* w)      { return w->y - w->paddingT; }
+static int Table_Width(struct TableWidget* w)  { return w->width  + w->paddingL + w->paddingR; }
+static int Table_Height(struct TableWidget* w) { return w->height + w->paddingT + w->paddingB; }
+
+static cc_bool TableWidget_GetCoords(struct TableWidget* w, int i, int* cellX, int* cellY) {
+	int x, y;
+	x = i % w->blocksPerRow;
+	y = i / w->blocksPerRow - w->scroll.topRow;
+
+	*cellX = w->x + w->cellSizeX * x;
+	*cellY = w->y + w->cellSizeY * y + 3;
+	return y >= 0 && y < w->rowsVisible;
+}
+
+static void TableWidget_MoveCursorToSelected(struct TableWidget* w) {
+	int x, y, idx;
+	if (w->selectedIndex == -1) return;
+
+	idx = w->selectedIndex;
+	TableWidget_GetCoords(w, idx, &x, &y);
+
+	x += w->cellSizeX / 2; y += w->cellSizeY / 2;
+	Cursor_SetPosition(x, y);
+}
+
+void TableWidget_RecreateTitle(struct TableWidget* w, cc_bool force) {
+	BlockID block;
+	if (!force && w->selectedIndex == w->lastCreatedIndex) return;
+	if (w->blocksCount == 0) return;
+	w->lastCreatedIndex = w->selectedIndex;
+
+	block = w->selectedIndex == -1 ? BLOCK_AIR : w->blocks[w->selectedIndex];
+	w->UpdateTitle(block);
+}
+
+void TableWidget_RecreateBlocks(struct TableWidget* w) {
+	int max = Game_UseCPEBlocks ? BLOCK_MAX_DEFINED : BLOCK_MAX_ORIGINAL;
+	int i, begCount, rowEnd;
+	cc_bool emptyRow;
+	BlockID block;
+	w->blocksCount = 0;
+
+	for (i = 0; i < Array_Elems(Inventory.Map);) {
+		emptyRow = true;
+		begCount = w->blocksCount;
+		rowEnd   = min(i + w->blocksPerRow, Array_Elems(Inventory.Map));
+
+		for (; i < rowEnd; i++) {
+			block = Inventory.Map[i];
+			if (block > max) continue;
+			
+			w->blocks[w->blocksCount++] = block;
+			if (block != BLOCK_AIR) emptyRow = false;
+		}
+
+		if (emptyRow) w->blocksCount = begCount;
+	}
+
+	w->rowsTotal = Math_CeilDiv(w->blocksCount, w->blocksPerRow);
+	Widget_Layout(w);
+}
+
+static void TableWidget_BuildMesh(void* widget, struct VertexTextured** vertices) {
+	struct TableWidget* w = (struct TableWidget*)widget;
+	struct VertexTextured* data = *vertices;
+	int cellSizeX, cellSizeY;
+	int i, x, y;
+
+	cellSizeX = w->cellSizeX;
+	cellSizeY = w->cellSizeY;
+
+	IsometricDrawer_BeginBatch(data, w->state);
+	for (i = 0; i < w->blocksCount; i++) {
+		if (!TableWidget_GetCoords(w, i, &x, &y)) continue;
+
+		/* We want to always draw the selected block on top of others */
+		/* TODO: Need two size arguments, in case X/Y dpi differs */
+		if (i == w->selectedIndex) continue;
+		IsometricDrawer_AddBatch(w->blocks[i],
+			w->normBlockSize, x + cellSizeX / 2, y + cellSizeY / 2);
+	}
+
+	i = w->selectedIndex;
+	if (i != -1) {
+		TableWidget_GetCoords(w, i, &x, &y);
+
+		IsometricDrawer_AddBatch(w->blocks[i],
+			w->selBlockSize, x + cellSizeX / 2, y + cellSizeY / 2);
+	}
+
+	w->verticesCount = IsometricDrawer_EndBatch();
+	*vertices        = data + TABLE_MAX_VERTICES;
+}
+
+static int TableWidget_Render2(void* widget, int offset) {
+	struct TableWidget* w = (struct TableWidget*)widget;
+	int cellSizeX, cellSizeY, size;
+	float off;
+	int x, y;
+
+	/* These were sourced by taking a screenshot of vanilla */
+	/* Then using paint to extract the color components */
+	/* Then using wolfram alpha to solve the glblendfunc equation */
+	PackedCol topBackColor    = PackedCol_Make( 34,  34,  34, 168);
+	PackedCol bottomBackColor = PackedCol_Make( 57,  57, 104, 202);
+	PackedCol topSelColor     = PackedCol_Make(255, 255, 255, 142);
+	PackedCol bottomSelColor  = PackedCol_Make(255, 255, 255, 192);
+
+	Gfx_Draw2DGradient(Table_X(w), Table_Y(w),
+		Table_Width(w), Table_Height(w), topBackColor, bottomBackColor);
+
+	if (w->rowsVisible < w->rowsTotal) {
+		Elem_Render(&w->scroll, 0);
+	}
+
+	cellSizeX = w->cellSizeX;
+	cellSizeY = w->cellSizeY;
+	if (w->selectedIndex != -1 && Gui.ClassicInventory && w->blocks[w->selectedIndex] != BLOCK_AIR) {
+		TableWidget_GetCoords(w, w->selectedIndex, &x, &y);
+
+		/* TODO: Need two size arguments, in case X/Y dpi differs */
+		off  = cellSizeX * 0.1f;
+		size = (int)(cellSizeX + off * 2);
+		Gfx_Draw2DGradient((int)(x - off), (int)(y - off),
+			size, size, topSelColor, bottomSelColor);
+	}
+
+	Gfx_SetVertexFormat(VERTEX_FORMAT_TEXTURED);
+	Gfx_BindDynamicVb(w->vb);
+
+	if (w->verticesCount) {
+		IsometricDrawer_Render(w->verticesCount, offset, w->state);
+	}
+	return offset + TABLE_MAX_VERTICES;
+}
+
+static int TableWidget_MaxVertices(void* w) { return TABLE_MAX_VERTICES; }
+
+static void TableWidget_Free(void* widget) { }
+
+static void TableWidget_Reposition(void* widget) {
+	struct TableWidget* w = (struct TableWidget*)widget;
+	cc_bool classic = Gui.ClassicInventory;
+	float scale = Math_SqrtF(w->scale);
+	int cellSize, blockSize;
+
+	cellSize     = classic ? 48 : 50;
+	w->cellSizeX = Display_ScaleX(cellSize * scale);
+	w->cellSizeY = Display_ScaleY(cellSize * scale);
+
+	blockSize    = classic ? 40 : 50;
+	blockSize    = Display_ScaleX(blockSize * scale);
+	w->normBlockSize = (blockSize             ) * 0.7f / 2.0f;
+	w->selBlockSize  = (blockSize + 25 * scale) * 0.7f / 2.0f;
+	w->rowsVisible   = min(8, w->rowsTotal); /* 8 rows max */
+
+	do {
+		w->width  = w->cellSizeX * w->blocksPerRow;
+		w->height = w->cellSizeY * w->rowsVisible;
+		Widget_CalcPosition(w);
+
+		/* Does the table fit on screen? */
+		if (classic || Table_Y(w) >= 0) break;
+		w->rowsVisible--;
+	} while (w->rowsVisible > 1);
+
+	w->scroll.x = Table_X(w) + Table_Width(w);
+	w->scroll.y = Table_Y(w);
+	w->scroll.height      = Table_Height(w);
+	w->scroll.rowsTotal   = w->rowsTotal;
+	w->scroll.rowsVisible = w->rowsVisible;
+}
+
+static void TableWidget_ScrollRelative(struct TableWidget* w, int delta) {
+	int start = w->selectedIndex, index = start;
+	index += delta;
+	if (index < 0) index -= delta;
+	if (index >= w->blocksCount) index -= delta;
+	w->selectedIndex = index;
+
+	/* adjust scrollbar by number of rows moved up/down */
+	w->scroll.topRow += (index / w->blocksPerRow) - (start / w->blocksPerRow);
+	ScrollbarWidget_ClampTopRow(&w->scroll);
+
+	TableWidget_RecreateTitle(w, false);
+	TableWidget_MoveCursorToSelected(w);
+}
+
+static int TableWidget_PointerDown(void* widget, int id, int x, int y) {
+	struct TableWidget* w = (struct TableWidget*)widget;
+	w->pendingClose = false;
+
+	if (Elem_HandlesPointerDown(&w->scroll, id, x, y)) {
+		return TOUCH_TYPE_GUI;
+	} else if (w->selectedIndex != -1 && w->blocks[w->selectedIndex] != BLOCK_AIR) {
+		Inventory_SetSelectedBlock(w->blocks[w->selectedIndex]);
+		w->pendingClose = true;
+		return TOUCH_TYPE_GUI;
+	} else if (Gui_Contains(Table_X(w), Table_Y(w), Table_Width(w), Table_Height(w), x, y)) {
+		return TOUCH_TYPE_GUI;
+	}
+	return false;
+}
+
+static void TableWidget_PointerUp(void* widget, int id, int x, int y) {
+	struct TableWidget* w = (struct TableWidget*)widget;
+	Elem_OnPointerUp(&w->scroll, id, x, y);
+}
+
+static int TableWidget_MouseScroll(void* widget, float delta) {
+	struct TableWidget* w = (struct TableWidget*)widget;
+	int origTopRow, index;
+
+	cc_bool bounds = Gui_ContainsPointers(Table_X(w), Table_Y(w),
+		Table_Width(w) + w->scroll.width, Table_Height(w));
+	if (!bounds) return false;
+
+	origTopRow = w->scroll.topRow;
+	Elem_HandlesMouseScroll(&w->scroll, delta);
+	if (w->selectedIndex == -1) return true;
+
+	index = w->selectedIndex;
+	index += (w->scroll.topRow - origTopRow) * w->blocksPerRow;
+	if (index >= w->blocksCount) index = -1;
+
+	w->selectedIndex = index;
+	TableWidget_RecreateTitle(w, false);
+	return true;
+}
+
+static int TableWidget_PointerMove(void* widget, int id, int x, int y) {
+	struct TableWidget* w = (struct TableWidget*)widget;
+	int cellSizeX, cellSizeY, maxHeight;
+	int i, cellX, cellY;
+
+	if (Elem_HandlesPointerMove(&w->scroll, id, x, y)) return true;
+	if (w->lastX == x && w->lastY == y) return true;
+	w->lastX = x; w->lastY = y;
+
+	w->selectedIndex = -1;
+	cellSizeX = w->cellSizeX;
+	cellSizeY = w->cellSizeY;
+	maxHeight = cellSizeY * w->rowsVisible;
+
+	if (Gui_Contains(w->x, w->y + 3, w->width, maxHeight - 3 * 2, x, y)) {
+		for (i = 0; i < w->blocksCount; i++) {
+			TableWidget_GetCoords(w, i, &cellX, &cellY);
+
+			if (Gui_Contains(cellX, cellY, cellSizeX, cellSizeY, x, y)) {
+				w->selectedIndex = i;
+				break;
+			}
+		}
+	}
+	TableWidget_RecreateTitle(w, false);
+	return true;
+}
+
+static int TableWidget_KeyDown(void* widget, int key) {
+	struct TableWidget* w = (struct TableWidget*)widget;
+	int delta;
+	if (w->selectedIndex == -1) return false;
+
+	delta = Input_CalcDelta(key, 1, w->blocksPerRow);
+	if (delta) {
+		TableWidget_ScrollRelative(w, delta);
+		return true;
+	}
+	return false;
+}
+
+static int TableWidget_PadAxis(void* widget, int axis, float x, float y) {
+	struct TableWidget* w = (struct TableWidget*)widget;
+	int xSteps, ySteps;
+	if (w->selectedIndex == -1) return false;
+
+	xSteps = Utils_AccumulateWheelDelta(&w->padXAcc, x / 100.0f);
+	if (xSteps) TableWidget_ScrollRelative(w, xSteps > 0 ? 1 : -1);
+
+	ySteps = Utils_AccumulateWheelDelta(&w->padYAcc, y / 100.0f);
+	if (ySteps) TableWidget_ScrollRelative(w, ySteps > 0 ? w->blocksPerRow : -w->blocksPerRow);
+
+	return true;
+}
+
+static const struct WidgetVTABLE TableWidget_VTABLE = {
+	NULL,                    TableWidget_Free,      TableWidget_Reposition,
+	TableWidget_KeyDown,     Widget_InputUp,        TableWidget_MouseScroll,
+	TableWidget_PointerDown, TableWidget_PointerUp, TableWidget_PointerMove,
+	TableWidget_BuildMesh,   TableWidget_Render2,   TableWidget_MaxVertices,
+	TableWidget_PadAxis
+};
+void TableWidget_Add(void* screen, struct TableWidget* w, int sbWidth) {
+	cc_bool classic;
+	Widget_Reset(w);
+	w->VTABLE = &TableWidget_VTABLE;
+	w->lastCreatedIndex = -1000;
+	ScrollbarWidget_Create(&w->scroll, sbWidth);
+	
+	w->horAnchor = ANCHOR_CENTRE;
+	w->verAnchor = ANCHOR_CENTRE;
+	w->lastX = -20; w->lastY = -20;
+	w->scale = 1;
+	w->padXAcc = 0; w->padYAcc = 0;
+
+	if (!w->everCreated) {
+		w->everCreated   = true;
+		w->selectedIndex = -1;
+	}
+	AddWidget(screen, w);
+
+	classic     = Gui.ClassicInventory;
+	w->paddingL = Display_ScaleX(classic ? 20 : 15);
+	w->paddingR = Display_ScaleX(classic ? 28 : 15);
+	w->paddingT = Display_ScaleY(classic ? 46 : 35);
+	w->paddingB = Display_ScaleY(classic ? 14 : 15);
+}
+
+void TableWidget_SetToBlock(struct TableWidget* w, BlockID block) {
+	int i, index = -1;
+	
+	for (i = 0; i < w->blocksCount; i++) 
+	{
+		if (w->blocks[i] == block) index = i;
+	}
+	/* When holding air, inventory should open at middle */
+	if (block == BLOCK_AIR) index = -1;
+
+	TableWidget_SetToIndex(w, index);
+}
+
+void TableWidget_SetToIndex(struct TableWidget* w, int index) {
+	w->selectedIndex = index;
+
+	w->scroll.topRow = w->selectedIndex / w->blocksPerRow;
+	w->scroll.topRow -= (w->rowsVisible - 1);
+	ScrollbarWidget_ClampTopRow(&w->scroll);
+	TableWidget_MoveCursorToSelected(w);
+	TableWidget_RecreateTitle(w, true);
+}
+
+void TableWidget_OnInventoryChanged(struct TableWidget* w) {
+	TableWidget_RecreateBlocks(w);
+	if (w->selectedIndex >= w->blocksCount) {
+		w->selectedIndex = w->blocksCount - 1;
+	}
+	w->lastX = -1; w->lastY = -1;
+
+	w->scroll.topRow = w->selectedIndex / w->blocksPerRow;
+	ScrollbarWidget_ClampTopRow(&w->scroll);
+	TableWidget_RecreateTitle(w, true);
+}
+
+
+/*########################################################################################################################*
+*-------------------------------------------------------InputWidget-------------------------------------------------------*
+*#########################################################################################################################*/
+static void InputWidget_Reset(struct InputWidget* w) {
+	Widget_Reset(w);
+	w->caretPos      = -1;
+	w->caretOffset   = Display_ScaleY(2);
+	w->OnTextChanged = NULL;
+}
+
+static void InputWidget_FormatLine(struct InputWidget* w, int i, cc_string* line) {
+	cc_string src = w->lines[i];
+	if (!w->convertPercents) { String_AppendString(line, &src); return; }
+
+	for (i = 0; i < src.length; i++) {
+		char c = src.buffer[i];
+		if (c == '%' && Drawer2D_ValidColorCodeAt(&src, i + 1)) { c = '&'; }
+		String_Append(line, c);
+	}
+}
+
+static void InputWidget_CalculateLineSizes(struct InputWidget* w) {
+	cc_string line; char lineBuffer[STRING_SIZE];
+	struct DrawTextArgs args;
+	int y;
+
+	for (y = 0; y < INPUTWIDGET_MAX_LINES; y++) {
+		w->lineWidths[y] = 0;
+	}
+	w->lineWidths[0] = w->prefixWidth;
+	DrawTextArgs_MakeEmpty(&args, w->font, true);
+
+	String_InitArray(line, lineBuffer);
+	for (y = 0; y < w->GetMaxLines(); y++) {
+		line.length = 0;
+		InputWidget_FormatLine(w, y, &line);
+
+		args.text = line;
+		w->lineWidths[y] += Drawer2D_TextWidth(&args);
+	}
+}
+
+static char InputWidget_GetLastCol(struct InputWidget* w, int x, int y) {
+	cc_string line; char lineBuffer[STRING_SIZE];
+	char col;
+	String_InitArray(line, lineBuffer);
+
+	for (; y >= 0; y--) {
+		line.length = 0;
+		InputWidget_FormatLine(w, y, &line);
+
+		col = Drawer2D_LastColor(&line, x);
+		if (col) return col;
+		if (y > 0) { x = w->lines[y - 1].length; }
+	}
+	return '\0';
+}
+
+static void InputWidget_UpdateCaret(struct InputWidget* w) {
+	static const cc_string caret = String_FromConst("_");
+	BitmapCol col;
+	cc_string line; char lineBuffer[STRING_SIZE];
+	struct DrawTextArgs args;
+	int maxChars, lineWidth;
+	char colCode;
+
+	if (!w->caretTex.ID) {
+		DrawTextArgs_Make(&args, &caret, w->font, true);
+		Drawer2D_MakeTextTexture(&w->caretTex, &args);
+		w->caretWidth = (cc_uint16)((w->caretTex.width * 3) / 4);
+	}
+	
+	maxChars = w->GetMaxLines() * INPUTWIDGET_LEN;
+	if (w->caretPos >= maxChars) w->caretPos = -1;
+	WordWrap_GetCoords(w->caretPos, w->lines, w->GetMaxLines(), &w->caretX, &w->caretY);
+
+	DrawTextArgs_MakeEmpty(&args, w->font, false);
+	w->caretAccumulator = 0;
+	w->caretTex.width   = w->caretWidth;
+
+	/* Caret is at last character on line */
+	if (w->caretX == INPUTWIDGET_LEN) {
+		lineWidth = w->lineWidths[w->caretY];	
+	} else {
+		String_InitArray(line, lineBuffer);
+		InputWidget_FormatLine(w, w->caretY, &line);
+
+		args.text = String_UNSAFE_Substring(&line, 0, w->caretX);
+		lineWidth = Drawer2D_TextWidth(&args);
+		if (w->caretY == 0) lineWidth += w->prefixWidth;
+
+		if (w->caretX < line.length) {
+			args.text = String_UNSAFE_Substring(&line, w->caretX, 1);
+			args.useShadow = true;
+			w->caretTex.width = Drawer2D_TextWidth(&args);
+		}
+	}
+
+	w->caretTex.x = w->x + w->padding + lineWidth;
+	w->caretTex.y = (w->inputTex.y + w->caretOffset) + w->caretY * w->lineHeight;
+	colCode = InputWidget_GetLastCol(w, w->caretX, w->caretY);
+
+	if (colCode) {
+		col = Drawer2D_GetColor(colCode);
+		/* Component order might be different to BitmapCol */
+		w->caretCol = PackedCol_Make(BitmapCol_R(col), BitmapCol_G(col), 
+									 BitmapCol_B(col), BitmapCol_A(col));
+	} else {
+		w->caretCol = PackedCol_Scale(PACKEDCOL_WHITE, 0.8f);
+	}
+}
+
+static void InputWidget_RenderCaret(struct InputWidget* w, float delta) {
+	float second;
+	if (!w->showCaret) return;
+	w->caretAccumulator += delta;
+
+	second = Math_Mod1(w->caretAccumulator);
+	if (second < 0.5f) Texture_RenderShaded(&w->caretTex, w->caretCol);
+}
+
+static void InputWidget_OnPressedEnter(void* widget) {
+	struct InputWidget* w = (struct InputWidget*)widget;
+	InputWidget_Clear(w);
+	w->height = w->lineHeight;
+	/* TODO get rid of this awful hack.. */
+	Widget_Layout(w);
+}
+
+void InputWidget_Clear(struct InputWidget* w) {
+	int i;
+	w->text.length = 0;
+	
+	for (i = 0; i < Array_Elems(w->lines); i++) {
+		w->lines[i] = String_Empty;
+	}
+
+	w->caretPos = -1;
+	Gfx_DeleteTexture(&w->inputTex.ID);
+	/* TODO: Maybe call w->OnTextChanged */
+}
+
+static cc_bool InputWidget_AllowedChar(void* widget, char c) {
+	return Server.SupportsFullCP437 || (Convert_CP437ToUnicode(c) == c);
+}
+
+static void InputWidget_AppendChar(struct InputWidget* w, char c) {
+	if (w->caretPos == -1) {
+		String_InsertAt(&w->text, w->text.length, c);
+	} else {
+		String_InsertAt(&w->text, w->caretPos, c);
+		w->caretPos++;
+		if (w->caretPos >= w->text.length) { w->caretPos = -1; }
+	}
+}
+
+static cc_bool InputWidget_TryAppendChar(struct InputWidget* w, char c) {
+	int maxChars = w->GetMaxLines() * INPUTWIDGET_LEN;
+	if (w->text.length >= maxChars) return false;
+	if (!w->AllowedChar(w, c)) return false;
+
+	InputWidget_AppendChar(w, c);
+	return true;
+}
+
+static int InputWidget_DoAppendText(struct InputWidget* w, const cc_string* text) {
+	int i, appended = 0;
+	for (i = 0; i < text->length; i++) {
+		if (InputWidget_TryAppendChar(w, text->buffer[i])) appended++;
+	}
+	return appended;
+}
+
+void InputWidget_AppendText(struct InputWidget* w, const cc_string* text) {
+	int appended = InputWidget_DoAppendText(w, text);
+	if (appended) InputWidget_UpdateText(w);
+}
+
+void InputWidget_Append(struct InputWidget* w, char c) {
+	if (!InputWidget_TryAppendChar(w, c)) return;
+	InputWidget_UpdateText(w);
+}
+
+static void InputWidget_DeleteChar(struct InputWidget* w) {
+	if (!w->text.length) return;
+
+	if (w->caretPos == -1) {
+		String_DeleteAt(&w->text, w->text.length - 1);
+	} else if (w->caretPos > 0) {
+		w->caretPos--;
+		String_DeleteAt(&w->text, w->caretPos);
+	}
+}
+
+static void InputWidget_BackspaceKey(struct InputWidget* w) {
+	int i, len;
+
+	if (Input_IsActionPressed()) {
+		if (w->caretPos == -1) { w->caretPos = w->text.length - 1; }
+		len = WordWrap_GetBackLength(&w->text, w->caretPos);
+		if (!len) return;
+
+		w->caretPos -= len;
+		if (w->caretPos < 0) { w->caretPos = 0; }
+
+		for (i = 0; i <= len; i++) {
+			String_DeleteAt(&w->text, w->caretPos);
+		}
+
+		if (w->caretPos >= w->text.length) { w->caretPos = -1; }
+		if (w->caretPos == -1 && w->text.length > 0) {
+			String_InsertAt(&w->text, w->text.length, ' ');
+		} else if (w->caretPos >= 0 && w->text.buffer[w->caretPos] != ' ') {
+			String_InsertAt(&w->text, w->caretPos, ' ');
+		}
+		InputWidget_UpdateText(w);
+	} else if (w->text.length > 0 && w->caretPos != 0) {
+		InputWidget_DeleteChar(w);
+		InputWidget_UpdateText(w);
+	}
+}
+
+static void InputWidget_DeleteKey(struct InputWidget* w) {
+	if (w->text.length > 0 && w->caretPos != -1) {
+		String_DeleteAt(&w->text, w->caretPos);
+		if (w->caretPos >= w->text.length) { w->caretPos = -1; }
+		InputWidget_UpdateText(w);
+	}
+}
+
+static void InputWidget_LeftKey(struct InputWidget* w) {
+	if (Input_IsActionPressed()) {
+		if (w->caretPos == -1) { w->caretPos = w->text.length - 1; }
+		w->caretPos -= WordWrap_GetBackLength(&w->text, w->caretPos);
+		InputWidget_UpdateCaret(w);
+		return;
+	}
+
+	if (w->text.length > 0) {
+		if (w->caretPos == -1) { w->caretPos = w->text.length; }
+		w->caretPos--;
+		if (w->caretPos < 0) { w->caretPos = 0; }
+		InputWidget_UpdateCaret(w);
+	}
+}
+
+static void InputWidget_RightKey(struct InputWidget* w) {
+	if (Input_IsActionPressed()) {
+		w->caretPos += WordWrap_GetForwardLength(&w->text, w->caretPos);
+		if (w->caretPos >= w->text.length) { w->caretPos = -1; }
+		InputWidget_UpdateCaret(w);
+		return;
+	}
+
+	if (w->text.length > 0 && w->caretPos != -1) {
+		w->caretPos++;
+		if (w->caretPos >= w->text.length) { w->caretPos = -1; }
+		InputWidget_UpdateCaret(w);
+	}
+}
+
+static void InputWidget_HomeKey(struct InputWidget* w) {
+	if (!w->text.length) return;
+	w->caretPos = 0;
+	InputWidget_UpdateCaret(w);
+}
+
+static void InputWidget_EndKey(struct InputWidget* w) {
+	w->caretPos = -1;
+	InputWidget_UpdateCaret(w);
+}
+
+static void InputWidget_CopyFromClipboard(struct InputWidget* w) {
+	cc_string text; char textBuffer[2048];
+	String_InitArray(text, textBuffer);
+
+	Clipboard_GetText(&text);
+	InputWidget_AppendText(w, &text);
+}
+
+static cc_bool InputWidget_OtherKey(struct InputWidget* w, int key) {
+	int maxChars = w->GetMaxLines() * INPUTWIDGET_LEN;
+	if (key == INPUT_CLIPBOARD_PASTE && w->text.length < maxChars) {
+		InputWidget_CopyFromClipboard(w);
+		return true;
+	} else if (key == INPUT_CLIPBOARD_COPY) {
+		if (!w->text.length) return true;
+		Clipboard_SetText(&w->text);
+		return true;
+	}
+	return false;
+}
+
+void InputWidget_UpdateText(struct InputWidget* w) {
+	int lines = w->GetMaxLines();
+	if (lines > 1) {
+		WordWrap_Do(&w->text, w->lines, lines, INPUTWIDGET_LEN);
+	} else {
+		w->lines[0] = w->text;
+	}
+
+	Gfx_DeleteTexture(&w->inputTex.ID);
+	InputWidget_CalculateLineSizes(w);
+	w->RemakeTexture(w);
+	InputWidget_UpdateCaret(w);
+	OnscreenKeyboard_SetText(&w->text);
+	if (w->OnTextChanged) w->OnTextChanged(w);
+}
+
+void InputWidget_SetText(struct InputWidget* w, const cc_string* str) {
+	InputWidget_Clear(w);
+	InputWidget_DoAppendText(w, str);
+	InputWidget_UpdateText(w);
+}
+
+static void InputWidget_Free(void* widget) {
+	struct InputWidget* w = (struct InputWidget*)widget;
+	Gfx_DeleteTexture(&w->inputTex.ID);
+	Gfx_DeleteTexture(&w->caretTex.ID);
+}
+
+static void InputWidget_Reposition(void* widget) {
+	struct InputWidget* w = (struct InputWidget*)widget;
+	int oldX = w->x, oldY = w->y;
+	Widget_CalcPosition(w);
+	
+	w->caretTex.x += w->x - oldX; w->caretTex.y += w->y - oldY;
+	w->inputTex.x += w->x - oldX; w->inputTex.y += w->y - oldY;
+}
+
+static int InputWidget_KeyDown(void* widget, int key) {
+	struct InputWidget* w = (struct InputWidget*)widget;
+	if (Input_IsLeftButton(key)) {
+		InputWidget_LeftKey(w);
+	} else if (Input_IsRightButton(key)) {
+		InputWidget_RightKey(w);
+	} else if (key == CCKEY_BACKSPACE) {
+		InputWidget_BackspaceKey(w);
+	} else if (key == CCKEY_DELETE) {
+		InputWidget_DeleteKey(w);
+	} else if (key == CCKEY_HOME) {
+		InputWidget_HomeKey(w);
+	} else if (key == CCKEY_END) {
+		InputWidget_EndKey(w);
+	} else if (!InputWidget_OtherKey(w, key)) {
+		return false;
+	}
+	return true;
+}
+
+static int InputWidget_PointerDown(void* widget, int id, int x, int y) {
+	cc_string line; char lineBuffer[STRING_SIZE];
+	struct InputWidget* w = (struct InputWidget*)widget;
+	struct DrawTextArgs args;
+	int cx, cy, offset = 0;
+	int charX, charWidth, charHeight;
+
+	x -= w->inputTex.x; y -= w->inputTex.y;
+	DrawTextArgs_MakeEmpty(&args, w->font, true);
+	charHeight = w->lineHeight;
+	String_InitArray(line, lineBuffer);
+
+	for (cy = 0; cy < w->GetMaxLines(); cy++) {
+		line.length = 0;
+		InputWidget_FormatLine(w, cy, &line);
+		if (!line.length) continue;
+
+		for (cx = 0; cx < line.length; cx++) {
+			args.text = String_UNSAFE_Substring(&line, 0, cx);
+			charX     = Drawer2D_TextWidth(&args);
+			if (cy == 0) charX += w->prefixWidth;
+
+			args.text = String_UNSAFE_Substring(&line, cx, 1);
+			charWidth = Drawer2D_TextWidth(&args);
+
+			if (Gui_Contains(charX, cy * charHeight, charWidth, charHeight, x, y)) {
+				w->caretPos = offset + cx;
+				InputWidget_UpdateCaret(w);
+				return TOUCH_TYPE_GUI;
+			}
+		}
+		offset += line.length;
+	}
+
+	w->caretPos = -1;
+	InputWidget_UpdateCaret(w);
+	return TOUCH_TYPE_GUI;
+}
+
+
+/*########################################################################################################################*
+*-----------------------------------------------------MenuInputDesc-------------------------------------------------------*
+*#########################################################################################################################*/
+static void    MenuInput_NoDefault(struct MenuInputDesc* d, cc_string* value) { }
+static cc_bool MenuInput_NoProcess(struct MenuInputDesc* d, cc_string* value, int btn) { return false; }
+
+static void Hex_Range(struct MenuInputDesc* d, cc_string* range) {
+	String_AppendConst(range, "&7(#000000 - #FFFFFF)");
+}
+
+static cc_bool Hex_ValidChar(struct MenuInputDesc* d, char c) {
+	return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
+}
+
+static cc_bool Hex_ValidString(struct MenuInputDesc* d, const cc_string* s) {
+	return s->length <= 6;
+}
+
+static cc_bool Hex_ValidValue(struct MenuInputDesc* d, const cc_string* s) {
+	cc_uint8 rgb[3];
+	return PackedCol_TryParseHex(s, rgb);
+}
+
+static void Hex_Default(struct MenuInputDesc* d, cc_string* value) {
+	PackedCol_ToHex(value, d->meta.h.Default);
+}
+
+const struct MenuInputVTABLE HexInput_VTABLE = {
+	Hex_Range, Hex_ValidChar, Hex_ValidString, Hex_ValidValue, 
+	Hex_Default, MenuInput_NoProcess
+};
+
+static void Int_Range(struct MenuInputDesc* d, cc_string* range) {
+	String_Format2(range, "&7(%i - %i)", &d->meta.i.Min, &d->meta.i.Max);
+}
+
+static cc_bool Int_ValidChar(struct MenuInputDesc* d, char c) {
+	return (c >= '0' && c <= '9') || c == '-';
+}
+
+static cc_bool Int_ValidString(struct MenuInputDesc* d, const cc_string* s) {
+	int value;
+	if (s->length == 1 && s->buffer[0] == '-') return true; /* input is just a minus sign */
+	return Convert_ParseInt(s, &value);
+}
+
+static cc_bool Int_ValidValue(struct MenuInputDesc* d, const cc_string* s) {
+	int value, min = d->meta.i.Min, max = d->meta.i.Max;
+	return Convert_ParseInt(s, &value) && min <= value && value <= max;
+}
+
+static void Int_Default(struct MenuInputDesc* d, cc_string* value) {
+	String_AppendInt(value, d->meta.i.Default);
+}
+
+const struct MenuInputVTABLE IntInput_VTABLE = {
+	Int_Range, Int_ValidChar, Int_ValidString, Int_ValidValue, 
+	Int_Default, MenuInput_NoProcess
+};
+
+static void Seed_Range(struct MenuInputDesc* d, cc_string* range) {
+	String_AppendConst(range, "&7(an integer)");
+}
+
+const struct MenuInputVTABLE SeedInput_VTABLE = {
+	Seed_Range, Int_ValidChar, Int_ValidString, Int_ValidValue,
+	MenuInput_NoDefault, MenuInput_NoProcess
+};
+
+static void Float_Range(struct MenuInputDesc* d, cc_string* range) {
+	String_Format2(range, "&7(%f2 - %f2)", &d->meta.f.Min, &d->meta.f.Max);
+}
+
+static cc_bool Float_ValidChar(struct MenuInputDesc* d, char c) {
+	return (c >= '0' && c <= '9') || c == '-' || c == '.' || c == ',';
+}
+
+static cc_bool Float_ValidString(struct MenuInputDesc* d, const cc_string* s) {
+	float value;
+	if (s->length == 1 && Float_ValidChar(d, s->buffer[0])) return true;
+	return Convert_ParseFloat(s, &value);
+}
+
+static cc_bool Float_ValidValue(struct MenuInputDesc* d, const cc_string* s) {
+	float value, min = d->meta.f.Min, max = d->meta.f.Max;
+	return Convert_ParseFloat(s, &value) && min <= value && value <= max;
+}
+
+static void Float_Default(struct MenuInputDesc* d, cc_string* value) {
+	String_AppendFloat(value, d->meta.f.Default, 3);
+}
+
+const struct MenuInputVTABLE FloatInput_VTABLE = {
+	Float_Range, Float_ValidChar, Float_ValidString, Float_ValidValue, 
+	Float_Default, MenuInput_NoProcess
+};
+
+static void Path_Range(struct MenuInputDesc* d, cc_string* range) {
+	String_AppendConst(range, "&7(Enter name)");
+}
+
+static cc_bool Path_ValidChar(struct MenuInputDesc* d, char c) {
+	return !(c == '/' || c == '\\' || c == '?' || c == '*' || c == ':'
+		|| c == '<' || c == '>' || c == '|' || c == '"' || c == '.');
+}
+static cc_bool Path_ValidString(struct MenuInputDesc* d, const cc_string* s) { return true; }
+
+const struct MenuInputVTABLE PathInput_VTABLE = {
+	Path_Range, Path_ValidChar, Path_ValidString, Path_ValidString, 
+	MenuInput_NoDefault, MenuInput_NoProcess
+};
+
+static void String_Range(struct MenuInputDesc* d, cc_string* range) {
+	String_AppendConst(range, "&7(Enter text)");
+}
+
+static cc_bool String_ValidChar(struct MenuInputDesc* d, char c) {
+	return c != '&';
+}
+
+static cc_bool String_ValidString(struct MenuInputDesc* d, const cc_string* s) {
+	return s->length <= STRING_SIZE;
+}
+
+const struct MenuInputVTABLE StringInput_VTABLE = {
+	String_Range, String_ValidChar, String_ValidString, String_ValidString, 
+	MenuInput_NoDefault, MenuInput_NoProcess
+};
+
+
+/*########################################################################################################################*
+*-----------------------------------------------------TextInputWidget-----------------------------------------------------*
+*#########################################################################################################################*/
+static void TextInputWidget_Render(void* widget, float delta) {
+	struct InputWidget* w = (struct InputWidget*)widget;
+	Texture_Render(&w->inputTex);
+	InputWidget_RenderCaret(w, delta);
+}
+
+static void TextInputWidget_BuildMesh(void* widget, struct VertexTextured** vertices) {
+	struct InputWidget* w = (struct InputWidget*)widget;
+	Gfx_Make2DQuad(&w->inputTex, PACKEDCOL_WHITE, vertices);
+	Gfx_Make2DQuad(&w->caretTex, w->caretCol,     vertices);
+}
+
+static int TextInputWidget_Render2(void* widget, int offset) {
+	struct InputWidget* w = (struct InputWidget*)widget;
+	Gfx_BindTexture(w->inputTex.ID);
+	Gfx_DrawVb_IndexedTris_Range(4, offset);
+	offset += 4;
+
+	if (w->showCaret && Math_Mod1((float)w->caretAccumulator) < 0.5f) {
+		Gfx_BindTexture(w->caretTex.ID);
+		Gfx_DrawVb_IndexedTris_Range(4, offset);
+	}
+	return offset + 4;
+}
+
+static int TextInputWidget_MaxVertices(void* widget) { return MENUINPUTWIDGET_MAX; }
+
+static void TextInputWidget_RemakeTexture(void* widget) {
+	cc_string range; char rangeBuffer[STRING_SIZE];
+	struct TextInputWidget* w = (struct TextInputWidget*)widget;
+	PackedCol backColor = PackedCol_Make(30, 30, 30, 200);
+	struct MenuInputDesc* desc;
+	struct DrawTextArgs args;
+	struct Texture* tex;
+	int textWidth, lineHeight;
+	int width, height, hintX, y;
+	struct Context2D ctx;
+
+	DrawTextArgs_Make(&args, &w->base.text, w->base.font, false);
+	textWidth   = Drawer2D_TextWidth(&args);
+	lineHeight  = w->base.lineHeight;
+	w->base.caretAccumulator = 0.0;
+
+	String_InitArray(range, rangeBuffer);
+	desc = &w->desc;
+	desc->VTABLE->GetRange(desc, &range);
+
+	width  = max(textWidth,  w->minWidth);  w->base.width  = width;
+	height = max(lineHeight, w->minHeight); w->base.height = height;
+
+	Context2D_Alloc(&ctx, width, height);
+	{
+		/* Centre text vertically */
+		y = 0;
+		if (lineHeight < height) { y = height / 2 - lineHeight / 2; }
+		w->base.caretOffset = 2 + y;
+
+		Context2D_Clear(&ctx, backColor, 0, 0, width, height);
+		Context2D_DrawText(&ctx, &args, w->base.padding, y);
+
+		args.text = range;
+		hintX     = width - Drawer2D_TextWidth(&args);
+		/* Draw hint text right-aligned if it won't overlap input text */
+		if (textWidth + 3 < hintX) {
+			Context2D_DrawText(&ctx, &args, hintX, y);
+		}
+	}
+
+	tex = &w->base.inputTex;
+	Context2D_MakeTexture(tex, &ctx);
+	Context2D_Free(&ctx);
+
+	Widget_Layout(&w->base);
+	tex->x = w->base.x; tex->y = w->base.y;
+}
+
+static cc_bool TextInputWidget_AllowedChar(void* widget, char c) {
+	struct InputWidget* w = (struct InputWidget*)widget;
+	struct MenuInputDesc* desc;
+	int maxChars;
+	cc_bool valid;
+
+	if (c == '&') return false;
+	desc = &((struct TextInputWidget*)w)->desc;
+
+	if (!desc->VTABLE->IsValidChar(desc, c)) return false;
+	maxChars = w->GetMaxLines() * INPUTWIDGET_LEN;
+	if (w->text.length == maxChars) return false;
+
+	/* See if the new string is in valid format */
+	InputWidget_AppendChar(w, c);
+	valid = desc->VTABLE->IsValidString(desc, &w->text);
+	InputWidget_DeleteChar(w);
+	return valid;
+}
+
+static int TextInputWidget_PointerDown(void* widget, int id, int x, int y) {
+	struct TextInputWidget* w = (struct TextInputWidget*)widget;
+	struct OpenKeyboardArgs args;
+
+	OpenKeyboardArgs_Init(&args, &w->base.text, w->onscreenType);
+	args.placeholder = w->onscreenPlaceholder;
+	OnscreenKeyboard_Open(&args);
+
+	w->base.showCaret = true;
+	return InputWidget_PointerDown(widget, id, x, y);
+}
+
+static int TextInputWidget_GetMaxLines(void) { return 1; }
+static const struct WidgetVTABLE TextInputWidget_VTABLE = {
+	TextInputWidget_Render,      InputWidget_Free, InputWidget_Reposition,
+	InputWidget_KeyDown,         Widget_InputUp,   Widget_MouseScroll,
+	TextInputWidget_PointerDown, Widget_PointerUp, Widget_PointerMove,
+	TextInputWidget_BuildMesh,   TextInputWidget_Render2, TextInputWidget_MaxVertices
+};
+void TextInputWidget_Create(struct TextInputWidget* w, int width, const cc_string* text, struct MenuInputDesc* desc) {
+	InputWidget_Reset(&w->base);
+	w->base.VTABLE = &TextInputWidget_VTABLE;
+
+	w->minWidth  = Display_ScaleX(width);
+	w->minHeight = Display_ScaleY(30);
+	w->desc      = *desc;
+
+	w->base.convertPercents = false;
+	w->base.padding         = 3;
+	w->base.showCaret       = !Gui_TouchUI;
+	w->base.flags           = WIDGET_FLAG_SELECTABLE;
+
+	w->base.GetMaxLines    = TextInputWidget_GetMaxLines;
+	w->base.RemakeTexture  = TextInputWidget_RemakeTexture;
+	w->base.OnPressedEnter = InputWidget_OnPressedEnter;
+	w->base.AllowedChar    = TextInputWidget_AllowedChar;
+
+	String_InitArray(w->base.text, w->_textBuffer);
+	String_Copy(&w->base.text, text);
+	w->onscreenPlaceholder = "";
+	w->onscreenType        = KEYBOARD_TYPE_TEXT;
+}
+
+void TextInputWidget_Add(void* screen, struct TextInputWidget* w, int width, const cc_string* text, struct MenuInputDesc* d) {
+	TextInputWidget_Create(w, width, text, d);
+	AddWidget(screen, w);
+}
+
+void TextInputWidget_SetFont(struct TextInputWidget* w, struct FontDesc* font) {
+	w->base.font       = font;
+	w->base.lineHeight = Font_CalcHeight(font, false);
+	InputWidget_UpdateText(&w->base);
+}
+
+
+/*########################################################################################################################*
+*-----------------------------------------------------ChatInputWidget-----------------------------------------------------*
+*#########################################################################################################################*/
+static const cc_string chatInputPrefix = String_FromConst("> ");
+
+static void ChatInputWidget_MakeTexture(struct InputWidget* w, int width, int height) {
+	cc_string line; char lineBuffer[STRING_SIZE + 2];
+	struct DrawTextArgs args;
+	struct Context2D ctx;
+	char lastCol;
+	int i, x, y;
+
+	Context2D_Alloc(&ctx, width, height);
+
+	DrawTextArgs_Make(&args, &chatInputPrefix, w->font, true);
+	Context2D_DrawText(&ctx, &args, 0, 0);
+
+	String_InitArray(line, lineBuffer);
+	for (i = 0, y = 0; i < Array_Elems(w->lines); i++) {
+		if (!w->lines[i].length) break;
+		line.length = 0;
+
+		/* Color code continues in next line */
+		lastCol = InputWidget_GetLastCol(w, 0, i);
+		if (!Drawer2D_IsWhiteColor(lastCol)) {
+			String_Append(&line, '&'); String_Append(&line, lastCol);
+		}
+		/* Convert % to & for color codes */
+		InputWidget_FormatLine(w, i, &line);
+		args.text = line;
+
+		x = i == 0 ? w->prefixWidth : 0;
+		Context2D_DrawText(&ctx, &args, x, y);
+		y += w->lineHeight;
+	}
+
+	Context2D_MakeTexture(&w->inputTex, &ctx);
+	Context2D_Free(&ctx);
+}
+
+static void ChatInputWidget_RemakeTexture(void* widget) {
+	struct InputWidget* w = (struct InputWidget*)widget;
+	int width = 0, height = 0;
+	int i;
+
+	for (i = 0; i < w->GetMaxLines(); i++) {
+		if (!w->lines[i].length) break;
+		height += w->lineHeight;
+		width   = max(width, w->lineWidths[i]);
+	}
+
+	if (!width)  width  = w->prefixWidth;
+	if (!height) height = w->lineHeight;
+	
+	if (w->flags & WIDGET_FLAG_DISABLED) {
+		Gfx_DeleteTexture(&w->inputTex.ID);
+	} else {
+		ChatInputWidget_MakeTexture(w, width, height);
+	}
+	w->caretAccumulator = 0;
+
+	w->width  = width;
+	w->height = height;
+	Widget_Layout(w);
+	w->inputTex.x = w->x + w->padding;
+	w->inputTex.y = w->y;
+}
+
+static void ChatInputWidget_Render(void* widget, float delta) {
+	struct InputWidget* w = (struct InputWidget*)widget;
+	PackedCol backColor   = PackedCol_Make(0, 0, 0, 127);
+	int x = w->x, y = w->y;
+	cc_bool caretAtEnd;
+	int i, width;
+	if (w->flags & WIDGET_FLAG_DISABLED) return;
+
+	for (i = 0; i < INPUTWIDGET_MAX_LINES; i++) {
+		if (i > 0 && !w->lines[i].length) break;
+
+		caretAtEnd = (w->caretY == i) && (w->caretX == INPUTWIDGET_LEN || w->caretPos == -1);
+		width      = w->lineWidths[i] + (caretAtEnd ? w->caretTex.width : 0);
+		/* Cover whole window width to match Minecraft behaviour */
+		width      = max(width, Window_Main.Width - x * 4);
+	
+		Gfx_Draw2DFlat(x, y, width + w->padding * 2, w->lineHeight, backColor);
+		y += w->lineHeight;
+	}
+
+	Texture_Render(&w->inputTex);
+	InputWidget_RenderCaret(w, delta);
+}
+
+static void ChatInputWidget_OnPressedEnter(void* widget) {
+	struct ChatInputWidget* w = (struct ChatInputWidget*)widget;
+	/* Don't want trailing spaces in output message */
+	cc_string text = w->base.text;
+	String_UNSAFE_TrimEnd(&text);
+	if (text.length) { Chat_Send(&text, true); }
+
+	w->origStr.length = 0;
+	w->typingLogPos = Chat_InputLog.count; /* Index of newest entry + 1. */
+
+	Chat_AddOf(&String_Empty, MSG_TYPE_CLIENTSTATUS_2);
+	InputWidget_OnPressedEnter(widget);
+}
+
+static void ChatInputWidget_UpKey(struct InputWidget* w) {
+	struct ChatInputWidget* W = (struct ChatInputWidget*)w;
+	cc_string prevInput;
+	int pos;
+
+	if (Input_IsActionPressed()) {
+		pos = w->caretPos == -1 ? w->text.length : w->caretPos;
+		if (pos < INPUTWIDGET_LEN) return;
+
+		w->caretPos = pos - INPUTWIDGET_LEN;
+		InputWidget_UpdateCaret(w);
+		return;
+	}
+
+	if (W->typingLogPos == Chat_InputLog.count) {
+		String_Copy(&W->origStr, &w->text);
+	}
+
+	if (!Chat_InputLog.count) return;
+	W->typingLogPos--;
+	w->text.length = 0;
+
+	if (W->typingLogPos < 0) W->typingLogPos = 0;
+	prevInput = StringsBuffer_UNSAFE_Get(&Chat_InputLog, W->typingLogPos);
+	String_AppendString(&w->text, &prevInput);
+
+	w->caretPos = -1;
+	InputWidget_UpdateText(w);
+}
+
+static void ChatInputWidget_DownKey(struct InputWidget* w) {
+	struct ChatInputWidget* W = (struct ChatInputWidget*)w;
+	cc_string prevInput;
+
+	if (Input_IsActionPressed()) {
+		if (w->caretPos == -1) return;
+
+		w->caretPos += INPUTWIDGET_LEN;
+		if (w->caretPos >= w->text.length) { w->caretPos = -1; }
+		InputWidget_UpdateCaret(w);
+		return;
+	}
+
+	if (!Chat_InputLog.count) return;
+	W->typingLogPos++;
+	w->text.length = 0;
+
+	if (W->typingLogPos >= Chat_InputLog.count) {
+		W->typingLogPos = Chat_InputLog.count;
+		String_AppendString(&w->text, &W->origStr);
+	} else {
+		prevInput = StringsBuffer_UNSAFE_Get(&Chat_InputLog, W->typingLogPos);
+		String_AppendString(&w->text, &prevInput);
+	}
+
+	w->caretPos = -1;
+	InputWidget_UpdateText(w);
+}
+
+static cc_bool ChatInputWidget_IsNameChar(char c) {
+	return c == '_' || c == '.' || (c >= '0' && c <= '9')
+		|| (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
+}
+
+static void ChatInputWidget_TabKey(struct InputWidget* w) {
+	cc_string str; char strBuffer[STRING_SIZE];
+	EntityID matches[TABLIST_MAX_NAMES];
+	cc_string part, name;
+	int beg, end, len;
+	int i, j, numMatches;
+	char* buffer;
+
+	end = w->caretPos == -1 ? w->text.length - 1 : w->caretPos;
+	beg = end;
+	buffer = w->text.buffer;
+
+	/* e.g. if player typed "hi Nam", backtrack to "N" */
+	while (beg >= 0 && ChatInputWidget_IsNameChar(buffer[beg])) beg--;
+	beg++;
+	if (end < 0 || beg > end) return;
+
+	part = String_UNSAFE_Substring(&w->text, beg, (end + 1) - beg);
+	Chat_AddOf(&String_Empty, MSG_TYPE_CLIENTSTATUS_2);
+	numMatches = 0;
+
+	for (i = 0; i < TABLIST_MAX_NAMES; i++) {
+		if (!TabList.NameOffsets[i]) continue;
+
+		name = TabList_UNSAFE_GetPlayer(i);
+		if (!String_CaselessContains(&name, &part)) continue;
+		matches[numMatches++] = (EntityID)i;
+	}
+
+	if (numMatches == 1) {
+		if (w->caretPos == -1) end++;
+		len = end - beg;
+
+		/* Following on from above example, delete 'N','a','m' */
+		/* Then insert the e.g. matching 'nAME1' player name */
+		for (j = 0; j < len; j++) {
+			String_DeleteAt(&w->text, beg);
+		}
+
+		if (w->caretPos != -1) w->caretPos -= len;
+		name = TabList_UNSAFE_GetPlayer(matches[0]);
+		InputWidget_AppendText(w, &name);
+	} else if (numMatches > 1) {
+		String_InitArray(str, strBuffer);
+		String_Format1(&str, "&e%i matching names: ", &numMatches);
+
+		for (i = 0; i < numMatches; i++) {
+			name = TabList_UNSAFE_GetPlayer(matches[i]);
+			if ((str.length + name.length + 1) > STRING_SIZE) break;
+
+			String_AppendString(&str, &name);
+			String_Append(&str, ' ');
+		}
+		Chat_AddOf(&str, MSG_TYPE_CLIENTSTATUS_2);
+	}
+}
+
+static int ChatInputWidget_KeyDown(void* widget, int key) {
+	struct InputWidget* w = (struct InputWidget*)widget;
+	if (key == CCKEY_TAB) { 
+		ChatInputWidget_TabKey(w);  return true; 
+	} else if (Input_IsUpButton(key)) { 
+		ChatInputWidget_UpKey(w);   return true;
+	} else if (Input_IsDownButton(key)) { 
+		ChatInputWidget_DownKey(w); return true; 
+	}
+	return InputWidget_KeyDown(w, key);
+}
+
+static int ChatInputWidget_GetMaxLines(void) {
+	return !Game_ClassicMode && Server.SupportsPartialMessages ? INPUTWIDGET_MAX_LINES : 1;
+}
+
+static const struct WidgetVTABLE ChatInputWidget_VTABLE = {
+	ChatInputWidget_Render,  InputWidget_Free, InputWidget_Reposition,
+	ChatInputWidget_KeyDown, Widget_InputUp,   Widget_MouseScroll,
+	InputWidget_PointerDown, Widget_PointerUp, Widget_PointerMove
+};
+void ChatInputWidget_Create(struct ChatInputWidget* w) {
+	InputWidget_Reset(&w->base);
+	w->typingLogPos = Chat_InputLog.count; /* Index of newest entry + 1. */
+	w->base.VTABLE  = &ChatInputWidget_VTABLE;
+
+	w->base.convertPercents = !Game_ClassicMode;
+	w->base.showCaret       = true;
+	w->base.padding         = 5;
+	w->base.GetMaxLines     = ChatInputWidget_GetMaxLines;
+	w->base.RemakeTexture   = ChatInputWidget_RemakeTexture;
+	w->base.OnPressedEnter  = ChatInputWidget_OnPressedEnter;
+	w->base.AllowedChar     = InputWidget_AllowedChar;
+
+	String_InitArray(w->base.text, w->_textBuffer);
+	String_InitArray(w->origStr,   w->_origBuffer);
+}	
+
+void ChatInputWidget_SetFont(struct ChatInputWidget* w, struct FontDesc* font) {
+	struct DrawTextArgs args;	
+	DrawTextArgs_Make(&args, &chatInputPrefix, font, true);
+
+	w->base.font        = font;
+	w->base.prefixWidth = Drawer2D_TextWidth(&args);
+	w->base.lineHeight  = Drawer2D_TextHeight(&args);
+	Gfx_DeleteTexture(&w->base.caretTex.ID);
+}	
+
+
+/*########################################################################################################################*
+*-----------------------------------------------------TextGroupWidget-----------------------------------------------------*
+*#########################################################################################################################*/
+void TextGroupWidget_ShiftUp(struct TextGroupWidget* w) {
+	int last, i;
+	Gfx_DeleteTexture(&w->textures[0].ID);
+	last = w->lines - 1;
+
+	for (i = 0; i < last; i++) 
+	{
+		w->textures[i] = w->textures[i + 1];
+	}
+	w->textures[last].ID = 0; /* Gfx_DeleteTexture() called by TextGroupWidget_Redraw otherwise */
+	TextGroupWidget_Redraw(w, last);
+}
+
+void TextGroupWidget_ShiftDown(struct TextGroupWidget* w) {
+	int last, i;
+	last = w->lines - 1;
+	Gfx_DeleteTexture(&w->textures[last].ID);
+
+	for (i = last; i > 0; i--) 
+	{
+		w->textures[i] = w->textures[i - 1];
+	}
+	w->textures[0].ID = 0; /* Gfx_DeleteTexture() called by TextGroupWidget_Redraw otherwise */
+	TextGroupWidget_Redraw(w, 0);
+}
+
+int TextGroupWidget_UsedHeight(struct TextGroupWidget* w) {
+	struct Texture* textures = w->textures;
+	int i, height = 0;
+
+	for (i = 0; i < w->lines; i++) 
+	{
+		if (textures[i].ID) break;
+	}
+	for (; i < w->lines; i++) 
+	{
+		height += textures[i].height;
+	}
+	return height;
+}
+
+static void TextGroupWidget_Reposition(void* widget) {
+	struct TextGroupWidget* w = (struct TextGroupWidget*)widget;
+	struct Texture* textures  = w->textures;
+	int i, y, width = 0, height = 0;
+	
+	/* Work out how big the text group is now */
+	for (i = 0; i < w->lines; i++) 
+	{
+		width = max(width, textures[i].width);
+		height += textures[i].height;
+	}
+
+	w->width = width; w->height = height;
+	Widget_CalcPosition(w);
+
+	for (i = 0, y = w->y; i < w->lines; i++) 
+	{
+		textures[i].x = Gui_CalcPos(w->horAnchor, w->xOffset, textures[i].width, Window_Main.Width);
+		textures[i].y = y;
+		y += textures[i].height;
+	}
+}
+
+struct Portion { short Beg, Len, LineBeg, LineLen; };
+#define TEXTGROUPWIDGET_HTTP_LEN 7 /* length of http:// */
+#define TEXTGROUPWIDGET_URL 0x8000
+#define TEXTGROUPWIDGET_PACKED_LEN 0x7FFF
+
+static int TextGroupWidget_NextUrl(char* chars, int charsLen, int i) {
+	int start, left;
+
+	for (; i < charsLen; i++) 
+	{
+		if (!(chars[i] == 'h' || chars[i] == '&')) continue;
+		left = charsLen - i;
+		if (left < TEXTGROUPWIDGET_HTTP_LEN) return charsLen;
+
+		/* color codes at start of URL */
+		start = i;
+		while (left >= 2 && chars[i] == '&') { left -= 2; i += 2; }
+		if (left < TEXTGROUPWIDGET_HTTP_LEN) continue;
+
+		/* Starts with "http" */
+		if (chars[i] != 'h' || chars[i + 1] != 't' || chars[i + 2] != 't' || chars[i + 3] != 'p') continue;
+		left -= 4; i += 4;
+
+		/* And then with "s://" or "://" */
+		if (chars[i] == 's') { left--; i++; }
+		if (left >= 3 && chars[i] == ':' && chars[i + 1] == '/' && chars[i + 2] == '/') return start;
+	}
+	return charsLen;
+}
+
+static int TextGroupWidget_UrlEnd(char* chars, int charsLen, int* begs, int begsLen, int i) {
+	int start = i, j;
+	int next, left;
+	cc_bool isBeg;
+
+	for (; i < charsLen && chars[i] != ' '; i++) 
+	{
+		/* Is this character the start of a line */
+		isBeg = false;
+		for (j = 0; j < begsLen; j++) {
+			if (i == begs[j]) { isBeg = true; break; }
+		}
+
+		/* Definitely not a multilined URL */
+		if (!isBeg || i == start) continue;
+		if (chars[i] != '>') break;
+
+		/* Does this line start with "> ", making it a multiline */
+		next = i + 1; left = charsLen - next;
+		while (left >= 2 && chars[next] == '&') { left -= 2; next += 2; }
+		if (left == 0 || chars[next] != ' ') break;
+
+		i = next;
+	}
+	return i;
+}
+
+static void TextGroupWidget_Output(struct Portion bit, int lineBeg, int lineEnd, struct Portion** portions) {
+	struct Portion* cur;
+	int overBy, underBy;
+	if (bit.Beg >= lineEnd || !bit.Len) return;
+
+	bit.LineBeg = bit.Beg;
+	bit.LineLen = bit.Len & TEXTGROUPWIDGET_PACKED_LEN;
+
+	/* Adjust this portion to be within this line */
+	if (bit.Beg >= lineBeg) {
+	} else if (bit.Beg + bit.LineLen > lineBeg) {
+		/* Adjust start of portion to be within this line */
+		underBy = lineBeg - bit.Beg;
+		bit.LineBeg += underBy; bit.LineLen -= underBy;
+	} else { return; }
+
+	/* Limit length of portion to be within this line */
+	overBy = (bit.LineBeg + bit.LineLen) - lineEnd;
+	if (overBy > 0) bit.LineLen -= overBy;
+
+	bit.LineBeg -= lineBeg;
+	if (!bit.LineLen) return;
+
+	cur = *portions; *cur++ = bit; *portions = cur;
+}
+
+static int TextGroupWidget_Reduce(struct TextGroupWidget* w, char* chars, int target, struct Portion* portions) {
+	struct Portion* start = portions;	
+	int begs[GUI_MAX_CHATLINES];
+	int ends[GUI_MAX_CHATLINES];
+	struct Portion bit;
+	cc_string line;
+	int nextStart, i, total = 0, end;
+
+	for (i = 0; i < w->lines; i++) 
+	{
+		line = TextGroupWidget_UNSAFE_Get(w, i);
+		begs[i] = -1; ends[i] = -1;
+		if (!line.length) continue;
+
+		begs[i] = total;
+		Mem_Copy(&chars[total], line.buffer, line.length);
+		total += line.length; ends[i] = total;
+	}
+
+	end = 0;
+	for (;;) 
+	{
+		nextStart = TextGroupWidget_NextUrl(chars, total, end);
+
+		/* add normal portion between urls */
+		bit.Beg = end;
+		bit.Len = nextStart - end;
+		TextGroupWidget_Output(bit, begs[target], ends[target], &portions);
+
+		if (nextStart == total) break;
+		end = TextGroupWidget_UrlEnd(chars, total, begs, w->lines, nextStart);
+
+		/* add this url portion */
+		bit.Beg = nextStart;
+		bit.Len = (end - nextStart) | TEXTGROUPWIDGET_URL;
+		TextGroupWidget_Output(bit, begs[target], ends[target], &portions);
+	}
+	return (int)(portions - start);
+}
+
+static void TextGroupWidget_FormatUrl(cc_string* text, const cc_string* url) {
+	char* dst;
+	int i;
+	String_AppendColorless(text, url);
+
+	/* Delete "> " multiline chars from URLs */
+	dst = text->buffer;
+	for (i = text->length - 2; i >= 0; i--) 
+	{
+		if (dst[i] != '>' || dst[i + 1] != ' ') continue;
+
+		String_DeleteAt(text, i + 1);
+		String_DeleteAt(text, i);
+	}
+}
+
+static cc_bool TextGroupWidget_GetUrl(struct TextGroupWidget* w, cc_string* text, int index, int mouseX) {
+	char chars[GUI_MAX_CHATLINES * TEXTGROUPWIDGET_LEN];
+	struct Portion portions[2 * (TEXTGROUPWIDGET_LEN / TEXTGROUPWIDGET_HTTP_LEN)];
+	struct Portion bit;
+	struct DrawTextArgs args = { 0 };
+	cc_string line, url;
+	int portionsCount;
+	int i, x, width;
+
+	mouseX -= w->textures[index].x;
+	args.useShadow = true;
+	line = TextGroupWidget_UNSAFE_Get(w, index);
+
+	if (Game_ClassicMode) return false;
+	portionsCount = TextGroupWidget_Reduce(w, chars, index, portions);
+
+	for (i = 0, x = 0; i < portionsCount; i++) 
+	{
+		bit = portions[i];
+		args.text = String_UNSAFE_Substring(&line, bit.LineBeg, bit.LineLen);
+		args.font = w->font;
+
+		width = Drawer2D_TextWidth(&args);
+		if ((bit.Len & TEXTGROUPWIDGET_URL) && mouseX >= x && mouseX < x + width) {
+			bit.Len &= TEXTGROUPWIDGET_PACKED_LEN;
+			url = String_Init(&chars[bit.Beg], bit.Len, bit.Len);
+
+			TextGroupWidget_FormatUrl(text, &url);
+			return true;
+		}
+		x += width;
+	}
+	return false;
+}
+
+int TextGroupWidget_GetSelected(struct TextGroupWidget* w, cc_string* text, int x, int y) {
+	struct Texture tex;
+	cc_string line;
+	int i;
+
+	for (i = 0; i < w->lines; i++) 
+	{
+		if (!w->textures[i].ID) continue;
+		tex = w->textures[i];
+		if (!Gui_Contains(tex.x, tex.y, tex.width, tex.height, x, y)) continue;
+
+		if (!TextGroupWidget_GetUrl(w, text, i, x)) {
+			line = TextGroupWidget_UNSAFE_Get(w, i);
+			String_AppendString(text, &line);
+		}
+		return i;
+	}
+	return -1;
+}
+
+static cc_bool TextGroupWidget_MightHaveUrls(struct TextGroupWidget* w) {
+	cc_string line;
+	int i;
+
+	for (i = 0; i < w->lines; i++) 
+	{
+		line = TextGroupWidget_UNSAFE_Get(w, i);
+		if (String_IndexOf(&line, '/') >= 0) return true;
+	}
+	return false;
+}
+
+static void TextGroupWidget_DrawAdvanced(struct TextGroupWidget* w, struct Texture* tex, struct DrawTextArgs* args, int index, const cc_string* text) {
+	char chars[GUI_MAX_CHATLINES * TEXTGROUPWIDGET_LEN];
+	struct Portion portions[2 * (TEXTGROUPWIDGET_LEN / TEXTGROUPWIDGET_HTTP_LEN)];
+	struct Portion bit;
+	int width, height;
+	int partWidths[Array_Elems(portions)];
+	struct Context2D ctx;
+	int portionsCount;
+	int i, x, ul;
+
+	width = 0;
+	height = Drawer2D_TextHeight(args);
+	portionsCount = TextGroupWidget_Reduce(w, chars, index, portions);
+	
+	for (i = 0; i < portionsCount; i++) {
+		bit = portions[i];
+		args->text = String_UNSAFE_Substring(text, bit.LineBeg, bit.LineLen);
+
+		partWidths[i] = Drawer2D_TextWidth(args);
+		width += partWidths[i];
+	}
+	
+	Context2D_Alloc(&ctx, width, height);
+	{
+		x = 0;
+		for (i = 0; i < portionsCount; i++) {
+			bit = portions[i];
+			ul  = (bit.Len & TEXTGROUPWIDGET_URL);
+			args->text = String_UNSAFE_Substring(text, bit.LineBeg, bit.LineLen);
+
+			if (ul) args->font->flags |= FONT_FLAGS_UNDERLINE;
+			Context2D_DrawText(&ctx, args, x, 0);
+			if (ul) args->font->flags &= ~FONT_FLAGS_UNDERLINE;
+
+			x += partWidths[i];
+		}
+		Context2D_MakeTexture(tex, &ctx);
+	}
+	Context2D_Free(&ctx);
+}
+
+void TextGroupWidget_RedrawAll(struct TextGroupWidget* w) {
+	int i;
+	for (i = 0; i < w->lines; i++) { TextGroupWidget_Redraw(w, i); }
+}
+
+void TextGroupWidget_Redraw(struct TextGroupWidget* w, int index) {
+	cc_string text;
+	struct DrawTextArgs args;
+	struct Texture tex = { 0 };
+	Gfx_DeleteTexture(&w->textures[index].ID);
+
+	text = TextGroupWidget_UNSAFE_Get(w, index);
+	if (!Drawer2D_IsEmptyText(&text)) {
+		DrawTextArgs_Make(&args, &text, w->font, true);
+
+		if (w->underlineUrls && TextGroupWidget_MightHaveUrls(w)) {
+			TextGroupWidget_DrawAdvanced(w, &tex, &args, index, &text);
+		} else {
+			Drawer2D_MakeTextTexture(&tex, &args);
+		}
+		Drawer2D_ReducePadding_Tex(&tex, w->font->size, 3);
+	} else {
+		tex.height = w->collapsible[index] ? 0 : w->defaultHeight;
+	}
+
+	tex.x = Gui_CalcPos(w->horAnchor, w->xOffset, tex.width, Window_Main.Width);
+	w->textures[index] = tex;
+	Widget_Layout(w);
+}
+
+void TextGroupWidget_RedrawAllWithCol(struct TextGroupWidget* group, char col) {
+	cc_string line;
+	int i, j;
+
+	for (i = 0; i < group->lines; i++) 
+	{
+		line = TextGroupWidget_UNSAFE_Get(group, i);
+		if (!line.length) continue;
+
+		for (j = 0; j < line.length - 1; j++) 
+		{
+			if (line.buffer[j] == '&' && line.buffer[j + 1] == col) {
+				TextGroupWidget_Redraw(group, i);
+				break;
+			}
+		}
+	}
+}
+
+
+void TextGroupWidget_SetFont(struct TextGroupWidget* w, struct FontDesc* font) {
+	int i, height;
+	
+	height = Font_CalcHeight(font, true);
+	Drawer2D_ReducePadding_Height(&height, font->size, 3);
+	w->defaultHeight = height;
+
+	for (i = 0; i < w->lines; i++) 
+	{
+		w->textures[i].height = w->collapsible[i] ? 0 : height;
+	}
+	w->font = font;
+	Widget_Layout(w);
+}
+
+static void TextGroupWidget_Render(void* widget, float delta) {
+	struct TextGroupWidget* w = (struct TextGroupWidget*)widget;
+	struct Texture* textures  = w->textures;
+	int i;
+
+	for (i = 0; i < w->lines; i++) 
+	{
+		if (!textures[i].ID) continue;
+		Texture_Render(&textures[i]);
+	}
+}
+
+static void TextGroupWidget_Free(void* widget) {
+	struct TextGroupWidget* w = (struct TextGroupWidget*)widget;
+	int i;
+
+	for (i = 0; i < w->lines; i++) 
+	{
+		Gfx_DeleteTexture(&w->textures[i].ID);
+	}
+}
+
+static void TextGroupWidget_BuildMesh(void* widget, struct VertexTextured** vertices) {
+	struct TextGroupWidget* w = (struct TextGroupWidget*)widget;
+	int i;
+
+	for (i = 0; i < w->lines; i++)
+	{
+		Gfx_Make2DQuad(&w->textures[i], PACKEDCOL_WHITE, vertices);
+	}
+}
+
+static int TextGroupWidget_Render2(void* widget, int offset) {
+	struct TextGroupWidget* w = (struct TextGroupWidget*)widget;
+	struct Texture* textures  = w->textures;
+	int i;
+
+	for (i = 0; i < w->lines; i++, offset += 4)
+	{
+		if (!textures[i].ID) continue;
+
+		Gfx_BindTexture(textures[i].ID);
+		Gfx_DrawVb_IndexedTris_Range(4, offset);
+	}
+	return offset;
+}
+
+static int TextGroupWidget_MaxVertices(void* widget) { 
+	struct TextGroupWidget* w = (struct TextGroupWidget*)widget;
+	return w->lines * 4;
+}
+
+static const struct WidgetVTABLE TextGroupWidget_VTABLE = {
+	TextGroupWidget_Render, TextGroupWidget_Free, TextGroupWidget_Reposition,
+	Widget_InputDown,       Widget_InputUp,       Widget_MouseScroll,
+	Widget_Pointer,         Widget_PointerUp,     Widget_PointerMove,
+	TextGroupWidget_BuildMesh, TextGroupWidget_Render2, TextGroupWidget_MaxVertices
+};
+void TextGroupWidget_Create(struct TextGroupWidget* w, int lines, struct Texture* textures, TextGroupWidget_Get getLine) {
+	Widget_Reset(w);
+	w->VTABLE   = &TextGroupWidget_VTABLE;
+	w->lines    = lines;
+	w->textures = textures;
+	w->GetLine  = getLine;
+}
+
+
+/*########################################################################################################################*
+*---------------------------------------------------SpecialInputWidget----------------------------------------------------*
+*#########################################################################################################################*/
+static void SpecialInputWidget_UpdateColString(struct SpecialInputWidget* w) {
+	int i;
+	String_InitArray(w->colString, w->_colBuffer);
+
+	for (i = 0; i < DRAWER2D_MAX_COLORS; i++) {
+		if (i >= 'A' && i <= 'F')           continue;
+		if (!BitmapCol_A(Drawer2D.Colors[i])) continue;
+
+		String_Append(&w->colString, '&'); String_Append(&w->colString, (char)i);
+		String_Append(&w->colString, '%'); String_Append(&w->colString, (char)i);
+	}
+}
+
+static cc_bool SpecialInputWidget_IntersectsTitle(struct SpecialInputWidget* w, int x, int y) {
+	int i, width, titleX = 0;
+
+	for (i = 0; i < Array_Elems(w->tabs); i++) {
+		width = w->tabs[i].titleWidth;
+		if (Gui_Contains(titleX, 0, width, w->titleHeight, x, y)) {
+			w->selectedIndex = i;
+			return true;
+		}
+		titleX += width;
+	}
+	return false;
+}
+
+static void SpecialInputWidget_IntersectsBody(struct SpecialInputWidget* w, int x, int y) {
+	struct SpecialInputTab e = w->tabs[w->selectedIndex];
+	cc_string str;
+	int i;
+
+	y -= w->titleHeight;
+	x /= w->elementWidth; y /= w->elementHeight;
+	
+	i = (x + y * e.itemsPerRow) * e.charsPerItem;
+	if (i >= e.contents.length) return;
+
+	/* TODO: need to insert characters that don't affect w->caretPos index, adjust w->caretPos color */
+	str = String_Init(&e.contents.buffer[i], e.charsPerItem, 0);
+
+	/* TODO: Not be so hacky */
+	if (w->selectedIndex == 0) str.length = 2;
+	InputWidget_AppendText(w->target, &str);
+}
+
+static void SpecialInputTab_Init(struct SpecialInputTab* tab, STRING_REF cc_string* title, int itemsPerRow, int charsPerItem, STRING_REF cc_string* contents) {
+	tab->title      = *title;
+	tab->titleWidth = 0;
+	tab->contents   = *contents;
+	tab->itemsPerRow  = itemsPerRow;
+	tab->charsPerItem = charsPerItem;
+}
+
+static void SpecialInputWidget_InitTabs(struct SpecialInputWidget* w) {
+	static cc_string title_cols = String_FromConst("Colours");
+	static cc_string title_math = String_FromConst("Math");
+	static cc_string tab_math   = String_FromConst("\x9F\xAB\xAC\xE0\xE1\xE2\xE3\xE4\xE5\xE6\xE7\xE8\xE9\xEA\xEB\xEC\xED\xEE\xEF\xF0\xF1\xF2\xF3\xF4\xF5\xF6\xF7\xF8\xFB\xFC\xFD");
+	static cc_string title_line = String_FromConst("Line/Box");
+	static cc_string tab_line   = String_FromConst("\xB0\xB1\xB2\xB3\xB4\xB5\xB6\xB7\xB8\xB9\xBA\xBB\xBC\xBD\xBE\xBF\xC0\xC1\xC2\xC3\xC4\xC5\xC6\xC7\xC8\xC9\xCA\xCB\xCC\xCD\xCE\xCF\xD0\xD1\xD2\xD3\xD4\xD5\xD6\xD7\xD8\xD9\xDA\xDB\xDC\xDD\xDE\xDF\xFE");
+	static cc_string title_letters = String_FromConst("Letters");
+	static cc_string tab_letters   = String_FromConst("\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\xA0\xA1\xA2\xA3\xA4\xA5");
+	static cc_string title_other = String_FromConst("Other");
+	static cc_string tab_other   = String_FromConst("\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F\x7F\x9B\x9C\x9D\x9E\xA6\xA7\xA8\xA9\xAA\xAD\xAE\xAF\xF9\xFA");
+
+	SpecialInputWidget_UpdateColString(w);
+	SpecialInputTab_Init(&w->tabs[0], &title_cols,    10, 4, &w->colString);
+	SpecialInputTab_Init(&w->tabs[1], &title_math,    16, 1, &tab_math);
+	SpecialInputTab_Init(&w->tabs[2], &title_line,    17, 1, &tab_line);
+	SpecialInputTab_Init(&w->tabs[3], &title_letters, 17, 1, &tab_letters);
+	SpecialInputTab_Init(&w->tabs[4], &title_other,   16, 1, &tab_other);
+}
+
+#define SPECIAL_TITLE_SPACING 10
+#define SPECIAL_CONTENT_SPACING 5
+static int SpecialInputWidget_MeasureTitles(struct SpecialInputWidget* w) {
+	struct DrawTextArgs args; 
+	int i, width = 0;
+
+	DrawTextArgs_MakeEmpty(&args, w->font, false);
+	for (i = 0; i < Array_Elems(w->tabs); i++) {
+		args.text = w->tabs[i].title;
+
+		w->tabs[i].titleWidth = Drawer2D_TextWidth(&args) + SPECIAL_TITLE_SPACING;
+		width += w->tabs[i].titleWidth;
+	}
+
+	w->titleHeight = Drawer2D_TextHeight(&args);
+	return width;
+}
+
+static void SpecialInputWidget_DrawTitles(struct SpecialInputWidget* w, struct Context2D* ctx) {
+	BitmapCol color_selected = BitmapCol_Make(30, 30, 30, 200);
+	BitmapCol color_inactive = BitmapCol_Make( 0,  0,  0, 127);
+	BitmapCol color;
+	struct DrawTextArgs args;
+	int i, width, x = 0;
+
+	DrawTextArgs_MakeEmpty(&args, w->font, false);
+	for (i = 0; i < Array_Elems(w->tabs); i++) {
+		args.text = w->tabs[i].title;
+		color = i == w->selectedIndex ? color_selected : color_inactive;
+		width = w->tabs[i].titleWidth;
+
+		Context2D_Clear(ctx, color, x, 0, width, w->titleHeight);
+		Context2D_DrawText(ctx, &args, x + SPECIAL_TITLE_SPACING / 2, 0);
+		x += width;
+	}
+}
+
+static int SpecialInputWidget_MeasureContent(struct SpecialInputWidget* w, struct SpecialInputTab* tab) {
+	struct DrawTextArgs args;
+	int textWidth, textHeight;
+	int i, maxWidth = 0;
+
+	DrawTextArgs_MakeEmpty(&args, w->font, false);
+	args.text.length = tab->charsPerItem;
+	textHeight       = Drawer2D_TextHeight(&args);
+
+	for (i = 0; i < tab->contents.length; i += tab->charsPerItem) {
+		args.text.buffer = &tab->contents.buffer[i];
+		textWidth = Drawer2D_TextWidth(&args);
+		maxWidth  = max(maxWidth, textWidth);
+	}
+
+	w->elementWidth  = maxWidth   + SPECIAL_CONTENT_SPACING;
+	w->elementHeight = textHeight + SPECIAL_CONTENT_SPACING;
+	return w->elementWidth  * tab->itemsPerRow;
+}
+
+static int SpecialInputWidget_ContentHeight(struct SpecialInputWidget* w, struct SpecialInputTab* tab) {
+	int rows = Math_CeilDiv(tab->contents.length / tab->charsPerItem, tab->itemsPerRow);
+	return w->elementHeight * rows;
+}
+
+static void SpecialInputWidget_DrawContent(struct SpecialInputWidget* w, struct SpecialInputTab* tab, struct Context2D* ctx, int yOffset) {
+	struct DrawTextArgs args;
+	int i, x, y, item;	
+
+	int wrap = tab->itemsPerRow;
+	DrawTextArgs_MakeEmpty(&args, w->font, false);
+	args.text.length = tab->charsPerItem;
+
+	for (i = 0; i < tab->contents.length; i += tab->charsPerItem) {
+		args.text.buffer = &tab->contents.buffer[i];
+		item = i / tab->charsPerItem;
+
+		x = (item % wrap) * w->elementWidth;
+		y = (item / wrap) * w->elementHeight + yOffset;
+		Context2D_DrawText(ctx, &args, x, y);
+	}
+}
+
+static void SpecialInputWidget_Make(struct SpecialInputWidget* w, struct SpecialInputTab* tab) {
+	BitmapCol color = BitmapCol_Make(30, 30, 30, 200);
+	int titlesWidth, titlesHeight;
+	int contentWidth, contentHeight;
+	struct Context2D ctx;
+	int width, height;
+
+	titlesWidth   = SpecialInputWidget_MeasureTitles(w);
+	titlesHeight  = w->titleHeight;
+	contentWidth  = SpecialInputWidget_MeasureContent(w, tab);
+	contentHeight = SpecialInputWidget_ContentHeight(w, tab);
+
+	width  = max(titlesWidth, contentWidth);
+	height = titlesHeight + contentHeight;
+	Gfx_DeleteTexture(&w->tex.ID);
+
+	Context2D_Alloc(&ctx, width, height);
+	{
+		SpecialInputWidget_DrawTitles(w, &ctx);
+		Context2D_Clear(&ctx, color, 0, titlesHeight, width, contentHeight);
+		SpecialInputWidget_DrawContent(w, tab, &ctx, titlesHeight);
+	}
+	Context2D_MakeTexture(&w->tex, &ctx);
+	Context2D_Free(&ctx);
+}
+
+void SpecialInputWidget_Redraw(struct SpecialInputWidget* w) {
+	SpecialInputWidget_Make(w, &w->tabs[w->selectedIndex]);
+	w->pendingRedraw = false;
+	Widget_Layout(w);
+}
+
+static void SpecialInputWidget_Render(void* widget, float delta) {
+	struct SpecialInputWidget* w = (struct SpecialInputWidget*)widget;
+	Texture_Render(&w->tex);
+}
+
+static void SpecialInputWidget_Free(void* widget) {
+	struct SpecialInputWidget* w = (struct SpecialInputWidget*)widget;
+	Gfx_DeleteTexture(&w->tex.ID);
+}
+
+static void SpecialInputWidget_Reposition(void* widget) {
+	struct SpecialInputWidget* w = (struct SpecialInputWidget*)widget;
+	w->width  = w->tex.width;
+	w->height = w->active ? w->tex.height : 0;
+	Widget_CalcPosition(w);
+	w->tex.x = w->x; w->tex.y = w->y;
+}
+
+static int SpecialInputWidget_PointerDown(void* widget, int id, int x, int y) {
+	struct SpecialInputWidget* w = (struct SpecialInputWidget*)widget;
+	x -= w->x; y -= w->y;
+
+	if (SpecialInputWidget_IntersectsTitle(w, x, y)) {
+		SpecialInputWidget_Redraw(w);
+	} else {
+		SpecialInputWidget_IntersectsBody(w, x, y);
+	}
+	return TOUCH_TYPE_GUI;
+}
+
+void SpecialInputWidget_UpdateCols(struct SpecialInputWidget* w) {
+	SpecialInputWidget_UpdateColString(w);
+	w->tabs[0].contents = w->colString;
+	if (w->selectedIndex != 0) return;
+
+	/* defer updating colours tab until visible */
+	if (!w->active) { w->pendingRedraw = true; return; }
+	SpecialInputWidget_Redraw(w);
+}
+
+void SpecialInputWidget_SetActive(struct SpecialInputWidget* w, cc_bool active) {
+	w->active = active;
+	if (active && w->pendingRedraw) SpecialInputWidget_Redraw(w);
+	Widget_Layout(w);
+}
+
+static const struct WidgetVTABLE SpecialInputWidget_VTABLE = {
+	SpecialInputWidget_Render,      SpecialInputWidget_Free, SpecialInputWidget_Reposition,
+	Widget_InputDown,               Widget_InputUp,          Widget_MouseScroll,
+	SpecialInputWidget_PointerDown, Widget_PointerUp,        Widget_PointerMove
+};
+void SpecialInputWidget_Create(struct SpecialInputWidget* w, struct FontDesc* font, struct InputWidget* target) {
+	Widget_Reset(w);
+	w->VTABLE    = &SpecialInputWidget_VTABLE;
+	w->verAnchor = ANCHOR_MAX;
+	w->font      = font;
+	w->target    = target;
+	SpecialInputWidget_InitTabs(w);
+}
+
+
+/*########################################################################################################################*
+*----------------------------------------------------ThumbstickWidget-----------------------------------------------------*
+*#########################################################################################################################*/
+#ifdef CC_BUILD_TOUCH
+#define DIR_YMAX (1 << 0)
+#define DIR_YMIN (1 << 1)
+#define DIR_XMAX (1 << 2)
+#define DIR_XMIN (1 << 3)
+
+static void ThumbstickWidget_Rotate(void* widget, struct VertexTextured** vertices, int offset) {
+	struct ThumbstickWidget* w = (struct ThumbstickWidget*)widget;
+	struct VertexTextured* ptr;
+	int i;
+
+	ptr = *vertices - 4;
+	for (i = 0; i < 4; i++) {
+		int x = ptr[i].x - w->x;
+		int y = ptr[i].y - w->y;
+		ptr[i].x = -y + w->x + offset;
+		ptr[i].y =  x + w->y;
+	}
+}
+
+static void ThumbstickWidget_BuildGroup(void* widget, struct Texture* tex, struct VertexTextured** vertices) {
+	struct ThumbstickWidget* w = (struct ThumbstickWidget*)widget;
+	float tmp;
+	tex->y = w->y + w->height / 2;
+	Gfx_Make2DQuad(tex, PACKEDCOL_WHITE, vertices);
+
+	tex->y = w->y;
+	tmp    = tex->uv.v1; tex->uv.v1 = tex->uv.v2; tex->uv.v2 = tmp;
+	Gfx_Make2DQuad(tex, PACKEDCOL_WHITE, vertices);
+
+	Gfx_Make2DQuad(tex, PACKEDCOL_WHITE, vertices);
+	ThumbstickWidget_Rotate(widget, vertices, w->width);
+
+	tmp    = tex->uv.v1; tex->uv.v1 = tex->uv.v2; tex->uv.v2 = tmp;
+	Gfx_Make2DQuad(tex, PACKEDCOL_WHITE, vertices);
+	ThumbstickWidget_Rotate(widget, vertices, w->width / 2);
+}
+
+static void ThumbstickWidget_BuildMesh(void* widget, struct VertexTextured** vertices) {
+	struct ThumbstickWidget* w = (struct ThumbstickWidget*)widget;
+	struct Texture tex;
+
+	tex.x     = w->x;
+	tex.width = w->width; tex.height = w->height / 2;
+	tex.uv.u1 = 0.0f;     tex.uv.u2  = 1.0f;
+
+	tex.uv.v1 = 0.0f; tex.uv.v2 = 0.5f;
+	ThumbstickWidget_BuildGroup(widget, &tex, vertices);
+	tex.uv.v1 = 0.5f; tex.uv.v2 = 1.0f;
+	ThumbstickWidget_BuildGroup(widget, &tex, vertices);
+}
+
+static int ThumbstickWidget_CalcDirs(struct ThumbstickWidget* w) {
+	int i, dx, dy, dirs = 0;
+	float angle;
+
+	for (i = 0; i < INPUT_MAX_POINTERS; i++) {
+		if (!(w->active & (1 << i))) continue;
+
+		dx = Pointers[i].x - (w->x + w->width  / 2);
+		dy = Pointers[i].y - (w->y + w->height / 2);
+		angle = Math_Atan2f(dx, dy) * MATH_RAD2DEG;
+
+		/* 4 quadrants diagonally, but slightly expanded for overlap*/
+		if (angle >=   30 && angle <= 150) dirs |= DIR_YMAX;
+		if (angle >=  -60 && angle <=  60) dirs |= DIR_XMAX;
+		if (angle >= -150 && angle <= -30) dirs |= DIR_YMIN;
+		if (angle <  -120 || angle >  120) dirs |= DIR_XMIN;
+	}
+	return dirs;
+}
+
+static int ThumbstickWidget_Render2(void* widget, int offset) {
+	struct ThumbstickWidget* w = (struct ThumbstickWidget*)widget;
+	int i, base, flags = ThumbstickWidget_CalcDirs(w);
+
+	if (Gui.TouchTex) {
+		Gfx_BindTexture(Gui.TouchTex);
+		for (i = 0; i < 4; i++) {
+			base = (flags & (1 << i)) ? 0 : THUMBSTICKWIDGET_PER;
+			Gfx_DrawVb_IndexedTris_Range(4, offset + base + (i * 4));
+		}
+	}
+	return offset + THUMBSTICKWIDGET_MAX;
+}
+
+static void ThumbstickWidget_Reposition(void* widget) {
+	struct ThumbstickWidget* w = (struct ThumbstickWidget*)widget;
+	w->width  = Display_ScaleX(128 * w->scale);
+	w->height = Display_ScaleY(128 * w->scale);
+	Widget_CalcPosition(w);
+}
+
+static const struct WidgetVTABLE ThumbstickWidget_VTABLE = {
+	NULL, Screen_NullFunc, ThumbstickWidget_Reposition,
+	Widget_InputDown,  Widget_InputUp,   Widget_MouseScroll,
+	Widget_Pointer,    Widget_PointerUp, Widget_PointerMove,
+	ThumbstickWidget_BuildMesh, ThumbstickWidget_Render2
+};
+void ThumbstickWidget_Init(struct ThumbstickWidget* w) {
+	Widget_Reset(w);
+	w->VTABLE = &ThumbstickWidget_VTABLE;
+	w->scale  = 1;
+}
+
+void ThumbstickWidget_GetMovement(struct ThumbstickWidget* w, float* xMoving, float* zMoving) {
+	int dirs = ThumbstickWidget_CalcDirs(w);
+	if (dirs & DIR_XMIN) *xMoving -= 1;
+	if (dirs & DIR_XMAX) *xMoving += 1;
+	if (dirs & DIR_YMIN) *zMoving -= 1;
+	if (dirs & DIR_YMAX) *zMoving += 1;
+}
+#endif