#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"
#include "InputHandler.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, struct InputDevice* device)   { }
static int  Widget_InputDown(void* elem, int key, struct InputDevice* device) { 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) Process_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, DRAW_HINT_RECT);
	}
	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, DRAW_HINT_SPRITE);

	if (w->tex.ID) {
		Gfx_BindTexture(w->tex.ID);
		Gfx_DrawVb_IndexedTris_Range(4, offset + 8, DRAW_HINT_SPRITE);
	}
	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, DRAW_HINT_SPRITE);
}

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) {
	struct HotbarWidget* w = (struct HotbarWidget*)widget;
	Gfx_3DS_SetRenderScreen(BOTTOM_SCREEN);

	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, struct InputDevice* device) {
	int i;
	for (i = 0; i < INVENTORY_BLOCKS_PER_HOTBAR; i++)
	{
		if (InputBind_Claims(BIND_HOTBAR_1 + i, key, device)) 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 InputDevice* device) {
	struct HotbarWidget* w = (struct HotbarWidget*)widget;
	int index = HotbarWidget_MapKey(key, device);

	if (index == -1) {
		if (InputBind_Claims(BIND_HOTBAR_LEFT, key, device))
			return HotbarWidget_CycleIndex(-1);
		if (InputBind_Claims(BIND_HOTBAR_RIGHT, key, device))
			return HotbarWidget_CycleIndex(+1);
		return false;
	}

	if (Bind_IsTriggered[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 InputDevice* device) {
	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, device)) 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 (Bind_IsTriggered[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 InputDevice* device) {
	struct TableWidget* w = (struct TableWidget*)widget;
	int deltaX, deltaY;
	if (w->selectedIndex == -1) return false;

	Input_CalcDelta(key, device, &deltaX, &deltaY);
	if (deltaX || deltaY) {
		TableWidget_ScrollRelative(w, deltaX + deltaY * w->blocksPerRow);
		return true;
	}
	return false;
}

static int TableWidget_PadAxis(void* widget, struct PadAxisUpdate* upd) {
	struct TableWidget* w = (struct TableWidget*)widget;
	if (w->selectedIndex == -1) return false;

	if (upd->xSteps) TableWidget_ScrollRelative(w, upd->xSteps > 0 ? 1 : -1);
	if (upd->ySteps) TableWidget_ScrollRelative(w, upd->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;

	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 InputDevice* device) {
	struct InputWidget* w = (struct InputWidget*)widget;
	if (key == device->leftButton) {
		InputWidget_LeftKey(w);
	} else if (key == device->rightButton) {
		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, DRAW_HINT_RECT);
	offset += 4;

	if (w->showCaret && Math_Mod1((float)w->caretAccumulator) < 0.5f) {
		Gfx_BindTexture(w->caretTex.ID);
		Gfx_DrawVb_IndexedTris_Range(4, offset, DRAW_HINT_RECT);
	}
	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;
}

void TextInputWidget_OpenKeyboard(struct TextInputWidget* w, struct InputDevice* device) {
	struct OpenKeyboardArgs args;

	OpenKeyboardArgs_Init(&args, &w->base.text, w->onscreenType);
	args.placeholder = w->onscreenPlaceholder;
	args.device      = device;
	OnscreenKeyboard_Open(&args);
}

static int TextInputWidget_KeyDown(void* widget, int key, struct InputDevice* device) {
	struct TextInputWidget* w  = (struct TextInputWidget*)widget;
	struct MenuInputDesc* desc = &w->desc;

	if (Window_Main.SoftKeyboard && !DisplayInfo.ShowingSoftKeyboard && InputDevice_IsEnter(key, device)) { 
		TextInputWidget_OpenKeyboard(w, device); return true; 
	}
	if (InputWidget_KeyDown(&w->base, key, device)) return true;

	return desc->VTABLE->ProcessInput(desc, &w->base.text, key);
}

static int TextInputWidget_PointerDown(void* widget, int id, int x, int y) {
	struct TextInputWidget* w = (struct TextInputWidget*)widget;

	TextInputWidget_OpenKeyboard(w, NULL);
	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,
	TextInputWidget_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 InputDevice* device) {
	struct InputWidget* w = (struct InputWidget*)widget;
	if (key == CCKEY_TAB) { 
		ChatInputWidget_TabKey(w);  return true; 
	} else if (key == device->upButton) { 
		ChatInputWidget_UpKey(w);   return true;
	} else if (key == device->downButton) { 
		ChatInputWidget_DownKey(w); return true; 
	}
	return InputWidget_KeyDown(w, key, device);
}

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_RemoveColorPrefix(cc_string* text, int i) {
	if (i + 2 > text->length || text->buffer[i] != '&') return;
	if (!Drawer2D_ValidColorCodeAt(text, i + 1)) return;

	String_DeleteAt(text, i + 1); 
	String_DeleteAt(text, i);
}

static void TextGroupWidget_FormatUrl(cc_string* text, const cc_string* url) {
	char* dst;
	int i;
	String_AppendString(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);
		/* Delete leading multiline colour code if present */
		TextGroupWidget_RemoveColorPrefix(text, i);
	}

	/* Delete leading colour code if present */
	TextGroupWidget_RemoveColorPrefix(text, 0);
}

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, DRAW_HINT_RECT);
	}
	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), DRAW_HINT_NONE);
		}
	}
	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
