commit 518088c7fb788b170cfbcd7e8d048555f541f27f
parent bd0ff6bd2213fc11e8455d1d6d03a51a99d43570
Author: Michael Savage <mikejsavage@gmail.com>
Date:   Fri,  7 Sep 2018 16:27:17 +0300
Split platform independent UI stuff into ui.cc, defer all redrawing
Diffstat:
14 files changed, 542 insertions(+), 712 deletions(-)
diff --git a/make.lua b/make.lua
@@ -12,6 +12,6 @@ if OS == "windows" then
 	libs = { "lua", "lpeg", "lfs" }
 end
 
-bin( "mudgangster", { platform_objs, "src/script", "src/textbox", "src/input", "src/platform_network" }, libs )
+bin( "mudgangster", { platform_objs, "src/ui", "src/script", "src/textbox", "src/input", "src/platform_network" }, libs )
 msvc_bin_ldflags( "mudgangster", "gdi32.lib Ws2_32.lib" )
 gcc_bin_ldflags( "mudgangster", "-lm -lX11 -llua" )
diff --git a/src/input.cc b/src/input.cc
@@ -4,10 +4,11 @@
 #include "common.h"
 #include "input.h"
 #include "script.h"
+#include "ui.h"
 
 typedef struct {
 	char * text;
-	int len;
+	size_t len;
 } InputHistory;
 
 static InputHistory inputHistory[ MAX_INPUT_HISTORY ];
@@ -18,10 +19,15 @@ static int inputHistoryDelta = 0;
 static char * inputBuffer = NULL;
 static char * starsBuffer = NULL;
 
-static int inputBufferSize = 256;
+static size_t inputBufferSize = 256;
 
