mudgangster

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

textbox.cc (10101B)


      1 #include <string.h>
      2 
      3 #include "common.h"
      4 #include "textbox.h"
      5 #include "ui.h"
      6 #include "platform.h"
      7 #include "platform_ui.h"
      8 
      9 #if PLATFORM_WINDOWS
     10 #define NEWLINE_STRING "\r\n"
     11 #else
     12 #define NEWLINE_STRING "\n"
     13 #endif
     14 
     15 static u8 pack_style( Colour fg, Colour bg, bool bold ) {
     16 	STATIC_ASSERT( NUM_COLOURS * NUM_COLOURS * 2 < UINT8_MAX );
     17 
     18 	u32 style = 0;
     19 	style = checked_cast< u32 >( fg );
     20 	style = style * NUM_COLOURS + checked_cast< u32 >( bg );
     21 	style = style * 2 + checked_cast< u32 >( bold );
     22 
     23 	return checked_cast< u8 >( style );
     24 }
     25 
     26 static void unpack_style( u8 style, int * fg, int * bg, int * bold ) {
     27 	*bold = style % 2;
     28 	style /= 2;
     29 
     30 	*bg = style % NUM_COLOURS;
     31 	style /= NUM_COLOURS;
     32 
     33 	*fg = style;
     34 }
     35 
     36 void textbox_init( TextBox * tb, size_t scrollback ) {
     37 	ZoneScoped;
     38 
     39 	// TODO: this is kinda crap
     40 	*tb = { };
     41 	tb->lines = alloc_span< TextBox::Line >( scrollback );
     42 	memset( tb->lines.ptr, 0, tb->lines.num_bytes() );
     43 	tb->num_lines = 1;
     44 	tb->max_lines = scrollback;
     45 }
     46 
     47 void textbox_destroy( TextBox * tb ) {
     48 	free( tb->lines.ptr );
     49 }
     50 
     51 void textbox_add( TextBox * tb, const char * str, size_t len, Colour fg, Colour bg, bool bold ) {
     52 	TextBox::Line * line = &tb->lines[ ( tb->head + tb->num_lines ) % tb->max_lines ];
     53 	size_t remaining = MAX_LINE_LENGTH - line->len;
     54 	size_t n = min( strlen( str ), remaining );
     55 
     56 	for( size_t i = 0; i < n; i++ ) {
     57 		TextBox::Glyph & glyph = line->glyphs[ line->len + i ];
     58 		glyph.ch = str[ i ];
     59 		glyph.style = pack_style( fg, bg, bold );
     60 	};
     61 
     62 	line->len += n;
     63 	tb->dirty = true;
     64 }
     65 
     66 void textbox_newline( TextBox * tb ) {
     67 	bool freeze = tb->scroll_offset > 0 || tb->selecting;
     68 
     69 	if( tb->num_lines < tb->max_lines ) {
     70 		tb->num_lines++;
     71 		if( freeze )
     72 			tb->scroll_offset++;
     73 		else
     74 			tb->dirty = true;
     75 		return;
     76 	}
     77 
     78 	tb->head++;
     79 	if( freeze )
     80 		tb->scroll_offset = min( tb->scroll_offset + 1, tb->num_lines - 1 );
     81 	tb->dirty = true;
     82 
     83 	TextBox::Line * line = &tb->lines[ ( tb->head + tb->num_lines ) % tb->max_lines ];
     84 	line->len = 0;
     85 }
     86 
     87 void textbox_scroll( TextBox * tb, int offset ) {
     88 	if( offset < 0 ) {
     89 		tb->scroll_offset -= min( size_t( -offset ), tb->scroll_offset );
     90 	}
     91 	else {
     92 		tb->scroll_offset = min( tb->scroll_offset + offset, tb->num_lines - 1 );
     93 	}
     94 
     95 	tb->dirty = true;
     96 }
     97 
     98 static size_t num_rows( size_t h ) {
     99 	int fw, fh;
    100 	ui_get_font_size( &fw, &fh );
    101 	return h / ( fh + SPACING );
    102 }
    103 
    104 void textbox_page_down( TextBox * tb ) {
    105 	textbox_scroll( tb, -int( num_rows( tb->h ) ) + 1 );
    106 }
    107 
    108 void textbox_page_up( TextBox * tb ) {
    109 	textbox_scroll( tb, num_rows( tb->h ) - 1 );
    110 }
    111 
    112 void textbox_mouse_down( TextBox * tb, int window_x, int window_y ) {
    113 	int x = window_x - tb->x;
    114 	int y = window_y - tb->y;
    115 
    116 	if( x < 0 || y < 0 || x >= tb->w || y >= tb->h )
    117 		return;
    118 
    119 	int fw, fh;
    120 	ui_get_font_size( &fw, &fh );
    121 
    122 	int row = ( tb->h - y ) / ( fh + SPACING );
    123 	int col = x / fw;
    124 
    125 	tb->selecting = true;
    126 	tb->selecting_and_mouse_moved = false;
    127 	tb->scroll_down_after_selecting = tb->scroll_offset == 0;
    128 	tb->selection_start_col = col;
    129 	tb->selection_start_row = row;
    130 	tb->selection_end_col = col;
    131 	tb->selection_end_row = row;
    132 	tb->dirty = true;
    133 }
    134 
    135 void textbox_mouse_move( TextBox * tb, int window_x, int window_y ) {
    136 	if( !tb->selecting )
    137 		return;
    138 
    139 	int x = window_x - tb->x;
    140 	int y = window_y - tb->y;
    141 
    142 	int fw, fh;
    143 	ui_get_font_size( &fw, &fh );
    144 
    145 	int row = ( tb->h - y ) / ( fh + SPACING );
    146 	int col = x / fw;
    147 
    148 	if( col != tb->selection_end_col || row != tb->selection_end_row ) {
    149 		tb->selection_end_col = col;
    150 		tb->selection_end_row = row;
    151 		tb->dirty = true;
    152 	}
    153 	else if( !tb->selecting_and_mouse_moved ) {
    154 		tb->dirty = true;
    155 	}
    156 
    157 	tb->selecting_and_mouse_moved = true;
    158 }
    159 
    160 void textbox_mouse_up( TextBox * tb, int window_x, int window_y ) {
    161 	if( !tb->selecting || !tb->selecting_and_mouse_moved ) {
    162 		tb->selecting = false;
    163 		return;
    164 	}
    165 
    166 	int fw, fh;
    167 	ui_get_font_size( &fw, &fh );
    168 
    169 	size_t tb_cols = tb->w / fw;
    170 
    171 	// convert mouse start/end points to ordered start/end points
    172 	int start_row = tb->selection_start_row;
    173 	int end_row = tb->selection_end_row;
    174 	int start_col = tb->selection_start_col;
    175 	int end_col = tb->selection_end_col;
    176 	if( tb->selection_start_row == tb->selection_end_row ) {
    177 		start_col = min( tb->selection_start_col, tb->selection_end_col );
    178 		end_col = max( tb->selection_start_col, tb->selection_end_col );
    179 	}
    180 	else if( tb->selection_start_row < tb->selection_end_row ) {
    181 		swap( start_row, end_row );
    182 		swap( start_col, end_col );
    183 	}
    184 
    185 	// find what the start/end lines/offsets are
    186 	// TODO: this is incorrect when wrapping...
    187 	int end_line = 0;
    188 	int rows = 0;
    189 
    190 	while( rows < end_row && end_line < tb->num_lines ) {
    191 		const TextBox::Line & line = tb->lines[ ( tb->head + tb->num_lines - tb->scroll_offset - end_line ) % tb->max_lines ];
    192 		int line_rows = 1 + line.len / tb_cols;
    193 		if( line.len > 0 && line.len % tb_cols == 0 )
    194 			line_rows--;
    195 		rows += line_rows;
    196 		end_line++;
    197 	}
    198 
    199 	if( end_line == tb->num_lines )
    200 		return;
    201 
    202 	size_t end_line_offset = ( rows - end_row ) * tb_cols + end_col + 1;
    203 	int start_line = end_line;
    204 
    205 	while( rows < start_row && start_line < tb->num_lines ) {
    206 		const TextBox::Line & line = tb->lines[ ( tb->head + tb->num_lines - tb->scroll_offset - start_line ) % tb->max_lines ];
    207 		int line_rows = 1 + line.len / tb_cols;
    208 		if( line.len > 0 && line.len % tb_cols == 0 )
    209 			line_rows--;
    210 		rows += line_rows;
    211 		start_line++;
    212 	}
    213 
    214 	size_t start_line_offset = ( rows - start_row ) * tb_cols + start_col;
    215 	if( start_line == tb->num_lines ) {
    216 		start_line--;
    217 		start_line_offset = 0;
    218 	}
    219 
    220 	// first pass to get the length of the selected string
    221 	size_t selected_length = 1; // include space for \0
    222 	for( int i = start_line; i >= end_line; i-- ) {
    223 		const TextBox::Line & line = tb->lines[ ( tb->head + tb->num_lines - tb->scroll_offset - i ) % tb->max_lines ];
    224 		size_t start_offset = i == start_line ? start_line_offset : 0;
    225 		size_t end_offset = i == end_line ? end_line_offset : line.len;
    226 		// TODO: iterate over glyphs to see when ansi codes need inserting
    227 		if( start_offset <= line.len ) {
    228 			selected_length += min( line.len, end_offset ) - start_offset;
    229 		}
    230 		if( i != end_line ) {
    231 			selected_length += sizeof( NEWLINE_STRING ) - 1;
    232 		}
    233 	}
    234 
    235 	char * selected = alloc_many< char >( selected_length );
    236 	selected[ selected_length - 1 ] = '\0';
    237 
    238 	// second pass to copy the selection out
    239 	size_t n = 0;
    240 	for( int i = start_line; i >= end_line; i-- ) {
    241 		const TextBox::Line & line = tb->lines[ ( tb->head + tb->num_lines - tb->scroll_offset - i ) % tb->max_lines ];
    242 		size_t start_offset = i == start_line ? start_line_offset : 0;
    243 		size_t end_offset = i == end_line ? end_line_offset : line.len;
    244 		if( start_offset <= line.len ) {
    245 			size_t len = min( line.len, end_offset ) - start_offset;
    246 			// TODO: insert ansi codes when style changes
    247 			for( size_t j = 0; j < len; j++ ) {
    248 				selected[ n ] = line.glyphs[ j + start_offset ].ch;
    249 				n++;
    250 			}
    251 		}
    252 		if( i != end_line ) {
    253 			memcpy( selected + n, NEWLINE_STRING, sizeof( NEWLINE_STRING ) - 1 );
    254 			n += sizeof( NEWLINE_STRING ) - 1;
    255 		}
    256 	}
    257 
    258 	platform_set_clipboard( selected, selected_length );
    259 	free( selected );
    260 
    261 	tb->selecting = false;
    262 	tb->dirty = true;
    263 
    264 	if( tb->scroll_down_after_selecting )
    265 		tb->scroll_offset = 0;
    266 }
    267 
    268 void textbox_set_pos( TextBox * tb, int x, int y ) {
    269 	tb->x = x;
    270 	tb->y = y;
    271 }
    272 
    273 void textbox_set_size( TextBox * tb, int w, int h ) {
    274 	tb->w = w;
    275 	tb->h = h;
    276 }
    277 
    278 static bool inside_selection( int col, int row, int start_col, int start_row, int end_col, int end_row ) {
    279 	int min_row = min( start_row, end_row );
    280 	int max_row = max( start_row, end_row );
    281 
    282 	if( row < min_row || row > max_row )
    283 		return false;
    284 
    285 	if( start_row == end_row ) {
    286 		int min_col = min( start_col, end_col );
    287 		int max_col = max( start_col, end_col );
    288 		return col >= min_col && col <= max_col;
    289 	}
    290 
    291 	if( row > min_row && row < max_row )
    292 		return true;
    293 
    294 	if( start_row < end_row ) {
    295 		if( row == start_row )
    296 			return col <= start_col;
    297 		if( row == end_row )
    298 			return col >= end_col;
    299 	}
    300 	else {
    301 		if( row == start_row )
    302 			return col >= start_col;
    303 		if( row == end_row )
    304 			return col <= end_col;
    305 	}
    306 
    307 	return false;
    308 }
    309 
    310 void textbox_draw( TextBox * tb ) {
    311 	if( tb->w <= 0 || tb->h <= 0 )
    312 		return;
    313 
    314 	ui_fill_rect( tb->x, tb->y, tb->w, tb->h, COLOUR_BG, false );
    315 
    316 	/*
    317 	 * lines refers to lines of text sent from the game
    318 	 * rows refers to visual rows of text in the client, so when lines get
    319 	 * wrapped they have more than one row
    320 	 */
    321 
    322 	int fw, fh;
    323 	ui_get_font_size( &fw, &fh );
    324 
    325 	size_t lines_drawn = 0;
    326 	size_t rows_drawn = 0;
    327 	size_t tb_rows = num_rows( tb->h );
    328 	size_t tb_cols = tb->w / fw;
    329 
    330 	int top_spacing = SPACING / 2;
    331 	int bot_spacing = SPACING - top_spacing;
    332 
    333 	while( rows_drawn < tb_rows && lines_drawn + tb->scroll_offset < tb->num_lines ) {
    334 		const TextBox::Line & line = tb->lines[ ( tb->head + tb->num_lines - tb->scroll_offset - lines_drawn ) % tb->max_lines ];
    335 
    336 		size_t line_rows = 1 + line.len / tb_cols;
    337 		if( line.len > 0 && line.len % tb_cols == 0 )
    338 			line_rows--;
    339 
    340 		for( size_t i = 0; i < line.len; i++ ) {
    341 			const TextBox::Glyph & glyph = line.glyphs[ i ];
    342 
    343 			size_t row = i / tb_cols;
    344 			size_t col = i % tb_cols;
    345 
    346 			int left = col * fw;
    347 			int top = tb->h - ( rows_drawn + line_rows - row ) * ( fh + SPACING );
    348 			if( top < 0 )
    349 				continue;
    350 
    351 			int fg, bg, bold;
    352 			unpack_style( glyph.style, &fg, &bg, &bold );
    353 
    354 			bool bold_fg = bold;
    355 			bool bold_bg = false;
    356 			if( tb->selecting && tb->selecting_and_mouse_moved ) {
    357 				if( inside_selection( col, rows_drawn + line_rows - row - 1, tb->selection_start_col, tb->selection_start_row, tb->selection_end_col, tb->selection_end_row ) ) {
    358 					swap( fg, bg );
    359 					swap( bold_fg, bold_bg );
    360 				}
    361 			}
    362 
    363 			// bg
    364 			// TODO: top/bottom spacing seems to be inconsistent here, try with large spacing
    365 			if( bg != BLACK ) {
    366 				ui_fill_rect( tb->x + left, tb->y + top - top_spacing, fw, fh + bot_spacing, Colour( bg ), bold_bg );
    367 			}
    368 
    369 			// fg
    370 			ui_draw_char( tb->x + left, tb->y + top, glyph.ch, Colour( fg ), bold_fg, bold );
    371 		}
    372 
    373 		lines_drawn++;
    374 		rows_drawn += line_rows;
    375 	}
    376 
    377 	platform_make_dirty( tb->x, tb->y, tb->w, tb->h );
    378 
    379 	tb->dirty = false;
    380 }