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 }