-static int inputLen = 0;
-static int inputPos = 0;
+static size_t inputLen = 0;
+static size_t cursor_pos = 0;
+
+static int left, top;
+static int width, height;
+
+static bool dirty = false;
 
 void input_init() {
 	inputBuffer = ( char * ) malloc( inputBufferSize );
@@ -35,14 +41,6 @@ void input_term() {
 	free( starsBuffer );
 }
 
-InputBuffer input_get_buffer() {
-	InputBuffer buf;
-	buf.buf = inputBuffer;
-	buf.len = inputLen;
-	buf.cursor_pos = inputPos;
-	return buf;
-}
-
 void input_return() {
 	if( inputLen > 0 ) {
 		InputHistory * lastCmd = &inputHistory[ ( inputHistoryHead + inputHistoryCount - 1 ) % MAX_INPUT_HISTORY ];
@@ -71,23 +69,27 @@ void input_return() {
 	inputHistoryDelta = 0;
 
 	inputLen = 0;
-	inputPos = 0;
+	cursor_pos = 0;
+
+	dirty = true;
 }
 
 void input_backspace() {
-	if( inputPos > 0 ) {
-		memmove( inputBuffer + inputPos - 1, inputBuffer + inputPos, inputLen - inputPos );
+	if( cursor_pos > 0 ) {
+		memmove( inputBuffer + cursor_pos - 1, inputBuffer + cursor_pos, inputLen - cursor_pos );
 
 		inputLen--;
-		inputPos--;
+		cursor_pos--;
+		dirty = true;
 	}
 }
 
 void input_delete() {
-	if( inputPos < inputLen ) {
-		memmove( inputBuffer + inputPos, inputBuffer + inputPos + 1, inputLen - inputPos );
+	if( cursor_pos < inputLen ) {
+		memmove( inputBuffer + cursor_pos, inputBuffer + cursor_pos + 1, inputLen - cursor_pos );
 
 		inputLen--;
+		dirty = true;
 	}
 }
 
@@ -103,7 +105,8 @@ void input_up() {
 	memcpy( inputBuffer, cmd.text, cmd.len );
 
 	inputLen = cmd.len;
-	inputPos = cmd.len;
+	cursor_pos = cmd.len;
+	dirty = true;
 }
 
 void input_down() {
@@ -120,20 +123,26 @@ void input_down() {
 		memcpy( inputBuffer, cmd.text, cmd.len );
 
 		inputLen = cmd.len;
-		inputPos = cmd.len;
+		cursor_pos = cmd.len;
 	}
 	else {
 		inputLen = 0;
-		inputPos = 0;
+		cursor_pos = 0;
 	}
+
+	dirty = true;
 }
 
 void input_left() {
-	inputPos = max( inputPos - 1, 0 );
+	if( cursor_pos > 0 ) {
+		cursor_pos--;
+		dirty = true;
+	}
 }
 
 void input_right() {
-	inputPos = min( inputPos + 1, inputLen );
+	cursor_pos = min( cursor_pos + 1, inputLen );
+	dirty = true;
 }
 
 void input_add( const char * buffer, int len ) {
@@ -146,12 +155,47 @@ void input_add( const char * buffer, int len ) {
 		memset( starsBuffer + inputBufferSize / 2, '*', inputBufferSize / 2 );
 	}
 
-	if( inputPos < inputLen ) {
-		memmove( inputBuffer + inputPos + len, inputBuffer + inputPos, inputLen - inputPos );
+	if( cursor_pos < inputLen ) {
+		memmove( inputBuffer + cursor_pos + len, inputBuffer + cursor_pos, inputLen - cursor_pos );
 	}
 
-	memcpy( inputBuffer + inputPos, buffer, len );
+	memcpy( inputBuffer + cursor_pos, buffer, len );
 
 	inputLen += len;
-	inputPos += len;
+	cursor_pos += len;
+
+	dirty = true;
+}
+
+void input_set_pos( int x, int y ) {
+	left = x;
+	top = y;
+}
+
+void input_set_size( int w, int h ) {
+	width = w;
+	height = h;
+}
+
+bool input_is_dirty() {
+	return dirty;
+}
+
+void input_draw() {
+	int fw, fh;
+	ui_get_font_size( &fw, &fh );
+
+	ui_fill_rect( left, top, width, height, COLOUR_BG, false );
+
+	for( size_t i = 0; i < inputLen; i++ ) {
+		ui_draw_char( PADDING + i * fw, top - SPACING, inputBuffer[ i ], WHITE, false );
+	}
+
+	ui_fill_rect( PADDING + cursor_pos * fw, top, fw, fh, COLOUR_CURSOR, false );
+
+	if( cursor_pos < inputLen ) {
+		ui_draw_char( PADDING + cursor_pos * fw, top - SPACING, inputBuffer[ cursor_pos ], COLOUR_BG, false );
+	}
+
+	dirty = false;
 }
diff --git a/src/input.h b/src/input.h
@@ -2,13 +2,6 @@
 
 #include <stddef.h>
 
-struct InputBuffer {
-	char * buf;
-
-	size_t len;
-	size_t cursor_pos;
-};
-
 void input_init();
 void input_term();
 
@@ -23,4 +16,7 @@ void input_right();
 
 void input_add( const char * buffer, int len );
 
-InputBuffer input_get_buffer();
+void input_set_pos( int x, int y );
+void input_set_size( int w, int h );
+bool input_is_dirty();
+void input_draw();
diff --git a/src/main.cc b/src/main.cc
@@ -1,17 +1,20 @@
 #include "script.h"
 #include "input.h"
 #include "ui.h"
+#include "platform_ui.h"
 #include "platform_network.h"
 
 int main() {
 	net_init();
 	ui_init();
 	input_init();
+	platform_ui_init();
 	script_init();
 
 	event_loop();
 
 	script_term();
+	platform_ui_term();
 	input_term();
 	ui_term();
 	net_term();
diff --git a/src/platform_network.cc b/src/platform_network.cc
@@ -7,7 +7,7 @@ struct sockaddr_storage;
 
 static NetAddress sockaddr_to_netaddress( const struct sockaddr_storage & ss );
 static struct sockaddr_storage netaddress_to_sockaddr( const NetAddress & addr );
-static void setsockoptone( OSSocket fd, int level, int opt );
+static void setsockoptone( PlatformSocket fd, int level, int opt );
 
 #if PLATFORM_WINDOWS
 #include "win32_network.cc"
@@ -70,7 +70,7 @@ static struct sockaddr_storage netaddress_to_sockaddr( const NetAddress & addr )
 	return ss;
 }
 
-static void setsockoptone( OSSocket fd, int level, int opt ) {
+static void setsockoptone( PlatformSocket fd, int level, int opt ) {
 	int one = 1;
 	int ok = setsockopt( fd, level, opt, ( char * ) &one, sizeof( one ) );
 	if( ok == -1 ) {
diff --git a/src/platform_network.h b/src/platform_network.h
@@ -5,9 +5,9 @@
 
 #if PLATFORM_WINDOWS
 #include <winsock2.h>
-typedef SOCKET OSSocket;
+typedef SOCKET PlatformSocket;
 #elif PLATFORM_UNIX
-typedef int OSSocket;
+typedef int PlatformSocket;
 #else
 #error new platform
 #endif
@@ -21,7 +21,7 @@ enum TCPRecvResult {
 };
 
 struct TCPSocket {
-	OSSocket fd;
+	PlatformSocket fd;
 };
 
 struct IPv4 { u8 bytes[ 4 ]; };
diff --git a/src/platform_ui.h b/src/platform_ui.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include "ui.h"
+
+void platform_ui_init();
+void platform_ui_term();
+
+void platform_fill_rect( int left, int top, int width, int height, Colour colour, bool bold );
+void platform_draw_char( int left, int top, char c, Colour colour, bool bold, bool force_bold_font );
+void platform_make_dirty( int left, int top, int width, int height );
diff --git a/src/script.cc b/src/script.cc
@@ -192,8 +192,6 @@ extern "C" int mud_setStatus( lua_State * L ) {
 		lua_pop( L, 4 );
 	}
 
-	ui_draw_status();
-
 	return 0;
 }
 
diff --git a/src/textbox.cc b/src/textbox.cc
@@ -92,6 +92,55 @@ void textbox_page_up( TextBox * tb ) {
 	textbox_scroll( tb, num_rows( tb->h ) - 1 );
 }
 
+void textbox_mouse_down( TextBox * tb, int window_x, int window_y ) {
+	int x = window_x - tb->x;
+	int y = window_y - tb->y;
+
+	if( x < 0 || y < 0 || x >= tb->w || y >= tb->h )
+		return;
+
+	int fw, fh;
+	ui_get_font_size( &fw, &fh );
+
+	int row = ( tb->h - y ) / ( fh + SPACING );
+	int col = x / fw;
+
+	tb->selecting = true;
+	tb->selection_start_col = col;
+	tb->selection_start_row = row;
+	tb->selection_end_col = col;
+	tb->selection_end_row = row;
+	tb->dirty = true;
+}
+
+void textbox_mouse_move( TextBox * tb, int window_x, int window_y ) {
+	if( !tb->selecting )
+		return;
+
+	int x = window_x - tb->x;
+	int y = window_y - tb->y;
+
+	int fw, fh;
+	ui_get_font_size( &fw, &fh );
+
+	int row = ( tb->h - y ) / ( fh + SPACING );
+	int col = x / fw;
+
+	tb->selection_end_col = col;
+	tb->selection_end_row = row;
+	tb->dirty = true;
+}
+
+void textbox_mouse_up( TextBox * tb, int window_x, int window_y ) {
+	if( !tb->selecting )
+		return;
+
+	// TODO: copy the text
+
+	tb->selecting = false;
+	tb->dirty = true;
+}
+
 void textbox_set_pos( TextBox * tb, int x, int y ) {
 	tb->x = x;
 	tb->y = y;
diff --git a/src/textbox.h b/src/textbox.h
@@ -41,6 +41,10 @@ void textbox_scroll( TextBox * tb, int offset );
 void textbox_page_down( TextBox * tb );
 void textbox_page_up( TextBox * tb );
 
+void textbox_mouse_down( TextBox * tb, int x, int y );
+void textbox_mouse_move( TextBox * tb, int x, int y );
+void textbox_mouse_up( TextBox * tb, int x, int y );
+
 void textbox_set_pos( TextBox * tb, int x, int y );
 void textbox_set_size( TextBox * tb, int w, int h );
 
diff --git a/src/ui.cc b/src/ui.cc
@@ -0,0 +1,336 @@
+#include "platform_ui.h"
+#include "input.h"
+#include "textbox.h"
+
+static TextBox main_text;
+static TextBox chat_text;
+
+static int window_width, window_height;
+
+typedef struct {
+	char c;
+
+	Colour fg;
+	bool bold;
+} StatusChar;
+
+static StatusChar * statusContents = NULL;
+static size_t statusCapacity = 256;
+static size_t statusLen = 0;
+static bool status_dirty = false;
+
+void ui_init() {
+	textbox_init( &main_text, SCROLLBACK_SIZE );
+	textbox_init( &chat_text, CHAT_ROWS );
+
+	statusContents = ( StatusChar * ) malloc( statusCapacity * sizeof( StatusChar ) );
+	if( statusContents == NULL )
+		FATAL( "malloc" );
+}
+
+void ui_term() {
+	textbox_destroy( &main_text );
+	textbox_destroy( &chat_text );
+	free( statusContents );
+}
+
+void ui_fill_rect( int left, int top, int width, int height, Colour colour, bool bold ) {
+	platform_fill_rect( left, top, width, height, colour, bold );
+	platform_make_dirty( left, top, width, height );
+}
+
+void ui_draw_char( int left, int top, char c, Colour colour, bool bold, bool force_bold_font ) {
+	int fw, fh;
+	ui_get_font_size( &fw, &fh );
+
+	int left_spacing = fw / 2;
+	int right_spacing = fw - left_spacing;
+	int line_height = fh + SPACING;
+	int top_spacing = line_height / 2;
+	int bot_spacing = line_height - top_spacing;
+
+	// TODO: not the right char...
+	// if( uint8_t( c ) == 155 ) { // fill
+	// 	ui_fill_rect( left, top, fw, fh, colour, bold );
+	// 	return;
+	// }
+
+	// TODO: this has a vertical seam. using textbox-space coordinates would help
+	if( uint8_t( c ) == 176 ) { // light shade
+		for( int y = 0; y < fh; y += 3 ) {
+			for( int x = y % 6 == 0 ? 0 : 1; x < fw; x += 2 ) {
+				ui_fill_rect( left + x, top + y, 1, 1, colour, bold );
+			}
+		}
+		return;
+	}
+
+	// TODO: this has a horizontal seam but so does mm2k
+	if( uint8_t( c ) == 177 ) { // medium shade
+		for( int y = 0; y < fh; y += 2 ) {
+			for( int x = y % 4 == 0 ? 1 : 0; x < fw; x += 2 ) {
+				ui_fill_rect( left + x, top + y, 1, 1, colour, bold );
+			}
+		}
+		return;
+	}
+
+	// TODO: this probably has a horizontal seam
+	if( uint8_t( c ) == 178 ) { // heavy shade
+		for( int y = 0; y < fh + SPACING; y++ ) {
+			for( int x = y % 2 == 0 ? 1 : 0; x < fw; x += 2 ) {
+				ui_fill_rect( left + x, top + y, 1, 1, colour, bold );
+			}
+		}
+		return;
+	}
+
+	if( uint8_t( c ) == 179 ) { // vertical
+		ui_fill_rect( left + left_spacing, top, 1, line_height, colour, bold );
+		return;
+		// set_fg( colour, bold );
+		// const char asdf[] = "│";
+		// Xutf8DrawString( UI.display, UI.back_buffer, ( bold ? Style.fontBold : Style.font ).font, UI.gc, left, top + Style.font.ascent + SPACING, asdf, sizeof( asdf ) - 1 );
+	}
+
+	if( uint8_t( c ) == 180 ) { // right stopper
+		ui_fill_rect( left, top + top_spacing, left_spacing, 1, colour, bold );
+		ui_fill_rect( left + left_spacing, top, 1, line_height, colour, bold );
+		return;
+	}
+
+	if( uint8_t( c ) == 186 ) { // double vertical
+		ui_fill_rect( left + left_spacing - 1, top, 1, line_height, colour, bold );
+		ui_fill_rect( left + left_spacing + 1, top, 1, line_height, colour, bold );
+		return;
+	}
+
+	if( uint8_t( c ) == 187 ) { // double top right
+		ui_fill_rect( left, top + top_spacing - 1, right_spacing + 1, 1, colour, bold );
+		ui_fill_rect( left + left_spacing + 1, top + top_spacing - 1, 1, bot_spacing + 1, colour, bold );
+		ui_fill_rect( left, top + top_spacing + 1, right_spacing - 1, 1, colour, bold );
+		ui_fill_rect( left + left_spacing - 1, top + top_spacing + 1, 1, bot_spacing - 1, colour, bold );
+		return;
+	}
+
+	if( uint8_t( c ) == 188 ) { // double bottom right
+		ui_fill_rect( left, top + top_spacing + 1, right_spacing + 1, 1, colour, bold );
+		ui_fill_rect( left + left_spacing + 1, top, 1, top_spacing + 1, colour, bold );
+		ui_fill_rect( left, top + top_spacing - 1, right_spacing - 1, 1, colour, bold );
+		ui_fill_rect( left + left_spacing - 1, top, 1, top_spacing - 1, colour, bold );
+		return;
+	}
+
+	if( uint8_t( c ) == 191 ) { // top right
+		ui_fill_rect( left, top + top_spacing, left_spacing, 1, colour, bold );
+		ui_fill_rect( left + left_spacing, top + top_spacing, 1, bot_spacing, colour, bold );
+		return;
+	}
+
+	if( uint8_t( c ) == 192 ) { // bottom left
+		ui_fill_rect( left + left_spacing, top + top_spacing, right_spacing, 1, colour, bold );
+		ui_fill_rect( left + left_spacing, top, 1, top_spacing, colour, bold );
+		return;
+	}
+
+	if( uint8_t( c ) == 193 ) { // bottom stopper
+		ui_fill_rect( left + left_spacing, top, 1, top_spacing, colour, bold );
+		ui_fill_rect( left, top + top_spacing, fw, 1, colour, bold );
+		return;
+	}
+
+	if( uint8_t( c ) == 194 ) { // top stopper
+		ui_fill_rect( left + left_spacing, top + top_spacing, 1, bot_spacing, colour, bold );
+		ui_fill_rect( left, top + top_spacing, fw, 1, colour, bold );
+		return;
+	}
+
+	if( uint8_t( c ) == 195 ) { // left stopper
+		ui_fill_rect( left + left_spacing, top + top_spacing, right_spacing, 1, colour, bold );
+		ui_fill_rect( left + left_spacing, top, 1, line_height, colour, bold );
+		return;
+	}
+
+	if( uint8_t( c ) == 196 ) { // horizontal
+		ui_fill_rect( left, top + top_spacing, fw, 1, colour, bold );
+		return;
+	}
+
+	if( uint8_t( c ) == 197 ) { // cross
+		ui_fill_rect( left, top + top_spacing, fw, 1, colour, bold );
+		ui_fill_rect( left + left_spacing, top, 1, line_height, colour, bold );
+		return;
+	}
+
+	if( uint8_t( c ) == 200 ) { // double bottom left
+		ui_fill_rect( left + left_spacing - 1, top + top_spacing + 1, right_spacing + 1, 1, colour, bold );
+		ui_fill_rect( left + left_spacing - 1, top, 1, top_spacing + 1, colour, bold );
+		ui_fill_rect( left + left_spacing + 1, top + top_spacing - 1, right_spacing - 1, 1, colour, bold );
+		ui_fill_rect( left + left_spacing + 1, top, 1, top_spacing - 1, colour, bold );
+		return;
+	}
+
+	if( uint8_t( c ) == 201 ) { // double top left
+		ui_fill_rect( left + left_spacing - 1, top + top_spacing - 1, right_spacing + 1, 1, colour, bold );
+		ui_fill_rect( left + left_spacing - 1, top + top_spacing - 1, 1, bot_spacing + 1, colour, bold );
+		ui_fill_rect( left + left_spacing + 1, top + top_spacing + 1, right_spacing - 1, 1, colour, bold );
+		ui_fill_rect( left + left_spacing + 1, top + top_spacing + 1, 1, bot_spacing - 1, colour, bold );
+		return;
+	}
+
+	if( uint8_t( c ) == 205 ) { // double horizontal
+		ui_fill_rect( left, top + top_spacing - 1, fw, 1, colour, bold );
+		ui_fill_rect( left, top + top_spacing + 1, fw, 1, colour, bold );
+		return;
+	}
+
+	if( uint8_t( c ) == 217 ) { // bottom right
+		ui_fill_rect( left, top + top_spacing, right_spacing, 1, colour, bold );
+		ui_fill_rect( left + left_spacing, top, 1, top_spacing, colour, bold );
+		return;
+	}
+
+	if( uint8_t( c ) == 218 ) { // top left
+		ui_fill_rect( left + left_spacing, top + top_spacing, right_spacing, 1, colour, bold );
+		ui_fill_rect( left + left_spacing, top + top_spacing, 1, bot_spacing, colour, bold );
+		return;
+	}
+
+	platform_draw_char( left, top, c, colour, bold, force_bold_font );
+	platform_make_dirty( left, top, fw, line_height );
+}
+
+void ui_clear_status() {
+	statusLen = 0;
+	status_dirty = true;
+}
+
+// TODO: just cap this at like 8k
+void ui_statusAdd( const char c, const Colour fg, const bool bold ) {
+	if( ( statusLen + 1 ) * sizeof( StatusChar ) > statusCapacity ) {
+		size_t newcapacity = statusCapacity * 2;
+		StatusChar * newcontents = ( StatusChar * ) realloc( statusContents, newcapacity );
+		if( !newcontents )
+			FATAL( "REALLOC" );
+
+		statusContents = newcontents;
+		statusCapacity = newcapacity;
+	}
+
+	statusContents[ statusLen ] = ( StatusChar ) { c, fg, bold };
+	statusLen++;
+	status_dirty = true;
+}
+
+void ui_draw_status() {
+	int fw, fh;
+	ui_get_font_size( &fw, &fh );
+
+	ui_fill_rect( 0, window_height - PADDING * 4 - fh * 2, window_width, fh + PADDING * 2, COLOUR_STATUSBG, false );
+
+	for( size_t i = 0; i < statusLen; i++ ) {
+		StatusChar sc = statusContents[ i ];
+
+		int x = PADDING + i * fw;
+		int y = window_height - ( PADDING * 3 ) - fh * 2 - SPACING;
+		ui_draw_char( x, y, sc.c, sc.fg, sc.bold );
+	}
+
+	status_dirty = false;
+}
+
+void ui_redraw_dirty() {
+	if( main_text.dirty )
+		textbox_draw( &main_text );
+	if( chat_text.dirty )
+		textbox_draw( &chat_text );
+	if( input_is_dirty() )
+		input_draw();
+	if( status_dirty )
+		ui_draw_status();
+}
+
+void ui_redraw_everything() {
+	int fw, fh;
+	ui_get_font_size( &fw, &fh );
+
+	ui_fill_rect( 0, 0, window_width, window_height, COLOUR_BG, false );
+
+	input_draw();
+	ui_draw_status();
+
+	textbox_draw( &chat_text );
+	textbox_draw( &main_text );
+
+	int spacerY = ( 2 * PADDING ) + ( fh + SPACING ) * CHAT_ROWS;
+	ui_fill_rect( 0, spacerY, window_width, 1, COLOUR_STATUSBG, false );
+}
+
+void ui_main_newline() {
+	textbox_newline( &main_text );
+}
+
+void ui_main_print( const char * str, size_t len, Colour fg, Colour bg, bool bold ) {
+	textbox_add( &main_text, str, len, fg, bg, bold );
+}
+
+void ui_chat_newline() {
+	textbox_newline( &chat_text );
+}
+
+void ui_chat_print( const char * str, size_t len, Colour fg, Colour bg, bool bold ) {
+	textbox_add( &chat_text, str, len, fg, bg, bold );
+}
+
+void ui_resize( int width, int height ) {
+	int fw, fh;
+	ui_get_font_size( &fw, &fh );
+
+	int old_width = window_width;
+	int old_height = window_height;
+
+	window_width = width;
+	window_height = height;
+
+	if( window_width == old_width && window_height == old_height )
+		return;
+
+	textbox_set_pos( &chat_text, PADDING, PADDING );
+	textbox_set_size( &chat_text, window_width - ( 2 * PADDING ), ( fh + SPACING ) * CHAT_ROWS );
+
+	textbox_set_pos( &main_text, PADDING, ( PADDING * 2 ) + CHAT_ROWS * ( fh + SPACING ) + 1 );
+	textbox_set_size( &main_text, window_width - ( 2 * PADDING ), window_height
+		- ( ( ( fh + SPACING ) * CHAT_ROWS ) + ( PADDING * 2 ) )
+		- ( ( fh * 2 ) + ( PADDING * 5 ) ) - 1
+	);
+
+	input_set_pos( PADDING, window_height - PADDING - fh );
+	input_set_size( window_width - PADDING * 2, fh );
+}
+
+void ui_scroll( int offset ) {
+	textbox_scroll( &main_text, offset );
+}
+
+void ui_page_down() {
+	textbox_page_down( &main_text );
+}
+
+void ui_page_up() {
+	textbox_page_up( &main_text );
+}
+
+void ui_mouse_down( int x, int y ) {
+	textbox_mouse_down( &main_text, x, y );
+	textbox_mouse_down( &chat_text, x, y );
+}
+
+void ui_mouse_up( int x, int y ) {
+	textbox_mouse_up( &main_text, x, y );
+	textbox_mouse_up( &chat_text, x, y );
+}
+
+void ui_mouse_move( int x, int y ) {
+	textbox_mouse_move( &main_text, x, y );
+	textbox_mouse_move( &chat_text, x, y );
+}
diff --git a/src/ui.h b/src/ui.h
@@ -33,6 +33,19 @@ void ui_chat_print( const char * str, size_t len, Colour fg, Colour bg, bool bol
 void ui_fill_rect( int left, int top, int width, int height, Colour colour, bool bold );
 void ui_draw_char( int left, int top, char c, Colour colour, bool bold, bool force_bold_font = false );
 
+void ui_redraw_dirty();
+void ui_redraw_everything();
+
+void ui_resize( int width, int height );
+
+void ui_scroll( int offset );
+void ui_page_down();
+void ui_page_up();
+
+void ui_mouse_down( int x, int y );
+void ui_mouse_move( int x, int y );
+void ui_mouse_up( int x, int y );
+
 void ui_get_font_size( int * fw, int * fh );
 
 void ui_urgent();
diff --git a/src/win32.cc b/src/win32.cc
@@ -24,8 +24,6 @@ struct {
 
 	int width, height;
 	int max_width, max_height;
-
-	bool has_focus;
 } UI;
 
 struct Socket {
@@ -156,267 +154,23 @@ static COLORREF get_colour( Colour colour, bool bold ) {
 	return Style.colours[ bold ][ colour ];
 }
 
-static void make_dirty( int left, int top, int width, int height ) {
-	RECT r = { left, top, left + width, top + height };
-	InvalidateRect( UI.hwnd, &r, FALSE );
-}
-
-void ui_fill_rect( int left, int top, int width, int height, Colour colour, bool bold ) {
+void platform_fill_rect( int left, int top, int width, int height, Colour colour, bool bold ) {
+	// TODO: preallocate these
 	HBRUSH brush = CreateSolidBrush( get_colour( colour, bold ) );
 	RECT r = { left, top, left + width, top + height };
 	FillRect( UI.back_buffer, &r, brush );
 	DeleteObject( brush );
-	make_dirty( left, top, width, height );
 }
 
-void ui_draw_char( int left, int top, char c, Colour colour, bool bold, bool force_bold_font ) {
-	int left_spacing = Style.font.width / 2;
-	int right_spacing = Style.font.width - left_spacing;
-	int line_height = Style.font.height + SPACING;
-	int top_spacing = line_height / 2;
-	int bot_spacing = line_height - top_spacing;
-
-	// TODO: not the right char...
-	// if( uint8_t( c ) == 155 ) { // fill
-	// 	ui_fill_rect( left, top, Style.font.width, Style.font.height, colour, bold );
-	// 	return;
-	// }
-
-	// TODO: this has a vertical seam. using textbox-space coordinates would help
-	if( uint8_t( c ) == 176 ) { // light shade
-		for( int y = 0; y < Style.font.height; y += 3 ) {
-			for( int x = y % 6 == 0 ? 0 : 1; x < Style.font.width; x += 2 ) {
-				ui_fill_rect( left + x, top + y, 1, 1, colour, bold );
-			}
-		}
-		return;
-	}
-
-	// TODO: this has a horizontal seam but so does mm2k
-	if( uint8_t( c ) == 177 ) { // medium shade
-		for( int y = 0; y < Style.font.height; y += 2 ) {
-			for( int x = y % 4 == 0 ? 1 : 0; x < Style.font.width; x += 2 ) {
-				ui_fill_rect( left + x, top + y, 1, 1, colour, bold );
-			}
-		}
-		return;
-	}
-
-	// TODO: this probably has a horizontal seam
-	if( uint8_t( c ) == 178 ) { // heavy shade
-		for( int y = 0; y < Style.font.height + SPACING; y++ ) {
-			for( int x = y % 2 == 0 ? 1 : 0; x < Style.font.width; x += 2 ) {
-				ui_fill_rect( left + x, top + y, 1, 1, colour, bold );
-			}
-		}
-		return;
-	}
-
-	if( uint8_t( c ) == 179 ) { // vertical
-		ui_fill_rect( left + left_spacing, top, 1, line_height, colour, bold );
-		return;
-		// set_fg( colour, bold );
-		// const char asdf[] = "│";
-		// Xutf8DrawString( UI.display, UI.back_buffer, ( bold ? Style.fontBold : Style.font ).font, UI.gc, left, top + Style.font.ascent + SPACING, asdf, sizeof( asdf ) - 1 );
-	}
-
-	if( uint8_t( c ) == 180 ) { // right stopper
-		ui_fill_rect( left, top + top_spacing, left_spacing, 1, colour, bold );
-		ui_fill_rect( left + left_spacing, top, 1, line_height, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 186 ) { // double vertical
-		ui_fill_rect( left + left_spacing - 1, top, 1, line_height, colour, bold );
-		ui_fill_rect( left + left_spacing + 1, top, 1, line_height, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 187 ) { // double top right
-		ui_fill_rect( left, top + top_spacing - 1, right_spacing + 1, 1, colour, bold );
-		ui_fill_rect( left + left_spacing + 1, top + top_spacing - 1, 1, bot_spacing + 1, colour, bold );
-		ui_fill_rect( left, top + top_spacing + 1, right_spacing - 1, 1, colour, bold );
-		ui_fill_rect( left + left_spacing - 1, top + top_spacing + 1, 1, bot_spacing - 1, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 188 ) { // double bottom right
-		ui_fill_rect( left, top + top_spacing + 1, right_spacing + 1, 1, colour, bold );
-		ui_fill_rect( left + left_spacing + 1, top, 1, top_spacing + 1, colour, bold );
-		ui_fill_rect( left, top + top_spacing - 1, right_spacing - 1, 1, colour, bold );
-		ui_fill_rect( left + left_spacing - 1, top, 1, top_spacing - 1, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 191 ) { // top right
-		ui_fill_rect( left, top + top_spacing, left_spacing, 1, colour, bold );
-		ui_fill_rect( left + left_spacing, top + top_spacing, 1, bot_spacing, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 192 ) { // bottom left
-		ui_fill_rect( left + left_spacing, top + top_spacing, right_spacing, 1, colour, bold );
-		ui_fill_rect( left + left_spacing, top, 1, top_spacing, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 193 ) { // bottom stopper
-		ui_fill_rect( left + left_spacing, top, 1, top_spacing, colour, bold );
-		ui_fill_rect( left, top + top_spacing, Style.font.width, 1, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 194 ) { // top stopper
-		ui_fill_rect( left + left_spacing, top + top_spacing, 1, bot_spacing, colour, bold );
-		ui_fill_rect( left, top + top_spacing, Style.font.width, 1, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 195 ) { // left stopper
-		ui_fill_rect( left + left_spacing, top + top_spacing, right_spacing, 1, colour, bold );
-		ui_fill_rect( left + left_spacing, top, 1, line_height, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 196 ) { // horizontal
-		ui_fill_rect( left, top + top_spacing, Style.font.width, 1, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 197 ) { // cross
-		ui_fill_rect( left, top + top_spacing, Style.font.width, 1, colour, bold );
-		ui_fill_rect( left + left_spacing, top, 1, line_height, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 200 ) { // double bottom left
-		ui_fill_rect( left + left_spacing - 1, top + top_spacing + 1, right_spacing + 1, 1, colour, bold );
-		ui_fill_rect( left + left_spacing - 1, top, 1, top_spacing + 1, colour, bold );
-		ui_fill_rect( left + left_spacing + 1, top + top_spacing - 1, right_spacing - 1, 1, colour, bold );
-		ui_fill_rect( left + left_spacing + 1, top, 1, top_spacing - 1, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 201 ) { // double top left
-		ui_fill_rect( left + left_spacing - 1, top + top_spacing - 1, right_spacing + 1, 1, colour, bold );
-		ui_fill_rect( left + left_spacing - 1, top + top_spacing - 1, 1, bot_spacing + 1, colour, bold );
-		ui_fill_rect( left + left_spacing + 1, top + top_spacing + 1, right_spacing - 1, 1, colour, bold );
-		ui_fill_rect( left + left_spacing + 1, top + top_spacing + 1, 1, bot_spacing - 1, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 205 ) { // double horizontal
-		ui_fill_rect( left, top + top_spacing - 1, Style.font.width, 1, colour, bold );
-		ui_fill_rect( left, top + top_spacing + 1, Style.font.width, 1, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 217 ) { // bottom right
-		ui_fill_rect( left, top + top_spacing, right_spacing, 1, colour, bold );
-		ui_fill_rect( left + left_spacing, top, 1, top_spacing, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 218 ) { // top left
-		ui_fill_rect( left + left_spacing, top + top_spacing, right_spacing, 1, colour, bold );
-		ui_fill_rect( left + left_spacing, top + top_spacing, 1, bot_spacing, colour, bold );
-		return;
-	}
-
+void platform_draw_char( int left, int top, char c, Colour colour, bool bold, bool force_bold_font ) {
 	SelectObject( UI.back_buffer, ( bold || force_bold_font ? Style.font.bold : Style.font.regular ) );
 	SetTextColor( UI.back_buffer, get_colour( colour, bold ) );
 	TextOutA( UI.back_buffer, left, top + SPACING, &c, 1 );
-
-	make_dirty( left, top, Style.font.width, Style.font.height + SPACING );
-}
-
-typedef struct {
-	char c;
-
-	Colour fg;
-	bool bold;
-} StatusChar;
-
-static StatusChar * statusContents = NULL;
-static size_t statusCapacity = 256;
-static size_t statusLen = 0;
-
-void ui_clear_status() {
-	statusLen = 0;
 }
 
-void ui_statusAdd( const char c, const Colour fg, const bool bold ) {
-	if( ( statusLen + 1 ) * sizeof( StatusChar ) > statusCapacity ) {
-		size_t newcapacity = statusCapacity * 2;
-		StatusChar * newcontents = ( StatusChar * ) realloc( statusContents, newcapacity );
-		if( !newcontents )
-			FATAL( "realloc" );
-
-		statusContents = newcontents;
-		statusCapacity = newcapacity;
-	}
-
-	statusContents[ statusLen ] = { c, fg, bold };
-	statusLen++;
-}
-
-void ui_draw_status() {
-	ui_fill_rect( 0, UI.height - PADDING * 4 - Style.font.height * 2, UI.width, Style.font.height + PADDING * 2, COLOUR_STATUSBG, false );
-
-	for( size_t i = 0; i < statusLen; i++ ) {
-		StatusChar sc = statusContents[ i ];
-
-		int x = PADDING + i * Style.font.width;
-		int y = UI.height - ( PADDING * 3 ) - Style.font.height * 2 - SPACING;
-		ui_draw_char( x, y, sc.c, sc.fg, sc.bold );
-	}
-}
-
-void draw_input() {
-	InputBuffer input = input_get_buffer();
-
-	int top = UI.height - PADDING - Style.font.height;
-	ui_fill_rect( PADDING, top, UI.width - PADDING * 2, Style.font.height, COLOUR_BG, false );
-
-	for( size_t i = 0; i < input.len; i++ )
-		ui_draw_char( PADDING + i * Style.font.width, top - SPACING, input.buf[ i ], WHITE, false );
-
-	ui_fill_rect( PADDING + input.cursor_pos * Style.font.width, top, Style.font.width, Style.font.height, COLOUR_CURSOR, false );
-
-	if( input.cursor_pos < input.len ) {
-		ui_draw_char( PADDING + input.cursor_pos * Style.font.width, top - SPACING, input.buf[ input.cursor_pos ], COLOUR_BG, false );
-	}
-}
-
-void ui_draw() {
-	ui_fill_rect( 0, 0, UI.width, UI.height, COLOUR_BG, false );
-
-	draw_input();
-	ui_draw_status();
-
-	textbox_draw( &UI.chat_text );
-	textbox_draw( &UI.main_text );
-
-	int spacerY = ( 2 * PADDING ) + ( Style.font.height + SPACING ) * CHAT_ROWS;
-	ui_fill_rect( 0, spacerY, UI.width, 1, COLOUR_STATUSBG, false );
-}
-
-void ui_handleXEvents() { }
-
-void ui_main_newline() {
-	textbox_newline( &UI.main_text );
-}
-
-void ui_main_print( const char * str, size_t len, Colour fg, Colour bg, bool bold ) {
-	textbox_add( &UI.main_text, str, len, fg, bg, bold );
-}
-
-void ui_chat_newline() {
-	textbox_newline( &UI.chat_text );
-}
-
-void ui_chat_print( const char * str, size_t len, Colour fg, Colour bg, bool bold ) {
-	textbox_add( &UI.chat_text, str, len, fg, bg, bold );
+void platform_make_dirty( int left, int top, int width, int height ) {
+	RECT r = { left, top, left + width, top + height };
+	InvalidateRect( UI.hwnd, &r, FALSE );
 }
 
 void ui_get_font_size( int * fw, int * fh ) {
@@ -502,14 +256,8 @@ LRESULT CALLBACK WndProc( HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam ) {
 		} break;
 
 		case WM_SIZE: {
-			int old_width = UI.width;
-			int old_height = UI.height;
-
-			UI.width = LOWORD( lParam );
-			UI.height = HIWORD( lParam );
-
-			if( UI.width == old_width && UI.height == old_height )
-				break;
+			int width = LOWORD( lParam );
+			int height = HIWORD( lParam );
 
 			int old_max_width = UI.max_width;
 			int old_max_height = UI.max_height;
@@ -525,16 +273,8 @@ LRESULT CALLBACK WndProc( HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam ) {
 				SelectObject( UI.back_buffer, UI.back_buffer_bitmap );
 			}
 
-			textbox_set_pos( &UI.chat_text, PADDING, PADDING );
-			textbox_set_size( &UI.chat_text, UI.width - ( 2 * PADDING ), ( Style.font.height + SPACING ) * CHAT_ROWS );
-
-			textbox_set_pos( &UI.main_text, PADDING, ( PADDING * 2 ) + CHAT_ROWS * ( Style.font.height + SPACING ) + 1 );
-			textbox_set_size( &UI.main_text, UI.width - ( 2 * PADDING ), UI.height
-				- ( ( ( Style.font.height + SPACING ) * CHAT_ROWS ) + ( PADDING * 2 ) )
-				- ( ( Style.font.height * 2 ) + ( PADDING * 5 ) ) - 1
-			);
-
-			ui_draw();
+			ui_resized( width, height );
+			ui_redraw();
 		} break;
 
 		case WM_PAINT: {
@@ -550,28 +290,24 @@ LRESULT CALLBACK WndProc( HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam ) {
 			return TRUE;
 
 		case WM_LBUTTONDOWN: {
-			// char szFileName[MAX_PATH];
-			// HINSTANCE hInstance = GetModuleHandle(NULL);
-                        //
-			// GetModuleFileName(hInstance, szFileName, MAX_PATH);
-			// MessageBox(hwnd, szFileName, "This program is:", MB_OK | MB_ICONINFORMATION);
+			ui_mouse_down( GET_X_LPARAM( lParam ), GET_Y_LPARAM( lParam ) );
 		} break;
 
 		case WM_MOUSEMOVE: {
-			// char buf[ 128 ];
-			// int x = GET_X_LPARAM( lParam );
-			// int y = GET_Y_LPARAM( lParam );
-			// int l = snprintf( buf, sizeof( buf ), "what the fuck son %d %d", x, y );
-			// TextOutA( dc, 10, 10, buf, l );
+			ui_mouse_move( GET_X_LPARAM( lParam ), GET_Y_LPARAM( lParam ) );
+			break;
+
+		case WM_LBUTTONUP: {
+			ui_mouse_up( GET_X_LPARAM( lParam ), GET_Y_LPARAM( lParam ) );
 		} break;
 
-		case WM_CLOSE:
+		case WM_CLOSE: {
 			DestroyWindow( hwnd );
-			break;
+		} break;
 
-		case WM_DESTROY:
+		case WM_DESTROY: {
 			PostQuitMessage( 0 );
-			break;
+		} break;
 
 		case WM_TIMER: {
 			script_fire_intervals();
@@ -725,7 +461,7 @@ LRESULT CALLBACK WndProc( HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam ) {
 		} break;
 
 		default:
-			return DefWindowProc(hwnd, msg, wParam, lParam);
+			return DefWindowProc( hwnd, msg, wParam, lParam );
 	}
 
 	if( UI.main_text.dirty )
diff --git a/src/x11.cc b/src/x11.cc
@@ -85,10 +85,6 @@ struct {
 
 	Window window;
 
-	TextBox main_text;
-	TextBox chat_text;
-
-	int width, height;
 	int max_width, max_height;
 	int depth;
 
@@ -167,7 +163,7 @@ static void set_fg( Colour colour, bool bold ) {
 	XSetForeground( UI.display, UI.gc, c );
 }
 
-static void make_dirty( int left, int top, int width, int height ) {
+void platform_make_dirty( int left, int top, int width, int height ) {
 	int right = left + width;
 	int bottom = top + height;
 
@@ -186,332 +182,29 @@ static void make_dirty( int left, int top, int width, int height ) {
 	}
 }
 
-void ui_fill_rect( int left, int top, int width, int height, Colour colour, bool bold ) {
+void platform_fill_rect( int left, int top, int width, int height, Colour colour, bool bold ) {
 	set_fg( colour, bold );
 	XFillRectangle( UI.display, UI.back_buffer, UI.gc, left, top, width, height );
-	make_dirty( left, top, width, height );
 }
 
-void ui_draw_char( int left, int top, char c, Colour colour, bool bold, bool force_bold_font ) {
-	int left_spacing = Style.font.width / 2;
-	int right_spacing = Style.font.width - left_spacing;
-	int line_height = Style.font.height + SPACING;
-	int top_spacing = line_height / 2;
-	int bot_spacing = line_height - top_spacing;
-
-	// TODO: not the right char...
-	// if( uint8_t( c ) == 155 ) { // fill
-	// 	ui_fill_rect( left, top, Style.font.width, Style.font.height, colour, bold );
-	// 	return;
-	// }
-
-	// TODO: this has a vertical seam. using textbox-space coordinates would help
-	if( uint8_t( c ) == 176 ) { // light shade
-		for( int y = 0; y < Style.font.height; y += 3 ) {
-			for( int x = y % 6 == 0 ? 0 : 1; x < Style.font.width; x += 2 ) {
-				ui_fill_rect( left + x, top + y, 1, 1, colour, bold );
-			}
-		}
-		return;
-	}
-
-	// TODO: this has a horizontal seam but so does mm2k
-	if( uint8_t( c ) == 177 ) { // medium shade
-		for( int y = 0; y < Style.font.height; y += 2 ) {
-			for( int x = y % 4 == 0 ? 1 : 0; x < Style.font.width; x += 2 ) {
-				ui_fill_rect( left + x, top + y, 1, 1, colour, bold );
-			}
-		}
-		return;
-	}
-
-	// TODO: this probably has a horizontal seam
-	if( uint8_t( c ) == 178 ) { // heavy shade
-		for( int y = 0; y < Style.font.height + SPACING; y++ ) {
-			for( int x = y % 2 == 0 ? 1 : 0; x < Style.font.width; x += 2 ) {
-				ui_fill_rect( left + x, top + y, 1, 1, colour, bold );
-			}
-		}
-		return;
-	}
-
-	if( uint8_t( c ) == 179 ) { // vertical
-		ui_fill_rect( left + left_spacing, top, 1, line_height, colour, bold );
-		return;
-		// set_fg( colour, bold );
-		// const char asdf[] = "│";
-		// Xutf8DrawString( UI.display, UI.back_buffer, ( bold ? Style.fontBold : Style.font ).font, UI.gc, left, top + Style.font.ascent + SPACING, asdf, sizeof( asdf ) - 1 );
-	}
-
-	if( uint8_t( c ) == 180 ) { // right stopper
-		ui_fill_rect( left, top + top_spacing, left_spacing, 1, colour, bold );
-		ui_fill_rect( left + left_spacing, top, 1, line_height, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 186 ) { // double vertical
-		ui_fill_rect( left + left_spacing - 1, top, 1, line_height, colour, bold );
-		ui_fill_rect( left + left_spacing + 1, top, 1, line_height, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 187 ) { // double top right
-		ui_fill_rect( left, top + top_spacing - 1, right_spacing + 1, 1, colour, bold );
-		ui_fill_rect( left + left_spacing + 1, top + top_spacing - 1, 1, bot_spacing + 1, colour, bold );
-		ui_fill_rect( left, top + top_spacing + 1, right_spacing - 1, 1, colour, bold );
-		ui_fill_rect( left + left_spacing - 1, top + top_spacing + 1, 1, bot_spacing - 1, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 188 ) { // double bottom right
-		ui_fill_rect( left, top + top_spacing + 1, right_spacing + 1, 1, colour, bold );
-		ui_fill_rect( left + left_spacing + 1, top, 1, top_spacing + 1, colour, bold );
-		ui_fill_rect( left, top + top_spacing - 1, right_spacing - 1, 1, colour, bold );
-		ui_fill_rect( left + left_spacing - 1, top, 1, top_spacing - 1, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 191 ) { // top right
-		ui_fill_rect( left, top + top_spacing, left_spacing, 1, colour, bold );
-		ui_fill_rect( left + left_spacing, top + top_spacing, 1, bot_spacing, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 192 ) { // bottom left
-		ui_fill_rect( left + left_spacing, top + top_spacing, right_spacing, 1, colour, bold );
-		ui_fill_rect( left + left_spacing, top, 1, top_spacing, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 193 ) { // bottom stopper
-		ui_fill_rect( left + left_spacing, top, 1, top_spacing, colour, bold );
-		ui_fill_rect( left, top + top_spacing, Style.font.width, 1, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 194 ) { // top stopper
-		ui_fill_rect( left + left_spacing, top + top_spacing, 1, bot_spacing, colour, bold );
-		ui_fill_rect( left, top + top_spacing, Style.font.width, 1, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 195 ) { // left stopper
-		ui_fill_rect( left + left_spacing, top + top_spacing, right_spacing, 1, colour, bold );
-		ui_fill_rect( left + left_spacing, top, 1, line_height, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 196 ) { // horizontal
-		ui_fill_rect( left, top + top_spacing, Style.font.width, 1, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 197 ) { // cross
-		ui_fill_rect( left, top + top_spacing, Style.font.width, 1, colour, bold );
-		ui_fill_rect( left + left_spacing, top, 1, line_height, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 200 ) { // double bottom left
-		ui_fill_rect( left + left_spacing - 1, top + top_spacing + 1, right_spacing + 1, 1, colour, bold );
-		ui_fill_rect( left + left_spacing - 1, top, 1, top_spacing + 1, colour, bold );
-		ui_fill_rect( left + left_spacing + 1, top + top_spacing - 1, right_spacing - 1, 1, colour, bold );
-		ui_fill_rect( left + left_spacing + 1, top, 1, top_spacing - 1, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 201 ) { // double top left
-		ui_fill_rect( left + left_spacing - 1, top + top_spacing - 1, right_spacing + 1, 1, colour, bold );
-		ui_fill_rect( left + left_spacing - 1, top + top_spacing - 1, 1, bot_spacing + 1, colour, bold );
-		ui_fill_rect( left + left_spacing + 1, top + top_spacing + 1, right_spacing - 1, 1, colour, bold );
-		ui_fill_rect( left + left_spacing + 1, top + top_spacing + 1, 1, bot_spacing - 1, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 205 ) { // double horizontal
-		ui_fill_rect( left, top + top_spacing - 1, Style.font.width, 1, colour, bold );
-		ui_fill_rect( left, top + top_spacing + 1, Style.font.width, 1, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 217 ) { // bottom right
-		ui_fill_rect( left, top + top_spacing, right_spacing, 1, colour, bold );
-		ui_fill_rect( left + left_spacing, top, 1, top_spacing, colour, bold );
-		return;
-	}
-
-	if( uint8_t( c ) == 218 ) { // top left
-		ui_fill_rect( left + left_spacing, top + top_spacing, right_spacing, 1, colour, bold );
-		ui_fill_rect( left + left_spacing, top + top_spacing, 1, bot_spacing, colour, bold );
-		return;
-	}
-
+void platform_draw_char( int left, int top, char c, Colour colour, bool bold, bool force_bold_font ) {
 	XSetFont( UI.display, UI.gc, ( bold || force_bold_font ? Style.font.bold : Style.font.regular )->fid );
 	set_fg( colour, bold );
 	XDrawString( UI.display, UI.back_buffer, UI.gc, left, top + Style.font.ascent + SPACING, &c, 1 );
-
-	make_dirty( left, top, Style.font.width, Style.font.height + SPACING );
 }
 
 static Atom wmDeleteWindow;
 
-typedef struct {
-	char c;
-
-	Colour fg;
-	bool bold;
-} StatusChar;
-
-static StatusChar * statusContents = NULL;
-static size_t statusCapacity = 256;
-static size_t statusLen = 0;
-
-void ui_clear_status() {
-	statusLen = 0;
-}
-
-void ui_statusAdd( const char c, const Colour fg, const bool bold ) {
-	if( ( statusLen + 1 ) * sizeof( StatusChar ) > statusCapacity ) {
-		size_t newcapacity = statusCapacity * 2;
-		StatusChar * newcontents = ( StatusChar * ) realloc( statusContents, newcapacity );
-		if( !newcontents )
-			err( 1, "realloc" );
-
-		statusContents = newcontents;
-		statusCapacity = newcapacity;
-	}
-
-	statusContents[ statusLen ] = ( StatusChar ) { c, fg, bold };
-	statusLen++;
-}
-
-void ui_draw_status() {
-	ui_fill_rect( 0, UI.height - PADDING * 4 - Style.font.height * 2, UI.width, Style.font.height + PADDING * 2, COLOUR_STATUSBG, false );
-
-	for( size_t i = 0; i < statusLen; i++ ) {
-		StatusChar sc = statusContents[ i ];
-
-		int x = PADDING + i * Style.font.width;
-		int y = UI.height - ( PADDING * 3 ) - Style.font.height * 2 - SPACING;
-		ui_draw_char( x, y, sc.c, sc.fg, sc.bold );
-	}
-}
-
-void draw_input() {
-	InputBuffer input = input_get_buffer();
-
-	int top = UI.height - PADDING - Style.font.height;
-	ui_fill_rect( PADDING, top, UI.width - PADDING * 2, Style.font.height, COLOUR_BG, false );
-
-	for( size_t i = 0; i < input.len; i++ )
-		ui_draw_char( PADDING + i * Style.font.width, top - SPACING, input.buf[ i ], WHITE, false );
-
-	ui_fill_rect( PADDING + input.cursor_pos * Style.font.width, top, Style.font.width, Style.font.height, COLOUR_CURSOR, false );
-
-	if( input.cursor_pos < input.len ) {
-		ui_draw_char( PADDING + input.cursor_pos * Style.font.width, top - SPACING, input.buf[ input.cursor_pos ], COLOUR_BG, false );
-	}
-}
-
-void ui_draw() {
-	ui_fill_rect( 0, 0, UI.width, UI.height, COLOUR_BG, false );
-
-	draw_input();
-	ui_draw_status();
-
-	textbox_draw( &UI.chat_text );
-	textbox_draw( &UI.main_text );
-
-	int spacerY = ( 2 * PADDING ) + ( Style.font.height + SPACING ) * CHAT_ROWS;
-	ui_fill_rect( 0, spacerY, UI.width, 1, COLOUR_STATUSBG, false );
-}
-
 static void event_mouse_down( XEvent * xevent ) {
-	const XButtonEvent * event = &xevent->xbutton;
-
-	if( event->x >= UI.main_text.x && event->x < UI.main_text.x + UI.main_text.w && event->y >= UI.main_text.y && event->y < UI.main_text.y + UI.main_text.h ) {
-		int x = event->x - UI.main_text.x;
-		int y = event->y - UI.main_text.y;
-
-		int my = UI.main_text.h - y;
-
-		int row = my / ( Style.font.height + SPACING );
-		int col = x / Style.font.width;
-
-		UI.main_text.selecting = true;
-		UI.main_text.selection_start_col = col;
-		UI.main_text.selection_start_row = row;
-		UI.main_text.selection_end_col = col;
-		UI.main_text.selection_end_row = row;
-
-		textbox_draw( &UI.main_text );
-	}
-
-	if( event->x >= UI.chat_text.x && event->x < UI.chat_text.x + UI.chat_text.w && event->y >= UI.chat_text.y && event->y < UI.chat_text.y + UI.chat_text.h ) {
-		int x = event->x - UI.chat_text.x;
-		int y = event->y - UI.chat_text.y;
-
-		int my = UI.chat_text.h - y;
-
-		int row = my / ( Style.font.height + SPACING );
-		int col = x / Style.font.width;
-
-		UI.chat_text.selecting = true;
-		UI.chat_text.selection_start_col = col;
-		UI.chat_text.selection_start_row = row;
-		UI.chat_text.selection_end_col = col;
-		UI.chat_text.selection_end_row = row;
-
-		textbox_draw( &UI.chat_text );
-	}
-}
-
-static void event_mouse_up( XEvent * xevent ) {
-	const XButtonEvent * event = &xevent->xbutton;
-
-	if( UI.main_text.selecting ) {
-		UI.main_text.selecting = false;
-		textbox_draw( &UI.main_text );
-	}
-
-	if( UI.chat_text.selecting ) {
-		UI.chat_text.selecting = false;
-		textbox_draw( &UI.chat_text );
-	}
+	ui_mouse_down( xevent->xbutton.x, xevent->xbutton.y );
 }
 
 static void event_mouse_move( XEvent * xevent ) {
-	const XMotionEvent * event = &xevent->xmotion;
-
-	if( UI.main_text.selecting ) {
-		int x = event->x - UI.main_text.x;
-		int y = event->y - UI.main_text.y;
-
-		int my = UI.main_text.h - y;
-
-		int row = my / ( Style.font.height + SPACING );
-		int col = x / Style.font.width;
-
-		UI.main_text.selection_end_col = col;
-		UI.main_text.selection_end_row = row;
-
-		textbox_draw( &UI.main_text );
-	}
-
-	if( UI.chat_text.selecting ) {
-		int x = event->x - UI.chat_text.x;
-		int y = event->y - UI.chat_text.y;
-
-		int my = UI.chat_text.h - y;
-
-		int row = my / ( Style.font.height + SPACING );
-		int col = x / Style.font.width;
-
-		UI.chat_text.selection_end_col = col;
-		UI.chat_text.selection_end_row = row;
+	ui_mouse_move( xevent->xmotion.x, xevent->xmotion.y );
+}
 
-		textbox_draw( &UI.chat_text );
-	}
+static void event_mouse_up( XEvent * xevent ) {
+	ui_mouse_up( xevent->xbutton.x, xevent->xbutton.y );
 }
 
 static void event_message( XEvent * xevent ) {
@@ -522,40 +215,27 @@ static void event_message( XEvent * xevent ) {
 }
 
 static void event_resize( XEvent * xevent ) {
-	int old_width = UI.width;
-	int old_height = UI.height;
-
-	UI.width = xevent->xconfigure.width;
-	UI.height = xevent->xconfigure.height;
-
-	if( UI.width == old_width && UI.height == old_height )
-		return;
+	int width = xevent->xconfigure.width;
+	int height = xevent->xconfigure.height;
 
 	int old_max_width = UI.max_width;
 	int old_max_height = UI.max_height;
 
-	UI.max_width = max( UI.max_width, UI.width );
-	UI.max_height = max( UI.max_height, UI.height );
+	UI.max_width = max( UI.max_width, width );
+	UI.max_height = max( UI.max_height, height );
 
 	if( UI.max_width != old_max_width || UI.max_height != old_max_height ) {
-		if( old_width != -1 ) {
+		if( old_max_width != -1 ) {
 			XFreePixmap( UI.display, UI.back_buffer );
 		}
 		UI.back_buffer = XCreatePixmap( UI.display, UI.window, UI.max_width, UI.max_height, UI.depth );
 	}
 
-	textbox_set_pos( &UI.chat_text, PADDING, PADDING );
-	textbox_set_size( &UI.chat_text, UI.width - ( 2 * PADDING ), ( Style.font.height + SPACING ) * CHAT_ROWS );
-
-	textbox_set_pos( &UI.main_text, PADDING, ( PADDING * 2 ) + CHAT_ROWS * ( Style.font.height + SPACING ) + 1 );
-	textbox_set_size( &UI.main_text, UI.width - ( 2 * PADDING ), UI.height
-		- ( ( ( Style.font.height + SPACING ) * CHAT_ROWS ) + ( PADDING * 2 ) )
-		- ( ( Style.font.height * 2 ) + ( PADDING * 5 ) ) - 1
-	);
+	ui_resize( width, height );
 }
 
 static void event_expose( XEvent * xevent ) {
-	ui_draw();
+	ui_redraw_everything();
 }
 
 static void event_key_press( XEvent * xevent ) {
@@ -582,53 +262,44 @@ static void event_key_press( XEvent * xevent ) {
 	switch( key ) {
 		case XK_Return:
 			input_return();
-			draw_input();
 			break;
 
 		case XK_BackSpace:
 			input_backspace();
-			draw_input();
 			break;
 
 		case XK_Delete:
 			input_delete();
-			draw_input();
 			break;
 
 		case XK_Page_Up:
 			if( shift )
-				textbox_scroll( &UI.main_text, 1 );
+				ui_scroll( 1 );
 			else
-				textbox_page_up( &UI.main_text );
-			textbox_draw( &UI.main_text );
+				ui_page_up();
 			break;
 
 		case XK_Page_Down:
 			if( shift )
-				textbox_scroll( &UI.main_text, -1 );
+				ui_scroll( -1 );
 			else
-				textbox_page_down( &UI.main_text );
-			textbox_draw( &UI.main_text );
+				ui_page_down();
 			break;
 
 		case XK_Up:
 			input_up();
-			draw_input();
 			break;
 
 		case XK_Down:
 			input_down();
-			draw_input();
 			break;
 
 		case XK_Left:
 			input_left();
-			draw_input();
 			break;
 
 		case XK_Right:
 			input_right();
-			draw_input();
 			break;
 
 		ADD_MACRO( XK_KP_1, "kp1" );
@@ -688,7 +359,6 @@ static void event_key_press( XEvent * xevent ) {
 			}
 			else if( len > 0 ) {
 				input_add( keyBuffer, len );
-				draw_input();
 			}
 
 			break;
@@ -713,8 +383,8 @@ static void event_focus( XEvent * xevent ) {
 void ui_handleXEvents() {
 	void ( *event_handlers[ LASTEvent ] )( XEvent * ) = { };
 	event_handlers[ ButtonPress ] = event_mouse_down;
-	event_handlers[ ButtonRelease ] = event_mouse_up;
 	event_handlers[ MotionNotify ] = event_mouse_move;
+	event_handlers[ ButtonRelease ] = event_mouse_up;
 	event_handlers[ ClientMessage ] = event_message;
 	event_handlers[ ConfigureNotify ] = event_resize;
 	event_handlers[ Expose ] = event_expose;
@@ -744,10 +414,7 @@ void ui_handleXEvents() {
 			}
 		}
 
-		if( UI.main_text.dirty )
-			textbox_draw( &UI.main_text );
-		if( UI.chat_text.dirty )
-			textbox_draw( &UI.chat_text );
+		ui_redraw_dirty();
 
 		if( UI.dirty ) {
 			XCopyArea( UI.display, UI.back_buffer, UI.window, UI.gc, UI.dirty_left, UI.dirty_top, UI.dirty_right - UI.dirty_left, UI.dirty_bottom - UI.dirty_top, UI.dirty_left, UI.dirty_top );
@@ -808,7 +475,7 @@ static void initStyle() {
 	Style.font = load_font( "-windows-dina-medium-r-normal--10-*-*-*-c-0-*-*", "-windows-dina-bold-r-normal--10-*-*-*-c-0-*-*" );
 }
 
-void ui_init() {
+void platform_ui_init() {
 	for( Socket & s : sockets ) {
 		s.in_use = false;
 	}
@@ -818,22 +485,16 @@ void ui_init() {
 
 	UI = { };
 
-	textbox_init( &UI.main_text, SCROLLBACK_SIZE );
-	textbox_init( &UI.chat_text, CHAT_ROWS );
 	UI.display = XOpenDisplay( NULL );
 	UI.screen = XDefaultScreen( UI.display );
-	UI.width = -1;
-	UI.height = -1;
+	UI.max_width = -1;
+	UI.max_height = -1;
 
 	Window root = XRootWindow( UI.display, UI.screen );
 	UI.depth = XDefaultDepth( UI.display, UI.screen );
 	Visual * visual = XDefaultVisual( UI.display, UI.screen );
 	UI.colorMap = XDefaultColormap( UI.display, UI.screen );
 
-	statusContents = ( StatusChar * ) malloc( statusCapacity * sizeof( StatusChar ) );
-	if( statusContents == NULL )
-		err( 1, "malloc" );
-
 	initStyle();
 
 	XSetWindowAttributes attr = { };
@@ -864,22 +525,6 @@ void ui_init() {
 	ui_handleXEvents();
 }
 
-void ui_main_newline() {
-	textbox_newline( &UI.main_text );
-}
-
-void ui_main_print( const char * str, size_t len, Colour fg, Colour bg, bool bold ) {
-	textbox_add( &UI.main_text, str, len, fg, bg, bold );
-}
-
-void ui_chat_newline() {
-	textbox_newline( &UI.chat_text );
-}
-
-void ui_chat_print( const char * str, size_t len, Colour fg, Colour bg, bool bold ) {
-	textbox_add( &UI.chat_text, str, len, fg, bg, bold );
-}
-
 void ui_urgent() {
 	if( !UI.has_focus ) {
                 XWMHints * hints = XGetWMHints( UI.display, UI.window );
@@ -903,11 +548,7 @@ bool ui_set_font( const char * name, int size ) {
 	return false;
 }
 
-void ui_term() {
-	textbox_destroy( &UI.main_text );
-	textbox_destroy( &UI.chat_text );
-	free( statusContents );
-
+void platform_ui_term() {
 	XFreeFont( UI.display, Style.font.regular );
 	XFreeFont( UI.display, Style.font.bold );