mudgangster

Tiny, scriptable MUD client
Log | Files | Refs | README

x11.cc (15112B)


      1 #include <err.h>
      2 #include <poll.h>
      3 
      4 #include <X11/Xutil.h>
      5 #include <X11/XKBlib.h>
      6 #include <X11/cursorfont.h>
      7 #include <X11/keysym.h>
      8 
      9 #include "common.h"
     10 #include "input.h"
     11 #include "script.h"
     12 #include "ui.h"
     13 
     14 #include "platform_ui.h"
     15 #include "platform_network.h"
     16 
     17 #include "libclipboard/libclipboard.h"
     18 
     19 struct Socket {
     20 	TCPSocket sock;
     21 	bool in_use;
     22 };
     23 
     24 static Socket sockets[ 128 ];
     25 
     26 static bool closing = false;
     27 
     28 static clipboard_c * clipboard;
     29 
     30 void * platform_connect( const char ** err, const char * host, int port ) {
     31 	size_t idx;
     32 	{
     33 		bool ok = false;
     34 		for( size_t i = 0; i < ARRAY_COUNT( sockets ); i++ ) {
     35 			if( !sockets[ i ].in_use ) {
     36 				idx = i;
     37 				ok = true;
     38 				break;
     39 			}
     40 		}
     41 
     42 		if( !ok ) {
     43 			*err = "too many connections";
     44 			return NULL;
     45 		}
     46 	}
     47 
     48 	NetAddress addr;
     49 	{
     50 		bool ok = dns_first( host, &addr );
     51 		if( !ok ) {
     52 			*err = "couldn't resolve hostname"; // TODO: error from dns_first
     53 			return NULL;
     54 		}
     55 	}
     56 	addr.port = checked_cast< u16 >( port );
     57 
     58 	TCPSocket sock;
     59 	bool ok = net_new_tcp( &sock, addr, err );
     60 	if( !ok )
     61 		return NULL;
     62 
     63 	sockets[ idx ].sock = sock;
     64 	sockets[ idx ].in_use = true;
     65 
     66 	return &sockets[ idx ];
     67 }
     68 
     69 void platform_send( void * vsock, const char * data, size_t len ) {
     70 	Socket * sock = ( Socket * ) vsock;
     71 	net_send( sock->sock, data, len );
     72 }
     73 
     74 void platform_close( void * vsock ) {
     75 	Socket * sock = ( Socket * ) vsock;
     76 	net_destroy( &sock->sock );
     77 	sock->in_use = false;
     78 }
     79 
     80 struct {
     81 	Display * display;
     82 	int screen;
     83 
     84 	Pixmap back_buffer;
     85 
     86 	GC gc;
     87 	Colormap colorMap;
     88 
     89 	Window window;
     90 
     91 	int max_width, max_height;
     92 	int depth;
     93 
     94 	bool dirty;
     95 	int dirty_left, dirty_top, dirty_right, dirty_bottom;
     96 
     97 	bool has_focus;
     98 } UI;
     99 
    100 struct MudFont {
    101 	int width, height;
    102 	int ascent;
    103 	XFontStruct * regular;
    104 	XFontStruct * bold;
    105 };
    106 
    107 struct {
    108 	ulong bg;
    109 	ulong status_bg;
    110 	ulong cursor;
    111 
    112 	MudFont font;
    113 
    114 	union {
    115 		struct {
    116 			ulong black;
    117 			ulong red;
    118 			ulong green;
    119 			ulong yellow;
    120 			ulong blue;
    121 			ulong magenta;
    122 			ulong cyan;
    123 			ulong white;
    124 
    125 			ulong lblack;
    126 			ulong lred;
    127 			ulong lgreen;
    128 			ulong lyellow;
    129 			ulong lblue;
    130 			ulong lmagenta;
    131 			ulong lcyan;
    132 			ulong lwhite;
    133 
    134 			ulong system;
    135 		} Colours;
    136 
    137 		ulong colours[ 2 ][ 8 ];
    138 	};
    139 } Style;
    140 
    141 static void set_fg( Colour colour, bool bold ) {
    142 	ulong c;
    143 
    144 	switch( colour ) {
    145 		case SYSTEM:
    146 			c = Style.Colours.system;
    147 			break;
    148 
    149 		case COLOUR_BG:
    150 			c = Style.bg;
    151 			break;
    152 
    153 		case COLOUR_STATUSBG:
    154 			c = Style.status_bg;
    155 			break;
    156 
    157 		case COLOUR_CURSOR:
    158 			c = Style.cursor;
    159 			break;
    160 
    161 		default:
    162 			c = Style.colours[ bold ][ colour ];
    163 			break;
    164 	}
    165 
    166 	XSetForeground( UI.display, UI.gc, c );
    167 }
    168 
    169 void platform_make_dirty( int left, int top, int width, int height ) {
    170 	int right = left + width;
    171 	int bottom = top + height;
    172 
    173 	if( !UI.dirty ) {
    174 		UI.dirty = true;
    175 		UI.dirty_left = left;
    176 		UI.dirty_top = top;
    177 		UI.dirty_right = right;
    178 		UI.dirty_bottom = bottom;
    179 	}
    180 	else {
    181 		UI.dirty_left = min( UI.dirty_left, left );
    182 		UI.dirty_top = min( UI.dirty_top, top );
    183 		UI.dirty_right = max( UI.dirty_right, right );
    184 		UI.dirty_bottom = max( UI.dirty_bottom, bottom );
    185 	}
    186 }
    187 
    188 void platform_fill_rect( int left, int top, int width, int height, Colour colour, bool bold ) {
    189 	set_fg( colour, bold );
    190 	XFillRectangle( UI.display, UI.back_buffer, UI.gc, left, top, width, height );
    191 }
    192 
    193 void platform_draw_char( int left, int top, char c, Colour colour, bool bold, bool force_bold_font ) {
    194 	XSetFont( UI.display, UI.gc, ( bold || force_bold_font ? Style.font.bold : Style.font.regular )->fid );
    195 	set_fg( colour, bold );
    196 	XDrawString( UI.display, UI.back_buffer, UI.gc, left, top + Style.font.ascent + SPACING, &c, 1 );
    197 }
    198 
    199 static Atom wmDeleteWindow;
    200 
    201 static void event_mouse_down( XEvent * xevent ) {
    202 	ui_mouse_down( xevent->xbutton.x, xevent->xbutton.y );
    203 }
    204 
    205 static void event_mouse_move( XEvent * xevent ) {
    206 	ui_mouse_move( xevent->xmotion.x, xevent->xmotion.y );
    207 }
    208 
    209 static void event_mouse_up( XEvent * xevent ) {
    210 	ui_mouse_up( xevent->xbutton.x, xevent->xbutton.y );
    211 }
    212 
    213 static void event_message( XEvent * xevent ) {
    214 	if( ( Atom ) xevent->xclient.data.l[ 0 ] == wmDeleteWindow ) {
    215 		script_handleClose();
    216 		closing = true;
    217 	}
    218 }
    219 
    220 static void event_resize( XEvent * xevent ) {
    221 	int width = xevent->xconfigure.width;
    222 	int height = xevent->xconfigure.height;
    223 
    224 	int old_max_width = UI.max_width;
    225 	int old_max_height = UI.max_height;
    226 
    227 	UI.max_width = max( UI.max_width, width );
    228 	UI.max_height = max( UI.max_height, height );
    229 
    230 	if( UI.max_width != old_max_width || UI.max_height != old_max_height ) {
    231 		if( old_max_width != -1 ) {
    232 			XFreePixmap( UI.display, UI.back_buffer );
    233 		}
    234 		UI.back_buffer = XCreatePixmap( UI.display, UI.window, UI.max_width, UI.max_height, UI.depth );
    235 	}
    236 
    237 	ui_resize( width, height );
    238 }
    239 
    240 static void event_expose( XEvent * xevent ) {
    241 	ui_redraw_everything();
    242 }
    243 
    244 static void event_key_press( XEvent * xevent ) {
    245 	#define ADD_MACRO( key, name ) \
    246 		case key: \
    247 			script_doMacro( name, sizeof( name ) - 1, shift, ctrl, alt ); \
    248 			break
    249 
    250 	XKeyEvent * event = &xevent->xkey;
    251 
    252 	char keyBuffer[ 32 ];
    253 	KeySym key;
    254 
    255 	bool shift = event->state & ShiftMask;
    256 	bool ctrl = event->state & ControlMask;
    257 	bool alt = event->state & Mod1Mask;
    258 
    259 	int len = XLookupString( event, keyBuffer, sizeof( keyBuffer ), &key, NULL );
    260 
    261 	event->state &= ~ShiftMask;
    262 	char noShiftKeyBuffer[ 32 ];
    263 	int noShiftLen = XLookupString( event, noShiftKeyBuffer, sizeof( noShiftKeyBuffer ), NULL, NULL );
    264 
    265 	switch( key ) {
    266 		case XK_Return:
    267 			input_return();
    268 			break;
    269 
    270 		case XK_BackSpace:
    271 			input_backspace();
    272 			break;
    273 
    274 		case XK_Delete:
    275 			input_delete();
    276 			break;
    277 
    278 		case XK_Page_Up:
    279 			if( shift )
    280 				ui_scroll( 1 );
    281 			else
    282 				ui_page_up();
    283 			break;
    284 
    285 		case XK_Page_Down:
    286 			if( shift )
    287 				ui_scroll( -1 );
    288 			else
    289 				ui_page_down();
    290 			break;
    291 
    292 		case XK_Up:
    293 			input_up();
    294 			break;
    295 
    296 		case XK_Down:
    297 			input_down();
    298 			break;
    299 
    300 		case XK_Left:
    301 			input_left();
    302 			break;
    303 
    304 		case XK_Right:
    305 			input_right();
    306 			break;
    307 
    308 		case XK_Escape:
    309 			break;
    310 
    311 		ADD_MACRO( XK_KP_1, "kp1" );
    312 		ADD_MACRO( XK_KP_End, "kp1" );
    313 
    314 		ADD_MACRO( XK_KP_2, "kp2" );
    315 		ADD_MACRO( XK_KP_Down, "kp2" );
    316 
    317 		ADD_MACRO( XK_KP_3, "kp3" );
    318 		ADD_MACRO( XK_KP_Page_Down, "kp3" );
    319 
    320 		ADD_MACRO( XK_KP_4, "kp4" );
    321 		ADD_MACRO( XK_KP_Left, "kp4" );
    322 
    323 		ADD_MACRO( XK_KP_5, "kp5" );
    324 		ADD_MACRO( XK_KP_Begin, "kp5" );
    325 
    326 		ADD_MACRO( XK_KP_6, "kp6" );
    327 		ADD_MACRO( XK_KP_Right, "kp6" );
    328 
    329 		ADD_MACRO( XK_KP_7, "kp7" );
    330 		ADD_MACRO( XK_KP_Home, "kp7" );
    331 
    332 		ADD_MACRO( XK_KP_8, "kp8" );
    333 		ADD_MACRO( XK_KP_Up, "kp8" );
    334 
    335 		ADD_MACRO( XK_KP_9, "kp9" );
    336 		ADD_MACRO( XK_KP_Page_Up, "kp9" );
    337 
    338 		ADD_MACRO( XK_KP_0, "kp0" );
    339 		ADD_MACRO( XK_KP_Insert, "kp0" );
    340 
    341 		ADD_MACRO( XK_KP_Multiply, "kp*" );
    342 		ADD_MACRO( XK_KP_Divide, "kp/" );
    343 		ADD_MACRO( XK_KP_Subtract, "kp-" );
    344 		ADD_MACRO( XK_KP_Add, "kp+" );
    345 
    346 		ADD_MACRO( XK_KP_Delete, "kp." );
    347 		ADD_MACRO( XK_KP_Decimal, "kp." );
    348 
    349 		ADD_MACRO( XK_F1, "f1" );
    350 		ADD_MACRO( XK_F2, "f2" );
    351 		ADD_MACRO( XK_F3, "f3" );
    352 		ADD_MACRO( XK_F4, "f4" );
    353 		ADD_MACRO( XK_F5, "f5" );
    354 		ADD_MACRO( XK_F6, "f6" );
    355 		ADD_MACRO( XK_F7, "f7" );
    356 		ADD_MACRO( XK_F8, "f8" );
    357 		ADD_MACRO( XK_F9, "f9" );
    358 		ADD_MACRO( XK_F10, "f10" );
    359 		ADD_MACRO( XK_F11, "f11" );
    360 		ADD_MACRO( XK_F12, "f12" );
    361 
    362 		default:
    363 			if( ctrl && key == XK_v ) {
    364 				int len;
    365 				char * contents = clipboard_text_ex( clipboard, &len, LCB_CLIPBOARD );
    366 				if( contents != NULL ) {
    367 					input_add( contents, len );
    368 					free( contents );
    369 				}
    370 			}
    371 			else if( ctrl || alt ) {
    372 				script_doMacro( noShiftKeyBuffer, noShiftLen, shift, ctrl, alt );
    373 			}
    374 			else if( len > 0 ) {
    375 				input_add( keyBuffer, len );
    376 			}
    377 
    378 			break;
    379 	}
    380 
    381 	#undef ADD_MACRO
    382 }
    383 
    384 static void event_defocus( XEvent * xevent ) {
    385 	UI.has_focus = false;
    386 }
    387 
    388 static void event_focus( XEvent * xevent ) {
    389 	UI.has_focus = true;
    390 
    391 	XWMHints * hints = XGetWMHints( UI.display, UI.window );
    392 	hints->flags &= ~XUrgencyHint;
    393 	XSetWMHints( UI.display, UI.window, hints );
    394 	XFree( hints );
    395 }
    396 
    397 static void ui_handleXEvents() {
    398 	ZoneScoped;
    399 
    400 	void ( *event_handlers[ LASTEvent ] )( XEvent * ) = { };
    401 	event_handlers[ ButtonPress ] = event_mouse_down;
    402 	event_handlers[ MotionNotify ] = event_mouse_move;
    403 	event_handlers[ ButtonRelease ] = event_mouse_up;
    404 	event_handlers[ ClientMessage ] = event_message;
    405 	event_handlers[ ConfigureNotify ] = event_resize;
    406 	event_handlers[ Expose ] = event_expose;
    407 	event_handlers[ KeyPress ] = event_key_press;
    408 	event_handlers[ FocusOut ] = event_defocus;
    409 	event_handlers[ FocusIn ] = event_focus;
    410 
    411 	const char * event_names[ LASTEvent ] = { };
    412 	event_names[ ButtonPress ] = "ButtonPress";
    413 	event_names[ ButtonRelease ] = "ButtonRelease";
    414 	event_names[ MotionNotify ] = "MotionNotify";
    415 	event_names[ ClientMessage ] = "ClientMessage";
    416 	event_names[ ConfigureNotify ] = "ConfigureNotify";
    417 	event_names[ Expose ] = "Expose";
    418 	event_names[ KeyPress ] = "KeyPress";
    419 	event_names[ FocusOut ] = "FocusOut";
    420 	event_names[ FocusIn ] = "FocusIn";
    421 
    422 	do {
    423 		while( XPending( UI.display ) ) {
    424 			XEvent event;
    425 			XNextEvent( UI.display, &event );
    426 
    427 			if( event_handlers[ event.type ] != NULL ) {
    428 				// printf( "%s\n", event_names[ event.type ] );
    429 				event_handlers[ event.type ]( &event );
    430 			}
    431 		}
    432 
    433 		ui_redraw_dirty();
    434 
    435 		if( UI.dirty ) {
    436 			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 );
    437 			UI.dirty = false;
    438 			ui_handleXEvents();
    439 		}
    440 	} while( UI.dirty );
    441 }
    442 
    443 static MudFont load_font( const char * regular_name, const char * bold_name ) {
    444 	MudFont font;
    445 
    446 	font.regular = XLoadQueryFont( UI.display, regular_name );
    447 	if( font.regular == NULL )
    448 		errx( 1, "XLoadQueryFont: %s", regular_name );
    449 
    450 	font.bold = XLoadQueryFont( UI.display, bold_name );
    451 	if( font.bold == NULL )
    452 		errx( 1, "XLoadQueryFont: %s", bold_name );
    453 
    454 	font.ascent = font.regular->ascent;
    455 	font.width = font.regular->max_bounds.rbearing - font.regular->min_bounds.lbearing;
    456 	font.height = font.ascent + font.regular->descent;
    457 
    458 	return font;
    459 }
    460 
    461 static ulong make_color( const char * hex ) {
    462 	XColor color;
    463 	XAllocNamedColor( UI.display, UI.colorMap, hex, &color, &color );
    464 	return color.pixel;
    465 }
    466 
    467 static void initStyle() {
    468 	Style.bg             = make_color( "#1a1a1a" );
    469 	Style.status_bg      = make_color( "#333333" );
    470 	Style.cursor         = make_color( "#00ff00" );
    471 	Style.Colours.system = make_color( "#ffffff" );
    472 
    473 	Style.Colours.black   = make_color( "#1a1a1a" );
    474 	Style.Colours.red     = make_color( "#ca4433" );
    475 	Style.Colours.green   = make_color( "#178a3a" );
    476 	Style.Colours.yellow  = make_color( "#dc7c2a" );
    477 	Style.Colours.blue    = make_color( "#415e87" );
    478 	Style.Colours.magenta = make_color( "#5e468c" );
    479 	Style.Colours.cyan    = make_color( "#35789b" );
    480 	Style.Colours.white   = make_color( "#b6c2c4" );
    481 
    482 	Style.Colours.lblack   = make_color( "#666666" );
    483 	Style.Colours.lred     = make_color( "#ff2954" );
    484 	Style.Colours.lgreen   = make_color( "#5dd030" );
    485 	Style.Colours.lyellow  = make_color( "#fafc4f" );
    486 	Style.Colours.lblue    = make_color( "#3581e1" );
    487 	Style.Colours.lmagenta = make_color( "#875fff" );
    488 	Style.Colours.lcyan    = make_color( "#29fbff" );
    489 	Style.Colours.lwhite   = make_color( "#cedbde" );
    490 
    491 	Style.font = load_font( "-windows-dina-medium-r-normal--10-*-*-*-c-0-*-*", "-windows-dina-bold-r-normal--10-*-*-*-c-0-*-*" );
    492 }
    493 
    494 void platform_ui_init() {
    495 	ZoneScoped;
    496 
    497 	for( Socket & s : sockets ) {
    498 		s.in_use = false;
    499 	}
    500 
    501 	int default_width = 800;
    502 	int default_height = 600;
    503 
    504 	UI = { };
    505 	UI.display = XOpenDisplay( NULL );
    506 	UI.screen = XDefaultScreen( UI.display );
    507 	UI.max_width = -1;
    508 	UI.max_height = -1;
    509 
    510 	Window root = XRootWindow( UI.display, UI.screen );
    511 	UI.depth = XDefaultDepth( UI.display, UI.screen );
    512 	Visual * visual = XDefaultVisual( UI.display, UI.screen );
    513 	UI.colorMap = XDefaultColormap( UI.display, UI.screen );
    514 
    515 	initStyle();
    516 
    517 	XSetWindowAttributes attr = { };
    518 	attr.event_mask = ExposureMask | StructureNotifyMask | KeyPressMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask | FocusChangeMask;
    519 	attr.colormap = UI.colorMap;
    520 
    521 	UI.window = XCreateWindow( UI.display, root, 0, 0, default_height, default_width, 0, UI.depth, InputOutput, visual, CWBackPixel | CWEventMask | CWColormap, &attr );
    522 	UI.gc = XCreateGC( UI.display, UI.window, 0, NULL );
    523 
    524 	XWMHints * hints = XAllocWMHints();
    525 	XSetWMHints( UI.display, UI.window, hints );
    526 	XFree( hints );
    527 
    528 	Cursor cursor = XCreateFontCursor( UI.display, XC_xterm );
    529 	XDefineCursor( UI.display, UI.window, cursor );
    530 
    531 	XStoreName( UI.display, UI.window, "Mud Gangster" );
    532 	XMapWindow( UI.display, UI.window );
    533 
    534 	wmDeleteWindow = XInternAtom( UI.display, "WM_DELETE_WINDOW", false );
    535 	XSetWMProtocols( UI.display, UI.window, &wmDeleteWindow, 1 );
    536 
    537 	XEvent xev;
    538 	xev.xconfigure.width = default_width;
    539 	xev.xconfigure.height = default_height;
    540 	event_resize( &xev );
    541 
    542 	ui_handleXEvents();
    543 }
    544 
    545 void ui_urgent() {
    546 	if( !UI.has_focus ) {
    547 		XWMHints * hints = XGetWMHints( UI.display, UI.window );
    548 		hints->flags |= XUrgencyHint;
    549 		XSetWMHints( UI.display, UI.window, hints );
    550 		XFree( hints );
    551 	}
    552 }
    553 
    554 int ui_display_fd() {
    555 	return ConnectionNumber( UI.display );
    556 }
    557 
    558 void ui_get_font_size( int * fw, int * fh ) {
    559 	*fw = Style.font.width;
    560 	*fh = Style.font.height;
    561 }
    562 
    563 bool ui_set_font( const char * name, int size ) {
    564 	// TODO
    565 	return false;
    566 }
    567 
    568 void platform_set_clipboard( const char * str, size_t len ) {
    569 	// len includes the \0
    570 	clipboard_set_text_ex( clipboard, str, len - 1, LCB_CLIPBOARD );
    571 }
    572 
    573 void platform_ui_term() {
    574 	XFreeFont( UI.display, Style.font.regular );
    575 	XFreeFont( UI.display, Style.font.bold );
    576 
    577 	XFreePixmap( UI.display, UI.back_buffer );
    578 	XFreeGC( UI.display, UI.gc );
    579 	XDestroyWindow( UI.display, UI.window );
    580 	XCloseDisplay( UI.display );
    581 }
    582 
    583 static Socket * socket_from_fd( int fd ) {
    584 	for( Socket & sock : sockets ) {
    585 		if( sock.in_use && sock.sock.fd == fd ) {
    586 			return &sock;
    587 		}
    588 	}
    589 
    590 	return NULL;
    591 }
    592 
    593 int main() {
    594 	net_init();
    595 	ui_init();
    596 	platform_ui_init();
    597 	script_init();
    598 
    599 	clipboard = clipboard_new( NULL );
    600 	if( clipboard == NULL ) {
    601 		FATAL( "clipboard_new" );
    602 	}
    603 
    604 	FrameMark;
    605 
    606 	ui_handleXEvents();
    607 
    608 	while( !closing ) {
    609 		pollfd fds[ ARRAY_COUNT( sockets ) + 1 ] = { };
    610 		nfds_t num_fds = 1;
    611 
    612 		fds[ 0 ].fd = ConnectionNumber( UI.display );
    613 		fds[ 0 ].events = POLLIN;
    614 
    615 		for( Socket sock : sockets ) {
    616 			if( sock.in_use ) {
    617 				fds[ num_fds ].fd = sock.sock.fd;
    618 				fds[ num_fds ].events = POLLIN;
    619 				num_fds++;
    620 			}
    621 		}
    622 
    623 		int ok = poll( fds, num_fds, 500 );
    624 		if( ok == -1 )
    625 			FATAL( "poll" );
    626 
    627 		script_fire_intervals();
    628 
    629 		for( size_t i = 1; i < ARRAY_COUNT( fds ); i++ ) {
    630 			if( fds[ i ].revents & POLLIN ) {
    631 				Socket * sock = socket_from_fd( fds[ i ].fd );
    632 				assert( sock != NULL );
    633 
    634 				char buf[ 8192 ];
    635 				size_t n;
    636 				TCPRecvResult res = net_recv( sock->sock, buf, sizeof( buf ), &n );
    637 				if( res == TCP_OK ) {
    638 					script_socketData( sock, buf, n );
    639 				}
    640 				else if( res == TCP_CLOSED ) {
    641 					script_socketData( sock, NULL, 0 );
    642 				}
    643 				else {
    644 					FATAL( "net_recv" );
    645 				}
    646 			}
    647 		}
    648 
    649 		ui_handleXEvents();
    650 	}
    651 
    652 	clipboard_free( clipboard );
    653 
    654 	script_term();
    655 	platform_ui_term();
    656 	ui_term();
    657 	net_term();
    658 
    659 	return 0;
    660 }