#include "base.h" #include "../render/gl_2d_font.h" #include "../util/input.h" #include const U32 TEXTBOX_TITLE_OFFSET = 15; struct GUI_TEXTBOX_KEY_RESULT { U8 value_changed{}; U8 cursor_moved{}; }; static I32 gui_textbox_line_height() { GL_FONT* font = _gui.fonts.jpn12.glfnt; return font ? max( 1, (I32)font->char_height ) : 1; } static U32 gui_textbox_line_start( const char* text, U32 cursor ) { while( cursor > 0 && text[cursor - 1] != '\n' ) --cursor; return cursor; } static U32 gui_textbox_line_end( const char* text, U32 length, U32 cursor ) { while( cursor < length && text[cursor] != '\n' ) ++cursor; return cursor; } static U8 gui_textbox_has_selection( const GUI_TEXTBOX* tb ) { return tb->cursorin != tb->cursorout; } static U32 gui_textbox_selection_start( const GUI_TEXTBOX* tb ) { return min( tb->cursorin, tb->cursorout ); } static U32 gui_textbox_selection_end( const GUI_TEXTBOX* tb ) { return max( tb->cursorin, tb->cursorout ); } static U8 gui_textbox_shift_down( const GUI_TEXTBOX* tb ) { return tb->keys[SDLK_LSHIFT & 0xff] || tb->keys[SDLK_RSHIFT & 0xff]; } static I32 gui_textbox_range_width( const char* text, U32 start, U32 end ) { GL_FONT* font = _gui.fonts.jpn12.glfnt; if( !font ) return 0; F32 width = 0.f; for( U32 idx = start; idx < end; ++idx ) { U8 c = (U8)text[idx]; if( !c || c == '\n' ) break; width += font->glyphs[c].advance_x; } return (I32)( width + 0.5f ); } static I32 gui_textbox_cursor_row( const char* text, U32 cursor ) { I32 row = 0; for( U32 idx = 0; idx < cursor; ++idx ) { if( text[idx] == '\n' ) ++row; } return row; } static I32 gui_textbox_line_count( const char* text, U32 length ) { I32 count = 1; for( U32 idx = 0; idx < length; ++idx ) { if( text[idx] == '\n' ) ++count; } return count; } static U32 gui_textbox_line_start_from_row( const char* text, U32 length, I32 wanted_row ) { U32 start = 0; for( I32 row = 0; row < wanted_row; ++row ) { U32 end = gui_textbox_line_end( text, length, start ); if( end >= length ) return length; start = end + 1; } return start; } static U32 gui_textbox_nearest_cursor_x( const char* text, U32 line_start, U32 line_end, I32 wanted_x ) { GL_FONT* font = _gui.fonts.jpn12.glfnt; if( !font || wanted_x <= 0 ) return line_start; F32 x = 0.f; for( U32 idx = line_start; idx < line_end; ++idx ) { F32 advance = font->glyphs[(U8)text[idx]].advance_x; if( (F32)wanted_x < x + advance * 0.5f ) return idx; x += advance; } return line_end; } static void gui_textbox_update_cursor_metrics( GUI_TEXTBOX* tb ) { U32 length = (U32)strlen( tb->value ); tb->cursorin = min( tb->cursorin, length ); tb->cursorout = min( tb->cursorout, length ); U32 line_start_in = gui_textbox_line_start( tb->value, tb->cursorin ); U32 line_start_out = gui_textbox_line_start( tb->value, tb->cursorout ); tb->cursorpxin = gui_textbox_range_width( tb->value, line_start_in, tb->cursorin ); tb->cursorpxout = gui_textbox_range_width( tb->value, line_start_out, tb->cursorout ); tb->cursorpy = gui_textbox_cursor_row( tb->value, tb->cursorout ) * gui_textbox_line_height(); } static void gui_textbox_set_cursor( GUI_TEXTBOX* tb, U32 cursor, U8 extend_selection = 0, U8 preserve_vertical_goal = 0 ) { U32 length = (U32)strlen( tb->value ); tb->cursorout = min( cursor, length ); if( !extend_selection ) tb->cursorin = tb->cursorout; gui_textbox_update_cursor_metrics( tb ); if( !preserve_vertical_goal ) tb->cursor_goal_valid = 0; } static U32 gui_textbox_cursor_from_point( GUI_TEXTBOX* tb, I32 local_x, I32 local_y ) { U32 length = (U32)strlen( tb->value ); I32 line_height = gui_textbox_line_height(); I32 line_count = gui_textbox_line_count( tb->value, length ); I32 row = local_y / line_height; row = m_clamp( row, 0, line_count - 1 ); U32 line_start = gui_textbox_line_start_from_row( tb->value, length, row ); U32 line_end = gui_textbox_line_end( tb->value, length, line_start ); return gui_textbox_nearest_cursor_x( tb->value, line_start, line_end, max( 0, local_x ) ); } static U8 gui_textbox_move_vertical( GUI_TEXTBOX* tb, I32 direction, U8 extend_selection = 0 ) { if( !direction ) return 0; U32 length = (U32)strlen( tb->value ); gui_textbox_update_cursor_metrics( tb ); if( !tb->cursor_goal_valid ) { tb->cursor_goal_px = tb->cursorpxout; tb->cursor_goal_valid = 1; } U32 current_start = gui_textbox_line_start( tb->value, tb->cursorout ); U32 target_start = 0; U32 target_end = 0; if( direction < 0 ) { if( current_start == 0 ) return 0; target_end = current_start - 1; target_start = gui_textbox_line_start( tb->value, target_end ); } else { U32 current_end = gui_textbox_line_end( tb->value, length, tb->cursorout ); if( current_end >= length ) return 0; target_start = current_end + 1; target_end = gui_textbox_line_end( tb->value, length, target_start ); } U32 target = gui_textbox_nearest_cursor_x( tb->value, target_start, target_end, tb->cursor_goal_px ); gui_textbox_set_cursor( tb, target, extend_selection, 1 ); return 1; } static U8 gui_textbox_delete_selection( GUI_TEXTBOX* tb ) { if( !gui_textbox_has_selection( tb ) ) return 0; U32 length = (U32)strlen( tb->value ); U32 start = min( gui_textbox_selection_start( tb ), length ); U32 end = min( gui_textbox_selection_end( tb ), length ); if( start >= end ) { gui_textbox_set_cursor( tb, start ); return 0; } if( tb->pre_cb ) tb->pre_cb( tb ); memmove( tb->value + start, tb->value + end, length - end + 1 ); gui_textbox_set_cursor( tb, start ); return 1; } static U32 gui_textbox_insert_bytes( GUI_TEXTBOX* tb, const char* source, U32 count ) { if( !tb || !source || !count || !tb->maxlen ) return 0; U32 length = (U32)strlen( tb->value ); U32 limit = min( tb->maxlen, GUI_TEXTBOX_MAX ); U32 start = min( gui_textbox_selection_start( tb ), length ); U32 end = min( gui_textbox_selection_end( tb ), length ); U32 selected = end - start; U32 remaining = length - selected; if( limit <= 1 || remaining >= limit - 1 ) return 0; U32 available = limit - 1 - remaining; count = min( count, available ); if( !count ) return 0; if( tb->pre_cb ) tb->pre_cb( tb ); memmove( tb->value + start + count, tb->value + end, length - end + 1 ); memcpy( tb->value + start, source, count ); gui_textbox_set_cursor( tb, start + count ); return count; } static U8 gui_textbox_backspace( GUI_TEXTBOX* tb ) { if( gui_textbox_delete_selection( tb ) ) return 1; U32 length = (U32)strlen( tb->value ); U32 cursor = min( tb->cursorout, length ); if( !cursor ) return 0; if( tb->pre_cb ) tb->pre_cb( tb ); memmove( tb->value + cursor - 1, tb->value + cursor, length - cursor + 1 ); gui_textbox_set_cursor( tb, cursor - 1 ); return 1; } static U8 gui_textbox_delete_forward( GUI_TEXTBOX* tb ) { if( gui_textbox_delete_selection( tb ) ) return 1; U32 length = (U32)strlen( tb->value ); U32 cursor = min( tb->cursorout, length ); if( cursor >= length ) return 0; if( tb->pre_cb ) tb->pre_cb( tb ); memmove( tb->value + cursor, tb->value + cursor + 1, length - cursor ); gui_textbox_set_cursor( tb, cursor ); return 1; } void gui_textbox_draw_blinker( GUI_TEXTBOX* tb, I32 x, I32 y, CLR clr ) { if( !tb->active ) return; if( gui_time() - tb->blinktime > 1.0f ) { tb->blink = !tb->blink; tb->blinktime = gui_time(); } if( tb->blink ) { I32 caret_h = max( 1, gui_textbox_line_height() - 2 ); gui_draw_frect( x, y + 1, 1, caret_h, clr ); } } static I32 gui_textbox_newline_width() { GL_FONT* font = _gui.fonts.jpn12.glfnt; if( !font ) return 1; return max( 1, (I32)( font->glyphs[(U8)' '].advance_x + 0.5f ) ); } static void gui_textbox_draw_selection( GUI_TEXTBOX* tb, I32 draw_x, I32 draw_y, I32 line_height, CLR color ) { if( !gui_textbox_has_selection( tb ) ) return; U32 length = (U32)strlen( tb->value ); U32 selection_start = min( gui_textbox_selection_start( tb ), length ); U32 selection_end = min( gui_textbox_selection_end( tb ), length ); U32 line_start = gui_textbox_line_start( tb->value, selection_start ); I32 row = gui_textbox_cursor_row( tb->value, line_start ); I32 newline_width = gui_textbox_newline_width(); for( ;; ) { U32 line_end = gui_textbox_line_end( tb->value, length, line_start ); U32 range_start = max( selection_start, line_start ); U32 range_end = min( selection_end, line_end ); I32 x0 = gui_textbox_range_width( tb->value, line_start, range_start ); I32 x1 = gui_textbox_range_width( tb->value, line_start, range_end ); I32 width = x1 - x0; if( line_end < length && selection_end > line_end ) width += newline_width; if( width > 0 ) { gui_draw_frect( draw_x + x0, draw_y + row * line_height, width, line_height, color ); } if( line_end >= length || line_end >= selection_end ) break; line_start = line_end + 1; ++row; } } static void gui_textbox_draw_frame( GUI_TEXTBOX* tb, I32 x, I32 y ) { CLR border = gui_is_fg_window( tb ) ? ui_clr.border : ui_clr.border_inactive; gui_draw_frect( x, y, tb->w, tb->h, border ); gui_draw_frect( x + 1, y + 1, max( 0, tb->w - 2 ), max( 0, tb->h - 2 ), ui_clr.bg_sec ); } static void gui_textbox_update_scroll( GUI_TEXTBOX* tb, I32 content_w, I32 content_h, I32 line_height ) { I32 text_h = line_height; I32 text_w = 0; gui_draw_get_str_bounds( &text_w, &text_h, FNT_JPN12, "%s", tb->value ); text_h = max( text_h, tb->cursorpy + line_height ); if( tb->cursorpxout < tb->scrollpx ) tb->scrollpx = tb->cursorpxout; else if( tb->cursorpxout + 1 > tb->scrollpx + content_w ) tb->scrollpx = tb->cursorpxout + 1 - content_w; I32 max_scroll_x = max( 0, max( text_w, tb->cursorpxout + 1 ) - content_w ); tb->scrollpx = m_clamp( tb->scrollpx, 0, max_scroll_x ); if( !tb->allow_newl ) { tb->cursorpy = 0; tb->scrollpy = 0; return; } if( tb->cursorpy < tb->scrollpy ) tb->scrollpy = tb->cursorpy; else if( tb->cursorpy + line_height > tb->scrollpy + content_h ) tb->scrollpy = tb->cursorpy + line_height - content_h; tb->scrollpy = m_clamp( tb->scrollpy, 0, max( 0, text_h - content_h ) ); } static void gui_textbox_draw_content( GUI_TEXTBOX* tb, I32 x, I32 y, I32 line_height, CLR text_color ) { I32 draw_x = x + 2 - tb->scrollpx; I32 draw_y = y + 3 - tb->scrollpy; gui_draw_push_clip( x + 1, y + 1, max( 0, tb->w - 2 ), max( 0, tb->h - 2 ) ); gui_textbox_draw_selection( tb, draw_x, draw_y, line_height, ui_clr.selection ); gui_draw_str( draw_x, draw_y, ALIGN_L, FNT_JPN12, text_color, "%s", tb->value ); gui_textbox_draw_blinker( tb, draw_x + tb->cursorpxout, draw_y + tb->cursorpy, text_color ); gui_draw_pop_clip(); } void gui_textbox_draw_fn( void* ptr ) { GUI_TEXTBOX* tb = (GUI_TEXTBOX*)ptr; I32 x = gui_relx( tb ); I32 title_y = gui_rely( tb ); gui_draw_str( x, title_y, ALIGN_L, FNT_JPN12, ui_clr.txt, "%s", tb->name ); I32 y = title_y + TEXTBOX_TITLE_OFFSET; I32 line_height = gui_textbox_line_height(); gui_textbox_draw_frame( tb, x, y ); gui_textbox_update_cursor_metrics( tb ); gui_textbox_update_scroll( tb, max( 1, tb->w - 4 ), max( 1, tb->h - 4 ), line_height ); CLR text_color = tb->active ? ui_clr.txt : ui_clr.txt_inactive; gui_textbox_draw_content( tb, x, y, line_height, text_color ); } void gui_textbox_handle_mouse( GUI_TEXTBOX* tb ) { I32 x = gui_relx( tb ); I32 y = gui_rely( tb ) + TEXTBOX_TITLE_OFFSET; U8 m1 = gui_mbutton_down( GUI_MBTNLEFT ); I32 mx = 0; I32 my = 0; gui_cursor_pos( &mx, &my ); U8 inbounds = mx >= x && mx < x + tb->w && my >= y && my < y + tb->h; if( !m1 ) { tb->m1held = 0; return; } if( !tb->m1held ) { if( !inbounds ) { tb->m1held = 2; tb->active = 0; return; } I32 local_x = mx - ( x + 2 ) + tb->scrollpx; I32 local_y = my - ( y + 3 ) + tb->scrollpy; U32 cursor = gui_textbox_cursor_from_point( tb, local_x, local_y ); gui_textbox_set_cursor( tb, cursor ); tb->m1held = 1; tb->active = 1; } else if( tb->m1held == 1 ) { I32 local_x = mx - ( x + 2 ) + tb->scrollpx; I32 local_y = my - ( y + 3 ) + tb->scrollpy; U32 cursor = gui_textbox_cursor_from_point( tb, local_x, local_y ); gui_textbox_set_cursor( tb, cursor, 1 ); } if( tb->m1held == 1 ) { tb->blink = 1; tb->blinktime = gui_time(); } } static U8 gui_textbox_move_horizontal_key( GUI_TEXTBOX* tb, I32 direction, U8 extend_selection ) { if( !extend_selection && gui_textbox_has_selection( tb ) ) { U32 target = direction < 0 ? gui_textbox_selection_start( tb ) : gui_textbox_selection_end( tb ); gui_textbox_set_cursor( tb, target ); return 1; } U32 length = (U32)strlen( tb->value ); if( direction < 0 && tb->cursorout > 0 ) { gui_textbox_set_cursor( tb, tb->cursorout - 1, extend_selection ); return 1; } if( direction > 0 && tb->cursorout < length ) { gui_textbox_set_cursor( tb, tb->cursorout + 1, extend_selection ); return 1; } return 0; } static U8 gui_textbox_move_vertical_key( GUI_TEXTBOX* tb, I32 direction, U8 extend_selection ) { U8 had_selection = gui_textbox_has_selection( tb ); U8 moved = gui_textbox_move_vertical( tb, direction, extend_selection ); if( !extend_selection && had_selection && !moved ) { gui_textbox_set_cursor( tb, tb->cursorout ); return 1; } return moved; } static U8 gui_textbox_move_line_edge( GUI_TEXTBOX* tb, U8 move_to_end, U8 extend_selection ) { U32 length = (U32)strlen( tb->value ); U32 target = move_to_end ? gui_textbox_line_end( tb->value, length, tb->cursorout ) : gui_textbox_line_start( tb->value, tb->cursorout ); U8 moved = target != tb->cursorout || ( !extend_selection && gui_textbox_has_selection( tb ) ); gui_textbox_set_cursor( tb, target, extend_selection ); return moved; } static U8 gui_textbox_apply_edit_key( GUI_TEXTBOX* tb, U32 key, GUI_TEXTBOX_KEY_RESULT* result ) { constexpr U32 kp_enter = SDLK_KP_ENTER & 0xff; constexpr U32 del = SDLK_DELETE & 0xff; if( key == '\b' ) { result->value_changed = gui_textbox_backspace( tb ); result->cursor_moved = result->value_changed; return 1; } if( key == del ) { result->value_changed = gui_textbox_delete_forward( tb ); return 1; } if( key == '\r' || key == kp_enter ) { if( tb->allow_newl ) { result->value_changed = gui_textbox_insert_bytes( tb, "\n", 1 ) == 1; result->cursor_moved = result->value_changed; } else tb->active = 0; return 1; } if( key == '\x1b' ) { tb->active = 0; return 1; } return 0; } static U8 gui_textbox_apply_move_key( GUI_TEXTBOX* tb, U32 key, U8 extend_selection, GUI_TEXTBOX_KEY_RESULT* result ) { constexpr U32 right = SDLK_RIGHT & 0xff; constexpr U32 down = SDLK_DOWN & 0xff; constexpr U32 home = SDLK_HOME & 0xff; constexpr U32 left = SDLK_LEFT & 0xff; constexpr U32 end = SDLK_END & 0xff; constexpr U32 up = SDLK_UP & 0xff; if( key == left || key == right ) { result->cursor_moved = gui_textbox_move_horizontal_key( tb, key == left ? -1 : 1, extend_selection ); return 1; } if( key == up || key == down ) { if( !tb->allow_newl ) return 0; result->cursor_moved = gui_textbox_move_vertical_key( tb, key == up ? -1 : 1, extend_selection ); return 1; } if( key == home || key == end ) { result->cursor_moved = gui_textbox_move_line_edge( tb, key == end, extend_selection ); return 1; } return 0; } static void gui_textbox_apply_key_result( GUI_TEXTBOX* tb, const GUI_TEXTBOX_KEY_RESULT& result, F32 now ) { if( result.value_changed && tb->cb ) tb->cb( tb ); if( result.value_changed || result.cursor_moved ) { tb->blink = 1; tb->blinktime = now; } } static U8 gui_textbox_key_repeatable( const GUI_TEXTBOX* tb, U32 key ) { constexpr U32 right = SDLK_RIGHT & 0xff; constexpr U32 down = SDLK_DOWN & 0xff; constexpr U32 left = SDLK_LEFT & 0xff; constexpr U32 del = SDLK_DELETE & 0xff; constexpr U32 up = SDLK_UP & 0xff; return key == '\b' || key == del || key == left || key == right || ( tb->allow_newl && ( key == up || key == down ) ); } void gui_textbox_loop_keys( GUI_TEXTBOX* tb ) { U32 length = strlen( tb->value ); tb->cursorin = min( tb->cursorin, length ); tb->cursorout = min( tb->cursorout, length ); U8 extend_selection = gui_textbox_shift_down( tb ); for( U32 idx = 0; idx < 0x100; ++idx ) { U8 prev = tb->prevkeys[idx]; U8 key = tb->keys[idx]; if( !key ) { if( prev && idx == tb->heldkey ) tb->heldkey = 0; continue; } if( prev ) continue; GUI_TEXTBOX_KEY_RESULT result{}; U8 handled = gui_textbox_apply_edit_key( tb, idx, &result ); if( !handled ) { handled = gui_textbox_apply_move_key( tb, idx, extend_selection, &result ); } if( !handled ) continue; F32 now = gui_time(); gui_textbox_apply_key_result( tb, result, now ); tb->heldtime = now; tb->repeattime = now; tb->heldkey = (U8)idx; } } void gui_textbox_handle_repeat( GUI_TEXTBOX* tb ) { if( !gui_textbox_key_repeatable( tb, tb->heldkey ) ) return; F32 now = gui_time(); if( now - tb->heldtime <= 0.75f || now - tb->repeattime <= 0.05f ) return; GUI_TEXTBOX_KEY_RESULT result{}; U8 extend_selection = gui_textbox_shift_down( tb ); U8 handled = gui_textbox_apply_edit_key( tb, tb->heldkey, &result ); if( !handled ) { handled = gui_textbox_apply_move_key( tb, tb->heldkey, extend_selection, &result ); } if( handled ) gui_textbox_apply_key_result( tb, result, now ); tb->repeattime = now; } void gui_textbox_append_input_text( GUI_TEXTBOX* tb ) { if( !tb->active || !input.textlen || !tb->maxlen ) return; if( !tb->allow_newl ) { U32 filtered = 0; for( U32 idx = 0; idx < input.textlen; ++idx ) { char c = input.text[idx]; if( c != '\n' && c != '\r' ) input.text[filtered++] = c; } input.textlen = filtered; input.text[filtered] = 0; } U32 inserted = gui_textbox_insert_bytes( tb, input.text, input.textlen ); if( inserted ) { tb->blink = 1; tb->blinktime = gui_time(); if( tb->cb ) tb->cb( tb ); } input.textlen = 0; input.text[0] = 0; } void gui_textbox_handle_keyboard( GUI_TEXTBOX* tb ) { if( !tb->active ) return; gui_textbox_append_input_text( tb ); memcpy( tb->prevkeys, tb->keys, 0x100 ); memcpy( tb->keys, input.keys, 0x100 ); gui_textbox_loop_keys( tb ); gui_textbox_handle_repeat( tb ); } void gui_textbox_input_fn( void* ptr ) { GUI_TEXTBOX* tb = (GUI_TEXTBOX*)ptr; gui_textbox_handle_mouse( tb ); gui_textbox_handle_keyboard( tb ); } GUI_TEXTBOX* gui_textbox( I32 x, I32 y, I32 w, I32 h, const char* title, U32 maxlen, U8 allow_newl ) { GUI_TEXTBOX* tb = new GUI_TEXTBOX{}; tb->x = x; tb->y = y; tb->w = w; tb->h = h; tb->xbound = tb->w; tb->ybound = tb->h + TEXTBOX_TITLE_OFFSET; tb->parent = _gui.cur_view; tb->draw_fn = gui_textbox_draw_fn; tb->input_fn = gui_textbox_input_fn; strcpy( tb->name, title ); tb->cb = 0; tb->active = 0; tb->m1held = 0; tb->heldkey = 0; tb->cursorpy = 0; tb->heldtime = 0; tb->repeattime = 0; tb->maxlen = maxlen; tb->cursor_goal_px = 0; tb->cursor_goal_valid = 0; tb->allow_newl = allow_newl; tb->scrollpx = tb->scrollpy = 0; tb->cursorin = tb->cursorout = 0; tb->cursorpxin = tb->cursorpxout = 0; memset( tb->value, 0, GUI_TEXTBOX_MAX ); memset( tb->keys, 0, 0x100 ); memset( tb->prevkeys, 0, 0x100 ); tb->blink = 1; tb->blinktime = gui_time(); gui_get_view()->children.push( tb ); return tb; }