summary refs log tree commit diff
path: root/src/LWidgets.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/LWidgets.c
initial commit
Diffstat (limited to 'src/LWidgets.c')
-rw-r--r--src/LWidgets.c793
1 files changed, 793 insertions, 0 deletions
diff --git a/src/LWidgets.c b/src/LWidgets.c
new file mode 100644
index 0000000..7a7da60
--- /dev/null
+++ b/src/LWidgets.c
@@ -0,0 +1,793 @@
+#include "LWidgets.h"
+#ifndef CC_BUILD_WEB
+#include "String.h"
+#include "Gui.h"
+#include "Drawer2D.h"
+#include "Launcher.h"
+#include "ExtMath.h"
+#include "Window.h"
+#include "Funcs.h"
+#include "LWeb.h"
+#include "Platform.h"
+#include "LScreens.h"
+#include "Input.h"
+#include "Utils.h"
+#include "LBackend.h"
+
+static int flagXOffset, flagYOffset;
+static int oneX, twoX, fourX;
+static int oneY, twoY, fourY;
+
+void LWidget_CalcOffsets(void) {
+	oneX = Display_ScaleX(1);
+	oneY = Display_ScaleY(1);
+
+	if (oneX < 1) { oneX = 1; }
+	if (oneY < 1) { oneY = 1; }
+
+	twoX = oneX * 2; fourX = oneX * 4;
+	twoY = oneY * 2; fourY = oneY * 4;
+
+	flagXOffset  = Display_ScaleX(2);
+	flagYOffset  = Display_ScaleY(6);
+}
+
+static void LWidget_DrawInsetBorder(struct Context2D* ctx, BitmapCol color, int insetX, int insetY,
+									int x, int y, int width, int height) {
+	Context2D_Clear(ctx, color,
+					x + insetX,         y,
+					width - 2 * insetX, insetY);
+	Context2D_Clear(ctx, color,
+					x + insetX,         y + height - insetY,
+					width - 2 * insetX, insetY);
+	Context2D_Clear(ctx, color,
+					x,                  y + insetY,
+					insetX,             height - 2 * insetY);
+	Context2D_Clear(ctx, color,
+					x + width - insetX, y + insetY,
+					insetX,             height - 2 * insetY);
+}
+
+void LWidget_DrawBorder(struct Context2D* ctx, BitmapCol color, int borderX, int borderY,
+									int x, int y, int width, int height) {
+	Context2D_Clear(ctx, color,
+					x,         y,
+					width, borderY);
+	Context2D_Clear(ctx, color,
+					x,         y + height - borderY,
+					width, borderY);
+	Context2D_Clear(ctx, color,
+					x,                   y,
+					borderX,             height);
+	Context2D_Clear(ctx, color,
+					x + width - borderX, y,
+					borderX,             height);
+}
+
+
+/*########################################################################################################################*
+*------------------------------------------------------ButtonWidget-------------------------------------------------------*
+*#########################################################################################################################*/
+static void LButton_DrawBase(struct Context2D* ctx, int x, int y, int width, int height, cc_bool active) {
+	BitmapCol color = active ? Launcher_Theme.ButtonForeActiveColor 
+							 : Launcher_Theme.ButtonForeColor;
+
+	if (Launcher_Theme.ClassicBackground) {
+		Gradient_Noise(ctx, color, 8,
+						x + oneX,       y + oneY,
+						width - twoX,   height - twoY);
+	} else {
+		Gradient_Vertical(ctx, BitmapColor_Offset(color, 8,8,8), BitmapColor_Offset(color, -8,-8,-8),
+						  x + oneX,     y + oneY,
+						  width - twoX, height - twoY);
+	}
+}
+
+static void LButton_DrawBorder(struct Context2D* ctx, int x, int y, int width, int height) {
+	BitmapCol backColor = Launcher_Theme.ButtonBorderColor;
+#ifdef CC_BUILD_IOS
+	LWidget_DrawBorder(ctx,      backColor, oneX, oneY, x, y, width, height);
+#else
+	LWidget_DrawInsetBorder(ctx, backColor, oneX, oneY, x, y, width, height);
+#endif
+}
+
+static void LButton_DrawHighlight(struct Context2D* ctx, int x, int y, int width, int height, cc_bool active) {
+	BitmapCol activeColor = BitmapColor_RGB(189, 198, 255);
+	BitmapCol color       = Launcher_Theme.ButtonHighlightColor;
+
+	if (Launcher_Theme.ClassicBackground) {
+		if (active) color = activeColor;
+
+		Context2D_Clear(ctx, color,
+						x + twoX,      y + oneY,
+						width - fourX, oneY);
+		Context2D_Clear(ctx, color,
+						x + oneX,      y + twoY,
+						oneX,          height - fourY);
+	} else if (!active) {
+		Context2D_Clear(ctx, color,
+						x + twoX,      y + oneY,
+						width - fourX, oneY);
+	}
+}
+
+void LButton_DrawBackground(struct Context2D* ctx, int x, int y, int width, int height, cc_bool active) {
+	LButton_DrawBase(     ctx, x, y, width, height, active);
+	LButton_DrawBorder(   ctx, x, y, width, height);
+	LButton_DrawHighlight(ctx, x, y, width, height, active);
+}
+
+static void LButton_Draw(void* widget) {
+	struct LButton* w = (struct LButton*)widget;
+	LBackend_ButtonDraw(w);
+}
+
+static void LButton_Hover(void* w, int idx, cc_bool wasOver) {
+	/* only need to redraw when changing from unhovered to active */
+	if (!wasOver) LBackend_MarkDirty(w);
+}
+
+static void LButton_Unhover(void* w) { LBackend_MarkDirty(w); }
+static void LButton_OnSelect(void* w,   int idx, cc_bool wasSelected) { LBackend_MarkDirty(w); }
+static void LButton_OnUnselect(void* w, int idx) { LBackend_MarkDirty(w); }
+
+static const struct LWidgetVTABLE lbutton_VTABLE = {
+	LButton_Draw, NULL,
+	NULL, NULL,                          /* Key    */
+	LButton_Hover,    LButton_Unhover,   /* Hover  */
+	LButton_OnSelect, LButton_OnUnselect /* Select */
+};
+void LButton_Add(void* screen, struct LButton* w, int width, int height, const char* text, 
+					LWidgetFunc onClick, const struct LLayout* layouts) {
+	w->VTABLE  = &lbutton_VTABLE;
+	w->type    = LWIDGET_BUTTON;
+	w->OnClick = onClick;
+	w->layouts = layouts;
+	w->autoSelectable = true;
+
+	LBackend_ButtonInit(w, width, height);
+	LButton_SetConst(w, text);
+	LScreen_AddWidget(screen, w);
+}
+
+void LButton_SetConst(struct LButton* w, const char* text) {
+	w->text = String_FromReadonly(text);
+	LBackend_ButtonUpdate(w);
+}
+
+
+/*########################################################################################################################*
+*-----------------------------------------------------CheckboxWidget------------------------------------------------------*
+*#########################################################################################################################*/
+static void LCheckbox_Draw(void* widget) {
+	struct LCheckbox* w = (struct LCheckbox*)widget;
+	LBackend_CheckboxDraw(w);
+}
+
+static const struct LWidgetVTABLE lcheckbox_VTABLE = {
+	LCheckbox_Draw, NULL,
+	NULL, NULL, /* Key    */
+	NULL, NULL, /* Hover  */
+	NULL, NULL  /* Select */
+};
+void LCheckbox_Add(void* screen, struct LCheckbox* w, const char* text, 
+					LCheckboxChanged onChanged, const struct LLayout* layouts) {
+	w->VTABLE  = &lcheckbox_VTABLE;
+	w->type    = LWIDGET_CHECKBOX;
+	w->layouts = layouts;
+	w->autoSelectable = true;
+	w->ValueChanged   = onChanged;
+
+	w->text = String_FromReadonly(text);
+	LBackend_CheckboxInit(w);
+	LScreen_AddWidget(screen, w);
+}
+
+void LCheckbox_Set(struct LCheckbox* w, cc_bool value) {
+	w->value = value;
+	LBackend_CheckboxUpdate(w);
+}
+
+
+/*########################################################################################################################*
+*------------------------------------------------------InputWidget--------------------------------------------------------*
+*#########################################################################################################################*/
+void LInput_UNSAFE_GetText(struct LInput* w, cc_string* text) {
+	int i;
+	if (w->inputType != KEYBOARD_TYPE_PASSWORD) { *text = w->text; return; }
+
+	for (i = 0; i < w->text.length; i++) {
+		String_Append(text, '*');
+	}
+}
+
+static void LInput_Draw(void* widget) {
+	struct LInput* w = (struct LInput*)widget;
+	LBackend_InputDraw(w);
+}
+
+static void LInput_TickCaret(void* widget) {
+	struct LInput* w = (struct LInput*)widget;
+	LBackend_InputTick(w);
+}
+
+static void LInput_Select(void* widget, int idx, cc_bool wasSelected) {
+	struct LInput* w = (struct LInput*)widget;
+	LBackend_InputSelect(w, idx, wasSelected);
+}
+
+static void LInput_Unselect(void* widget, int idx) {
+	struct LInput* w = (struct LInput*)widget;
+	LBackend_InputUnselect(w);
+}
+
+static void LInput_AdvanceCaretPos(struct LInput* w, cc_bool forwards) {
+	if (forwards && w->caretPos == -1) return;
+	if (!forwards && w->caretPos == 0) return;
+	if (w->caretPos == -1 && !forwards) /* caret after text */
+		w->caretPos = w->text.length;
+
+	w->caretPos += (forwards ? 1 : -1);
+	if (w->caretPos < 0 || w->caretPos >= w->text.length) w->caretPos = -1;
+	LBackend_InputUpdate(w);
+}
+
+static void LInput_CopyFromClipboard(struct LInput* w) {
+	cc_string text; char textBuffer[2048];
+	String_InitArray(text, textBuffer);
+
+	Clipboard_GetText(&text);
+	String_UNSAFE_TrimStart(&text);
+	String_UNSAFE_TrimEnd(&text);
+
+	if (w->ClipboardFilter) w->ClipboardFilter(&text);
+	LInput_AppendString(w, &text);
+}
+
+/* If caret position is now beyond end of text, resets to -1 */
+static CC_INLINE void LInput_ClampCaret(struct LInput* w) {
+	if (w->caretPos >= w->text.length) w->caretPos = -1;
+}
+
+/* Removes the character preceding the caret in the currently entered text */
+static void LInput_Backspace(struct LInput* w) {
+	if (!w->text.length || w->caretPos == 0) return;
+
+	if (w->caretPos == -1) {
+		String_DeleteAt(&w->text, w->text.length - 1);
+	} else {	
+		String_DeleteAt(&w->text, w->caretPos - 1);
+		w->caretPos--;
+		if (w->caretPos == -1) w->caretPos = 0;
+	}
+
+	if (w->TextChanged) w->TextChanged(w);
+	LInput_ClampCaret(w);
+	LBackend_InputUpdate(w);
+}
+
+/* Removes the character at the caret in the currently entered text */
+static void LInput_Delete(struct LInput* w) {
+	if (!w->text.length || w->caretPos == -1) return;
+
+	String_DeleteAt(&w->text, w->caretPos);
+	if (w->caretPos == -1) w->caretPos = 0;
+
+	if (w->TextChanged) w->TextChanged(w);
+	LInput_ClampCaret(w);
+	LBackend_InputUpdate(w);
+}
+
+static cc_bool LInput_KeyDown(void* widget, int key, cc_bool was) {
+	struct LInput* w = (struct LInput*)widget;
+	if (key == CCKEY_BACKSPACE) {
+		LInput_Backspace(w);
+	} else if (key == CCKEY_DELETE) {
+		LInput_Delete(w);
+	} else if (key == INPUT_CLIPBOARD_COPY) {
+		if (w->text.length) Clipboard_SetText(&w->text);
+	} else if (key == INPUT_CLIPBOARD_PASTE) {
+		LInput_CopyFromClipboard(w);
+	} else if (Input_IsEscapeButton(key)) {
+		if (w->text.length) LInput_SetString(w, &String_Empty);
+	} else if (Input_IsLeftButton(key)) {
+		LInput_AdvanceCaretPos(w, false);
+	} else if (Input_IsRightButton(key)) {
+		LInput_AdvanceCaretPos(w, true);
+	} else { return false; }
+
+	return true;
+}
+
+static cc_bool LInput_CanAppend(struct LInput* w, char c) {
+	switch (w->inputType) {
+	case KEYBOARD_TYPE_PASSWORD:
+		return true; /* keyboard accepts all characters */
+	case KEYBOARD_TYPE_INTEGER:
+		return c >= '0' && c <= '9';
+	}
+	return c >= ' ' && c <= '~' && c != '&';
+}
+
+/* Appends a character to the currently entered text */
+static CC_NOINLINE cc_bool LInput_Append(struct LInput* w, char c) {
+	if (LInput_CanAppend(w, c) && w->text.length < w->text.capacity) {
+		if (w->caretPos == -1) {
+			String_Append(&w->text, c);
+		} else {
+			String_InsertAt(&w->text, w->caretPos, c);
+			w->caretPos++;
+		}
+		return true;
+	}
+	return false;
+}
+
+static void LInput_KeyChar(void* widget, char c) {
+	struct LInput* w = (struct LInput*)widget;
+	cc_bool appended = LInput_Append(w, c);
+
+	if (appended && w->TextChanged) w->TextChanged(w);
+	if (appended) LBackend_InputUpdate(w);
+}
+
+static void LInput_TextChanged(void* widget, const cc_string* str) {
+	struct LInput* w = (struct LInput*)widget;
+	LInput_SetText(w, str);
+	if (w->TextChanged) w->TextChanged(w);
+}
+
+static const struct LWidgetVTABLE linput_VTABLE = {
+	LInput_Draw, LInput_TickCaret,
+	LInput_KeyDown, LInput_KeyChar, /* Key    */
+	NULL, NULL,                     /* Hover  */
+	/* TODO: Don't redraw whole thing, just the outer border */
+	LInput_Select, LInput_Unselect, /* Select */
+	NULL, LInput_TextChanged        /* TextChanged */
+};
+void LInput_Add(void* screen, struct LInput* w, int width, const char* hintText, 
+				const struct LLayout* layouts) {
+	w->VTABLE  = &linput_VTABLE;
+	w->type    = LWIDGET_INPUT;
+	w->autoSelectable = true;
+	w->opaque  = true;
+	w->layouts = layouts;
+
+	/* Preserve existing input across Add calls */
+	if (!w->text.buffer) {
+		String_InitArray(w->text, w->_textBuffer);
+	}
+	
+	w->hintText = hintText;
+	w->caretPos = -1;
+	LBackend_InputInit(w, width);
+	LScreen_AddWidget(screen, w);
+}
+
+void LInput_SetText(struct LInput* w, const cc_string* text) {
+	String_Copy(&w->text, text);
+	LInput_ClampCaret(w);
+	LBackend_InputUpdate(w);
+}
+
+void LInput_ClearText(struct LInput* w) {
+	w->text.length = 0;
+	w->caretPos    = -1;
+	LBackend_InputUpdate(w);
+}
+
+void LInput_AppendString(struct LInput* w, const cc_string* str) {
+	int i, appended = 0;
+	for (i = 0; i < str->length; i++) {
+		if (LInput_Append(w, str->buffer[i])) appended++;
+	}
+
+	if (appended && w->TextChanged) w->TextChanged(w);
+	if (appended) LBackend_InputUpdate(w);
+}
+
+void LInput_SetString(struct LInput* w, const cc_string* str) {
+	LInput_SetText(w, str);
+	if (w->TextChanged) w->TextChanged(w);
+}
+
+
+/*########################################################################################################################*
+*------------------------------------------------------LabelWidget--------------------------------------------------------*
+*#########################################################################################################################*/
+static void LLabel_Draw(void* widget) {
+	struct LLabel* w = (struct LLabel*)widget;
+	LBackend_LabelDraw(w);
+}
+
+static const struct LWidgetVTABLE llabel_VTABLE = {
+	LLabel_Draw, NULL,
+	NULL, NULL, /* Key    */
+	NULL, NULL, /* Hover  */
+	NULL, NULL  /* Select */
+};
+void LLabel_Add(void* screen, struct LLabel* w, const char* text, 
+				const struct LLayout* layouts) {
+	w->VTABLE  = &llabel_VTABLE;
+	w->type    = LWIDGET_LABEL;
+	w->layouts = layouts;
+
+	String_InitArray(w->text, w->_textBuffer);
+	LBackend_LabelInit(w);
+	LLabel_SetConst(w, text);
+	LScreen_AddWidget(screen, w);
+}
+
+void LLabel_SetText(struct LLabel* w, const cc_string* text) {
+	String_Copy(&w->text, text);
+	LBackend_LabelUpdate(w);
+	LBackend_LayoutWidget((struct LWidget*)w);
+}
+
+void LLabel_SetConst(struct LLabel* w, const char* text) {
+	cc_string str = String_FromReadonly(text);
+	LLabel_SetText(w, &str);
+}
+
+
+/*########################################################################################################################*
+*-------------------------------------------------------LineWidget--------------------------------------------------------*
+*#########################################################################################################################*/
+static void LLine_Draw(void* widget) {
+	struct LLine* w = (struct LLine*)widget;
+	LBackend_LineDraw(w);
+}
+
+static const struct LWidgetVTABLE lline_VTABLE = {
+	LLine_Draw, NULL,
+	NULL, NULL, /* Key    */
+	NULL, NULL, /* Hover  */
+	NULL, NULL  /* Select */
+};
+void LLine_Add(void* screen, struct LLine* w, int width, 
+				const struct LLayout* layouts) {
+	w->VTABLE  = &lline_VTABLE;
+	w->type    = LWIDGET_LINE;
+	w->layouts = layouts;
+
+	LBackend_LineInit(w, width);
+	LScreen_AddWidget(screen, w);
+}
+
+#define CLASSIC_LINE_COLOR BitmapColor_RGB(128, 128, 128)
+BitmapCol LLine_GetColor(void) {
+	return Launcher_Theme.ClassicBackground ? CLASSIC_LINE_COLOR : Launcher_Theme.ButtonBorderColor;
+}
+
+
+/*########################################################################################################################*
+*------------------------------------------------------SliderWidget-------------------------------------------------------*
+*#########################################################################################################################*/
+static void LSlider_Draw(void* widget) {
+	struct LSlider* w = (struct LSlider*)widget;
+	LBackend_SliderDraw(w);
+}
+
+static const struct LWidgetVTABLE lslider_VTABLE = {
+	LSlider_Draw, NULL,
+	NULL, NULL, /* Key    */
+	NULL, NULL, /* Hover  */
+	NULL, NULL  /* Select */
+};
+void LSlider_Add(void* screen, struct LSlider* w, int width, int height, BitmapCol color, 
+				const struct LLayout* layouts) {
+	w->VTABLE  = &lslider_VTABLE;
+	w->type    = LWIDGET_SLIDER;
+	w->color   = color;
+	w->opaque  = true;
+	w->layouts = layouts;
+
+	LBackend_SliderInit(w, width, height);
+	LScreen_AddWidget(screen, w);
+}
+
+void LSlider_SetProgress(struct LSlider* w, int progress) {
+	if (progress == w->value) return;
+	w->value = progress;
+	LBackend_SliderUpdate(w);
+}
+
+
+/*########################################################################################################################*
+*------------------------------------------------------TableWidget--------------------------------------------------------*
+*#########################################################################################################################*/
+static void FlagColumn_Draw(struct ServerInfo* row, struct DrawTextArgs* args, struct LTableCell* cell, struct Context2D* ctx) {
+	struct Flag* flag = Flags_Get(row);
+	if (!flag) return;
+	Context2D_DrawPixels(ctx, cell->x + flagXOffset, cell->y + flagYOffset, &flag->bmp);
+}
+
+static void NameColumn_Draw(struct ServerInfo* row, struct DrawTextArgs* args, struct LTableCell* cell, struct Context2D* ctx) {
+	args->text = row->name;
+}
+static int NameColumn_Sort(const struct ServerInfo* a, const struct ServerInfo* b) {
+	return String_Compare(&b->name, &a->name);
+}
+
+static void PlayersColumn_Draw(struct ServerInfo* row, struct DrawTextArgs* args, struct LTableCell* cell, struct Context2D* ctx) {
+	String_Format2(&args->text, "%i/%i", &row->players, &row->maxPlayers);
+}
+static int PlayersColumn_Sort(const struct ServerInfo* a, const struct ServerInfo* b) {
+	return b->players - a->players;
+}
+
+static void UptimeColumn_Draw(struct ServerInfo* row, struct DrawTextArgs* args, struct LTableCell* cell, struct Context2D* ctx) {
+	LTable_FormatUptime(&args->text, row->uptime);
+}
+static int UptimeColumn_Sort(const struct ServerInfo* a, const struct ServerInfo* b) {
+	return b->uptime - a->uptime;
+}
+
+static void SoftwareColumn_Draw(struct ServerInfo* row, struct DrawTextArgs* args, struct LTableCell* cell, struct Context2D* ctx) {
+	/* last column, so adjust to fit size of table */
+	int leftover = cell->table->width - cell->x;
+	cell->width  = max(cell->width, leftover);
+	args->text   = row->software;
+}
+static int SoftwareColumn_Sort(const struct ServerInfo* a, const struct ServerInfo* b) {
+	return String_Compare(&b->software, &a->software);
+}
+
+static struct LTableColumn tableColumns[] = {
+	{ "",          15, FlagColumn_Draw,     NULL,                false, false, false },
+	{ "Name",     328, NameColumn_Draw,     NameColumn_Sort,     true,  false, true  },
+	{ "Players",   73, PlayersColumn_Draw,  PlayersColumn_Sort,  true,  true,  true  },
+	{ "Uptime",    73, UptimeColumn_Draw,   UptimeColumn_Sort,   true,  true,  true  },
+	{ "Software", 143, SoftwareColumn_Draw, SoftwareColumn_Sort, false, true,  true  }
+};
+
+
+void LTable_FormatUptime(cc_string* dst, int uptime) {
+	char unit = 's';
+
+	if (uptime >= SECS_PER_DAY * 7) {
+		uptime /= SECS_PER_DAY;  unit = 'd';
+	} else if (uptime >= SECS_PER_HOUR) {
+		uptime /= SECS_PER_HOUR; unit = 'h';
+	} else if (uptime >= SECS_PER_MIN) {
+		uptime /= SECS_PER_MIN;  unit = 'm';
+	}
+	String_Format2(dst, "%i%r", &uptime, &unit);
+}
+
+void LTable_GetScrollbarCoords(struct LTable* w, int* y, int* height) {
+	float scale;
+	if (!w->rowsCount) { *y = 0; *height = 0; return; }
+
+	scale   = w->height / (float)w->rowsCount;
+	*y      = Math_Ceil(w->topRow * scale);
+	*height = Math_Ceil(w->visibleRows * scale);
+	*height = min(*y + *height, w->height) - *y;
+}
+
+void LTable_ClampTopRow(struct LTable* w) { 
+	if (w->topRow > w->rowsCount - w->visibleRows) {
+		w->topRow = w->rowsCount - w->visibleRows;
+	}
+	if (w->topRow < 0) w->topRow = 0;
+}
+
+int LTable_GetSelectedIndex(struct LTable* w) {
+	struct ServerInfo* entry;
+	int row;
+
+	for (row = 0; row < w->rowsCount; row++) {
+		entry = LTable_Get(row);
+		if (String_CaselessEquals(w->selectedHash, &entry->hash)) return row;
+	}
+	return -1;
+}
+
+void LTable_SetSelectedTo(struct LTable* w, int index) {
+	if (!w->rowsCount) return;
+	if (index >= w->rowsCount) index = w->rowsCount - 1;
+	if (index < 0) index = 0;
+
+	String_Copy(w->selectedHash, &LTable_Get(index)->hash);
+	LTable_ShowSelected(w);
+	w->OnSelectedChanged();
+}
+
+void LTable_RowClick(struct LTable* w, int row) {
+	cc_uint64 now;
+	LTable_SetSelectedTo(w, row);
+	now = Stopwatch_Measure();
+
+	/* double click on row to join */
+	if (Stopwatch_ElapsedMS(w->_lastClick, now) < 1000 && row == w->_lastRow) {
+		Launcher_ConnectToServer(&LTable_Get(row)->hash);
+	}
+
+	w->_lastRow = LTable_GetSelectedIndex(w);
+	w->_lastClick = now;
+}
+
+cc_bool LTable_HandlesKey(int key) {
+	return Input_IsUpButton(key)   || key == CCKEY_PAGEUP ||
+		   Input_IsDownButton(key) || key == CCKEY_PAGEDOWN;
+}
+
+static cc_bool LTable_KeyDown(void* widget, int key, cc_bool was) {
+	struct LTable* w = (struct LTable*)widget;
+	int index = LTable_GetSelectedIndex(w);
+
+	if (Input_IsUpButton(key)) {
+		index--;
+	} else if (Input_IsDownButton(key)) {
+		index++;
+	} else if (key == CCKEY_PAGEUP) {
+		index -= w->visibleRows;
+	} else if (key == CCKEY_PAGEDOWN) {
+		index += w->visibleRows;
+	} else { return false; }
+
+	w->_lastRow = -1;
+	LTable_SetSelectedTo(w, index);
+	return true;
+}
+
+static void LTable_MouseDown(void* widget, int idx, cc_bool wasOver) {
+	struct LTable* w = (struct LTable*)widget;
+	LBackend_TableMouseDown(w, idx);
+}
+
+static void LTable_MouseMove(void* widget, int idx, cc_bool wasOver) {
+	struct LTable* w = (struct LTable*)widget;
+	LBackend_TableMouseMove(w, idx);
+}
+
+static void LTable_MouseUp(void* widget, int idx) {
+	struct LTable* w = (struct LTable*)widget;
+	LBackend_TableMouseUp(w, idx);
+}
+
+static void LTable_MouseWheel(void* widget, float delta) {
+	struct LTable* w = (struct LTable*)widget;
+	w->topRow -= Utils_AccumulateWheelDelta(&w->_wheelAcc, delta);
+	LTable_ClampTopRow(w);
+	LBackend_MarkDirty(w);
+	w->_lastRow = -1;
+}
+
+static void LTable_Draw(void* widget) {
+	struct LTable* w = (struct LTable*)widget;
+	LBackend_TableDraw(w);
+}
+
+static const struct LWidgetVTABLE ltable_VTABLE = {
+	LTable_Draw,      NULL,
+	LTable_KeyDown,   NULL, /* Key    */
+	LTable_MouseMove, NULL, /* Hover  */
+	LTable_MouseDown, LTable_MouseUp, /* Select */
+	LTable_MouseWheel,      /* Wheel */
+};
+void LTable_Add(void* screen, struct LTable* w, 
+				const struct LLayout* layouts) {
+	int i;
+	w->VTABLE     = &ltable_VTABLE;
+	w->type       = LWIDGET_TABLE;
+	w->columns    = tableColumns;
+	w->numColumns = Array_Elems(tableColumns);
+	w->sortingCol = -1;
+	w->opaque     = true;
+	w->layouts    = layouts;
+	
+	for (i = 0; i < w->numColumns; i++) {
+		w->columns[i].width = Display_ScaleX(w->columns[i].width);
+	}
+	LBackend_TableInit(w);
+	LScreen_AddWidget(screen, w);
+}
+
+void LTable_Reset(struct LTable* w) {
+	LBackend_TableMouseUp(w, 0);
+	LBackend_TableReposition(w);
+
+	w->topRow     = 0;
+	w->rowsCount  = 0;
+	w->_wheelAcc  = 0.0f;
+	w->sortingCol = -1;
+}
+
+static int ShouldShowServer(struct LTable* w, struct ServerInfo* server) {
+	return String_CaselessContains(&server->name, w->filter) 
+		&& (Launcher_ShowEmptyServers || server->players > 0);
+}
+
+void LTable_ApplyFilter(struct LTable* w) {
+	int i, j, count;
+
+	count = FetchServersTask.numServers;
+	for (i = 0, j = 0; i < count; i++) {
+		if (ShouldShowServer(w, Servers_Get(i))) {
+			FetchServersTask.servers[j++]._order = FetchServersTask.orders[i];
+		}
+	}
+
+	w->rowsCount = j;
+	for (; j < count; j++) {
+		FetchServersTask.servers[j]._order = -100000;
+	}
+
+	w->_lastRow = -1;
+	LTable_ClampTopRow(w);
+	LBackend_TableUpdate(w);
+}
+
+static int sortingCol;
+static int LTable_SortOrder(const struct ServerInfo* a, const struct ServerInfo* b) {
+	int order;
+	if (sortingCol >= 0) {
+		order = tableColumns[sortingCol].SortOrder(a, b);
+		return tableColumns[sortingCol].invertSort ? -order : order;
+	}
+
+	/* Default sort order. (most active server, then by highest uptime) */
+	if (a->players != b->players) return a->players - b->players;
+	return a->uptime - b->uptime;
+}
+
+static void LTable_QuickSort(int left, int right) {
+	cc_uint16* keys = FetchServersTask.orders; cc_uint16 key;
+
+	while (left < right) {
+		int i = left, j = right;
+		struct ServerInfo* mid = Servers_Get((i + j) >> 1);
+
+		/* partition the list */
+		while (i <= j) {
+			while (LTable_SortOrder(mid, Servers_Get(i)) < 0) i++;
+			while (LTable_SortOrder(mid, Servers_Get(j)) > 0) j--;
+			QuickSort_Swap_Maybe();
+		}
+		/* recurse into the smaller subset */
+		QuickSort_Recurse(LTable_QuickSort)
+	}
+}
+
+void LTable_Sort(struct LTable* w) {
+	sortingCol = w->sortingCol;
+	FetchServersTask_ResetOrder();
+
+	if (FetchServersTask.numServers)
+		LTable_QuickSort(0, FetchServersTask.numServers - 1);
+
+	LTable_ApplyFilter(w);
+	LTable_ShowSelected(w);
+}
+
+void LTable_ShowSelected(struct LTable* w) {
+	int i = LTable_GetSelectedIndex(w);
+	if (i == -1) return;
+
+	if (i >= w->topRow + w->visibleRows) {
+		w->topRow = i - (w->visibleRows - 1);
+	}
+	if (i < w->topRow) w->topRow = i;
+	LTable_ClampTopRow(w);
+}
+
+BitmapCol LTable_RowColor(int row, cc_bool selected, cc_bool featured) {
+	BitmapCol featSelColor  = BitmapColor_RGB( 50,  53,  0);
+	BitmapCol featuredColor = BitmapColor_RGB(101, 107,  0);
+	BitmapCol selectedColor = BitmapColor_RGB( 40,  40, 40);
+
+	if (featured) {
+		return selected ? featSelColor : featuredColor;
+	} else if (selected) {
+		return selectedColor;
+	}
+
+	if (!Launcher_Theme.ClassicBackground) {
+		return BitmapColor_RGB(20, 20, 10);
+	} else {
+		return (row & 1) == 0 ? Launcher_Theme.BackgroundColor : 0;
+	}
+}
+#endif