diff options
Diffstat (limited to 'src/gui')
| -rw-r--r-- | src/gui/base.cpp | 70 | ||||
| -rw-r--r-- | src/gui/base.h | 86 | ||||
| -rw-r--r-- | src/gui/console.cpp | 1779 | ||||
| -rw-r--r-- | src/gui/console.h | 67 | ||||
| -rw-r--r-- | src/gui/contextmenu.cpp | 437 | ||||
| -rw-r--r-- | src/gui/textbox.cpp | 1011 |
6 files changed, 3293 insertions, 157 deletions
diff --git a/src/gui/base.cpp b/src/gui/base.cpp index 79299de..153d82b 100644 --- a/src/gui/base.cpp +++ b/src/gui/base.cpp @@ -110,6 +110,23 @@ void gui_free( GUI_BASE* node ) { delete node; } +void gui_shutdown() { + _gui.callbacks.clear(); + _gui.clip_rects.clear(); + _gui.cur_view = 0; + _gui.cur_window = 0; + + while( _gui.windows.size ) { + GUI_WINDOW* wnd = _gui.windows[ + _gui.windows.size - 1 + ]; + gui_free( wnd ); + } + _gui.batch = 0; + _gui.fonts.jpn12.glfnt = 0; + _gui.fonts.jpn16.glfnt = 0; +} + I32 gui_relx( GUI_BASE* node ) { I32 x = node->x; for( GUI_BASE* p = node->parent; !!p; p = p->parent ) { @@ -283,47 +300,62 @@ GL_FONT* gui_font_from_idx( U32 fnt ) { return pfnt->glfnt; } -void gui_draw_str_internal( I32 x, I32 y, U8 align, U32 fnt, CLR col, const char* str ) { +void gui_draw_get_str_bounds_internal( I32* w, I32* h, U32 fnt, const char* buf ) { GL_FONT* pfnt = gui_font_from_idx( fnt ); if( !pfnt ) return; - I32 w; - gui_draw_get_str_bounds( &w, 0, fnt, str ); - switch( align ) { - case ALIGN_R: x -= w; break; - case ALIGN_C: x -= (I32)( w * 0.5f ); break; - } - - gl_font_draw( pfnt, _gui.batch, { (F32)x, (F32)y }, str, col ); + VEC2 dim = gl_font_dim( pfnt, buf ); + if( w ) *w = dim.x; + if( h ) *h = dim.y; } -void gui_draw_str( I32 x, I32 y, U8 align, U32 fnt, CLR col, const char* fmt, ... ) { +void gui_draw_get_str_bounds( I32* w, I32* h, U32 fnt, const char *fmt, ... ) { va_list args; va_start( args, fmt ); char buf[GUI_STR_BUF_MAX]; - vsprintf( buf, fmt, args ); + I32 result = vsnprintf( + buf, sizeof( buf ), + fmt, args + ); va_end( args ); - gui_draw_str_internal( x, y, align, fnt, col, buf ); + if( result < 0 ) + return; + + buf[sizeof( buf ) - 1] = 0; + gui_draw_get_str_bounds_internal( w, h, fnt, buf ); } -void gui_draw_get_str_bounds_internal( I32* w, I32* h, U32 fnt, const char* buf ) { +void gui_draw_str_internal( I32 x, I32 y, U8 align, U32 fnt, CLR col, const char* str ) { GL_FONT* pfnt = gui_font_from_idx( fnt ); if( !pfnt ) return; - VEC2 dim = gl_font_dim( pfnt, buf ); - if( w ) *w = dim.x; - if( h ) *h = dim.y; + I32 w; + gui_draw_get_str_bounds_internal( &w, 0, fnt, str ); + switch( align ) { + case ALIGN_R: x -= w; break; + case ALIGN_C: x -= (I32)( w * 0.5f ); break; + } + + gl_font_draw( pfnt, _gui.batch, { (F32)x, (F32)y }, str, col ); } -void gui_draw_get_str_bounds( I32* w, I32* h, U32 fnt, const char *fmt, ... ) { +void gui_draw_str( I32 x, I32 y, U8 align, U32 fnt, CLR col, const char* fmt, ... ) { va_list args; va_start( args, fmt ); char buf[GUI_STR_BUF_MAX]; - vsprintf( buf, fmt, args ); + I32 result = vsnprintf( + buf, sizeof( buf ), + fmt, args + ); va_end( args ); - gui_draw_get_str_bounds_internal( w, h, fnt, buf ); + if( result < 0 ) + return; + + buf[sizeof( buf ) - 1] = 0; + + gui_draw_str_internal( x, y, align, fnt, col, buf ); } void gui_draw_get_clip( I32 *x, I32 *y, I32 *w, I32 *h ) { diff --git a/src/gui/base.h b/src/gui/base.h index 108988d..3fedfcf 100644 --- a/src/gui/base.h +++ b/src/gui/base.h @@ -11,6 +11,8 @@ static const struct { CLR bg = CLR::blend( CLR::WHITE(), CLR::BLACK(), 0.92f ); CLR bg_sec = CLR::blend( CLR::WHITE(), CLR::BLACK(), 0.97f ); CLR bg_alt = CLR::blend( CLR::WHITE(), CLR::BLACK(), 0.985f ); + CLR shadow = CLR::blend_root( CLR::BLACK(), CLR::WHITE(), 0.05f ); + CLR selection = CLR::blend( CLR::WHITE(), CLR::BLACK(), 0.7f ); } ui_clr; // ========================================== [ base ] ============================================ @@ -35,6 +37,7 @@ extern struct GUI_WINDOW* gui_get_parent_wnd( struct GUI_BASE* node ); extern void gui_empty_children( struct GUI_BASE* node ); extern void gui_free( struct GUI_BASE* node ); +extern void gui_shutdown(); extern struct GUI_VIEW* gui_get_view(); // gets currently edited view extern void gui_set_view( struct GUI_VIEW* view ); // sets currently edited view @@ -58,6 +61,18 @@ struct GUI_CONTEXT_ENTRY { LIST<GUI_CONTEXT_ENTRY> subentries; }; +typedef void( *GUI_CONTEXTMENU_CALLBACK )( void* data ); +struct GUI_CONTEXTMENU_ITEM; +typedef U8( *GUI_CONTEXTMENU_ENABLED_CALLBACK )( GUI_CONTEXTMENU_ITEM* item ); + +struct GUI_CONTEXTMENU_ITEM { + char text[64]{}; + void* data{}; + GUI_CONTEXTMENU_CALLBACK cb{}; + GUI_CONTEXTMENU_ENABLED_CALLBACK enabled_cb{}; + U8 enabled{1}; + LIST<GUI_CONTEXTMENU_ITEM> items{}; +}; extern struct GUI_VIEW* gui_view( I32 x, I32 y, I32 w, I32 h ); // sets current view extern struct GUI_WINDOW* gui_window( I32 w, I32 h ); // sets current view and window @@ -76,6 +91,39 @@ extern struct GUI_LIST_ENTRY* gui_list_get_selected( struct GUI_LIST* list ); extern struct GUI_CONTEXTBOX* gui_contextbox( I32 x, I32 y, I32 w, I32 h, const char* title, LIST<GUI_CONTEXT_ENTRY>* entries ); extern struct GUI_CONTEXTBOX* gui_contextbox( I32 x, I32 y, I32 w, I32 h, LIST<GUI_CONTEXT_ENTRY>* entries ); +extern struct GUI_CONTEXTMENU* gui_show_contextmenu( + I32 x, I32 y, + LIST<GUI_CONTEXTMENU_ITEM>* items, + GUI_BASE* parent, + const char* title = 0, + I32 max_visible_rows = 0, + struct GUI_CONTEXTMENU** owner_ref = 0 +); +extern void gui_hide_contextmenu( GUI_CONTEXTMENU* menu ); +extern U8 gui_contextmenu_open( GUI_CONTEXTMENU* menu ); +extern I32 gui_contextmenu_height( + I32 rows, + const char* title = 0 +); +extern void gui_contextmenu_move_selection( + GUI_CONTEXTMENU* menu, + I32 delta, + U8 wrap = 1 +); +extern void gui_contextmenu_reveal_selection( + GUI_CONTEXTMENU* menu +); +extern void gui_contextmenu_set_item( + GUI_CONTEXTMENU_ITEM* item, + const char* text, + GUI_CONTEXTMENU_CALLBACK cb = 0, + void* data = 0, + GUI_CONTEXTMENU_ENABLED_CALLBACK enabled_cb = 0 +); +extern struct GUI_CONTEXTMENU_ITEM* gui_contextmenu_selected( + GUI_CONTEXTMENU* menu +); + extern struct GUI_FLOATINPUT* gui_floatinput( I32 x, I32 y, I32 w, const char* title, @@ -108,6 +156,16 @@ extern struct GUI_COLORINPUT* gui_colorinput( U8 showalpha = 1 ); +extern struct GUI_CONSOLEWINDOW* gui_consolewindow( + I32 x, + I32 y, + I32 w, + I32 h +); +extern U8 gui_consolewindow_is_open(); +extern void gui_consolewindow_toggle(); +extern U8 gui_consolewindow_input_focused(); + // ======================================= [ definitions ] ======================================== enum GuiTxtAlign_t { @@ -201,16 +259,22 @@ struct GUI_DROPDOWN : GUI_BASE { struct GUI_TEXTBOX : GUI_BASE { char value[GUI_TEXTBOX_MAX]; - U32 len; U32 maxlen; - U8 keys[0xff]; - U8 prevkeys[0xff]; + U8 keys[0x100]; + U8 prevkeys[0x100]; U8 heldkey; F32 heldtime; F32 repeattime; + U32 cursorin, cursorout; + I32 cursorpxin, cursorpxout; + I32 cursorpy; + I32 scrollpx, scrollpy; + U8 cursor_goal_valid; + I32 cursor_goal_px; + F32 blinktime; U8 blink; @@ -223,6 +287,22 @@ struct GUI_TEXTBOX : GUI_BASE { GUI_CALLBACK pre_cb; }; +struct GUI_CONTEXTMENU : GUI_BASE { + LIST<GUI_CONTEXTMENU_ITEM>* items{}; + GUI_CONTEXTMENU** owner_ref{}; + + I32 selected_idx{-1}; + I32 first_visible{}; + I32 visible_rows{}; + I32 max_visible_rows{}; + + U8 initheld{1}; + U8 held{}; + I32 held_idx{-1}; + + char title[64]{}; +}; + struct GUI_CONTEXTBOX : GUI_BASE { LIST<GUI_CONTEXT_ENTRY>* entries; }; diff --git a/src/gui/console.cpp b/src/gui/console.cpp new file mode 100644 index 0000000..4f74649 --- /dev/null +++ b/src/gui/console.cpp @@ -0,0 +1,1779 @@ +#include "SDL_clipboard.h" +#include "SDL_keyboard.h" +#include "SDL_keycode.h" +#include "SDL_mouse.h" + +#include "console.h" + +#include "../render/gl_2d_font.h" +#include "../util/console.h" +#include "../util/math.h" +#include "../game.h" +#include "base.h" + +#include <string.h> + +struct CONSOLE_INPUT_SEGMENT { + U32 length{}; + U32 start{}; + U8 valid{}; +}; + +struct CONSOLE_WRAP_RANGE { + U32 next; + U32 end; +}; + +extern GAME_DATA* game; +extern VAR_LIST* varl; + +constexpr I32 GUI_CONSOLE_HISTORY_MENU_ROWS = 4; +constexpr I32 GUI_CONSOLE_MINIMUM_HEIGHT = 128; +constexpr I32 GUI_CONSOLE_MINIMUM_WIDTH = 256; +constexpr I32 GUI_CONSOLE_INPUT_HEIGHT = 20; +constexpr I32 GUI_TEXTBOX_TITLE_OFFSET = 15; +constexpr I32 GUI_CONSOLE_BORDER_WIDTH = 1; +constexpr I32 GUI_CONSOLE_PADDING = 4; +constexpr I32 GUI_TITLE_HEIGHT = 26; +constexpr I32 GUI_BUTTON_SIZE = 16; +constexpr I32 GUI_GRAB_SIZE = 8; + +inline I32 get_input_y( I32 wnd_h ) { + return wnd_h + - ( GUI_CONSOLE_BORDER_WIDTH * 2 ) + - GUI_CONSOLE_PADDING + - GUI_TEXTBOX_TITLE_OFFSET + - GUI_CONSOLE_INPUT_HEIGHT; +} + +inline I32 get_input_w( I32 wnd_w ) { + return wnd_w + - ( GUI_CONSOLE_PADDING * 2 ) + - ( GUI_CONSOLE_BORDER_WIDTH * 2 ); +} + +inline I32 get_close_x( I32 wnd_w ) { + return wnd_w + - GUI_BUTTON_SIZE + - GUI_CONSOLE_PADDING + - ( GUI_CONSOLE_BORDER_WIDTH * 2 ); +} + +inline VEC2 get_console_output_pos( GUI_CONSOLEWINDOW* wnd ) { + I32 x = gui_relx( wnd ) + GUI_CONSOLE_BORDER_WIDTH + GUI_CONSOLE_PADDING; + I32 y = gui_rely( wnd ) + GUI_TITLE_HEIGHT + GUI_CONSOLE_PADDING; + return { x, y }; +} + +inline VEC2 get_console_output_sz( GUI_CONSOLEWINDOW* wnd ) { + I32 bottom = gui_rely( wnd->input ) + GUI_TEXTBOX_TITLE_OFFSET - GUI_CONSOLE_PADDING; + I32 width = wnd->w - ( GUI_CONSOLE_BORDER_WIDTH + GUI_CONSOLE_PADDING ) * 2; + I32 top = gui_rely( wnd ) + GUI_TITLE_HEIGHT + GUI_CONSOLE_PADDING; + return { + (F32)max<I32>( 0, width ), + (F32)max<I32>( 0, bottom - top ) + }; +} + +inline U8 constrain_console( GUI_CONSOLEWINDOW* wnd, I32 max_x, I32 max_y ) { + max_x = max<I32>( 1.0f, max_x ); + max_y = max<I32>( 1.0f, max_y ); + + I32 oldx = wnd->x; + I32 oldy = wnd->y; + I32 oldw = wnd->w; + I32 oldh = wnd->h; + + I32 minh = min<I32>( GUI_CONSOLE_MINIMUM_HEIGHT, max_y ); + I32 minw = min<I32>( GUI_CONSOLE_MINIMUM_WIDTH, max_x ); + + wnd->w = m_clamp<I32>( wnd->w, minw, max_x ); + wnd->h = m_clamp<I32>( wnd->h, minh, max_y ); + wnd->x = m_clamp<I32>( wnd->x, 0, max_x - wnd->w ); + wnd->y = m_clamp<I32>( wnd->y, 0, max_y - wnd->h ); + + return oldx != wnd->x + || oldy != wnd->y + || oldw != wnd->w + || oldh != wnd->h; +} + +inline GUI_VIEW* get_console_view( GUI_CONSOLEWINDOW* wnd ) { + if( !wnd->children.size ) + return 0; + return (GUI_VIEW*)wnd->children.at( 0 ); +} + +inline void layout_console_window( GUI_CONSOLEWINDOW* wnd ) { + GUI_VIEW* view = get_console_view( wnd ); + if( !view ) + return; + + view->w = max<I32>( 1, wnd->w - GUI_CONSOLE_BORDER_WIDTH * 2 ); + view->h = max<I32>( 1, wnd->h - GUI_CONSOLE_BORDER_WIDTH * 2 ); + + if( wnd->input ) { + wnd->input->y = get_input_y( wnd->h ); + wnd->input->w = get_input_w( wnd->w ); + } + + if( wnd->close ) + wnd->close->x = get_close_x( wnd->w ); + + if( wnd->title ) + wnd->title->w = view->w; +} + +inline void store_console_drag_origin( GUI_CONSOLEWINDOW* wnd ) { + wnd->drag_start_mouse = input.mouse.pos; + wnd->drag_start_pos = VEC2( wnd->x, wnd->y ); + wnd->drag_start_sz = VEC2( + wnd->x + wnd->w, + wnd->y + wnd->h + ); +} + +inline RESIZE_EDGES get_console_active_edges( + GUI_CONSOLEWINDOW* wnd, + VEC2 mouse, + I32 max_x, + I32 max_y +) { + F32 radius = GUI_GRAB_SIZE * 0.5f; + + if( mouse.x < 0 || mouse.x >= max_x + || mouse.y < 0 || mouse.y >= max_y + ) return RESIZE_NONE; + + F32 top = (F32)wnd->y; + F32 left = (F32)wnd->x; + F32 right = (F32)( wnd->x + wnd->w ); + F32 bottom = (F32)( wnd->y + wnd->h ); + + U8 within_vert = mouse.y >= top + GUI_TITLE_HEIGHT + && mouse.y < bottom + radius; + U8 within_horz = mouse.x >= left - radius + && mouse.x < right + radius; + U8 near_left = within_vert + && mouse.x >= left - radius + && mouse.x < left + radius; + U8 near_right = within_vert + && mouse.x >= right - radius + && mouse.x < right + radius; + U8 near_bottom = within_horz + && mouse.y >= bottom - radius + && mouse.y < bottom + radius; + + if( near_left && near_right ) { + F32 d_left = fabsf( mouse.x - left ); + F32 d_right = fabsf( mouse.x - right ); + + if( d_left <= d_right ) + near_right = 0; + else + near_left = 0; + } + + U8 mask = RESIZE_NONE; + if( near_left ) + mask |= RESIZE_LEFT; + if( near_right ) + mask |= RESIZE_RIGHT; + if( near_bottom ) + mask |= RESIZE_BOTTOM; + return (RESIZE_EDGES)mask; +} + +inline U8 is_over_console_input( GUI_CONSOLEWINDOW* wnd, VEC2 mouse ) { + if( !wnd->input ) + return 0; + I32 x = gui_relx( wnd->input ); + I32 y = gui_rely( wnd->input ) + GUI_TEXTBOX_TITLE_OFFSET; + + return mouse.x >= x + && mouse.x < x + wnd->input->w + && mouse.y >= y + && mouse.y < y + wnd->input->h; +} + +inline SDL_SystemCursor get_console_cursor( RESIZE_EDGES edges, U8 over_input ) { + if( ( edges & RESIZE_BOTTOM ) && ( edges & RESIZE_LEFT ) ) + return SDL_SYSTEM_CURSOR_SIZENESW; + if( ( edges & RESIZE_BOTTOM ) && ( edges & RESIZE_RIGHT ) ) + return SDL_SYSTEM_CURSOR_SIZENWSE; + if( edges & RESIZE_BOTTOM ) + return SDL_SYSTEM_CURSOR_SIZENS; + if( edges & ( RESIZE_LEFT | RESIZE_RIGHT ) ) + return SDL_SYSTEM_CURSOR_SIZEWE; + if( over_input ) + return SDL_SYSTEM_CURSOR_IBEAM; + return SDL_SYSTEM_CURSOR_ARROW; +} + +inline void set_console_cursor( RESIZE_EDGES edges, U8 over_input = 0 ) { + SDL_SystemCursor type = get_console_cursor( edges, over_input ); + SDL_Cursor* cursor = game->assets.cursors[type]; + if( cursor && SDL_GetCursor() != cursor ) + SDL_SetCursor( cursor ); +} + +inline void console_resize_begin( GUI_CONSOLEWINDOW* wnd, RESIZE_EDGES edges ) { + wnd->active_edges = edges; + wnd->locked = 1; + + store_console_drag_origin( wnd ); + SDL_CaptureMouse( SDL_TRUE ); +} + +inline void console_resize_end( GUI_CONSOLEWINDOW* wnd ) { + if( wnd->active_edges != RESIZE_NONE ) + SDL_CaptureMouse( SDL_FALSE ); + wnd->active_edges = RESIZE_NONE; + wnd->locked = 0; +} + +inline void resize_console_window( GUI_CONSOLEWINDOW* wnd, I32 max_x, I32 max_y ) { + F32 bottom = wnd->drag_start_sz.y; + F32 right = wnd->drag_start_sz.x; + F32 left = wnd->drag_start_pos.x; + F32 top = wnd->drag_start_pos.y; + + F32 dm_x = input.mouse.pos.x - wnd->drag_start_mouse.x; + F32 dm_y = input.mouse.pos.y - wnd->drag_start_mouse.y; + + F32 min_h = (F32)min<I32>( GUI_CONSOLE_MINIMUM_HEIGHT, max_y ); + F32 min_w = (F32)min<I32>( GUI_CONSOLE_MINIMUM_WIDTH, max_x ); + + U8 hit_constraint = 0; + + if( wnd->active_edges & RESIZE_LEFT ) { + F32 req = wnd->drag_start_pos.x + dm_x; + F32 applied = m_clamp<F32>( req, 0.0f, wnd->drag_start_sz.x - min_w ); + left = applied; + hit_constraint |= req != applied; + } else if( wnd->active_edges & RESIZE_RIGHT ) { + F32 req = wnd->drag_start_sz.x + dm_x; + F32 applied = m_clamp<F32>( req, + wnd->drag_start_pos.x + min_w, + (F32)max_x + ); + right = applied; + hit_constraint |= req != applied; + } + + if( wnd->active_edges & RESIZE_BOTTOM ) { + F32 req = wnd->drag_start_sz.y + dm_y; + F32 applied = m_clamp<F32>( req, + wnd->drag_start_pos.y + min_h, + (F32)max_y + ); + bottom = applied; + hit_constraint |= req != applied; + } + + wnd->y = (I32)top; + wnd->x = (I32)left; + wnd->w = max<I32>( 1, (I32)( right - left ) ); + wnd->h = max<I32>( 1, (I32)( bottom - top ) ); + layout_console_window( wnd ); + if( hit_constraint ) + store_console_drag_origin( wnd ); +} + +inline I32 console_output_line_height() { + I32 height = 0; + I32 width = 0; + + gui_draw_get_str_bounds( + &width, &height, + FNT_JPN12, "Ag" + ); + + return height > 0 + ? height + 1 + : 0; +} + +inline U8 console_output_id_bounds( + const CONSOLE_DATA* state, + U64* first, + U64* last +) { + if( !state ) + return 0; + + U8 has_partial = state->partial_stdout_line.size > 0; + U8 has_complete = state->lines.count > 0; + + if( !has_complete && !has_partial ) + return 0; + + if( first ) { + *first = has_complete + ? state->lines.total_pushed - state->lines.count + : state->lines.total_pushed; + } + + if( last ) { + *last = has_partial + ? state->lines.total_pushed + : state->lines.total_pushed - 1; + } + + return 1; +} + +inline U8 console_validate_output_selection( + GUI_CONSOLEWINDOW* wnd, + const CONSOLE_DATA* state +) { + if( !wnd || !wnd->output_selection_active ) + return 0; + + U64 first = 0; + U64 last = 0; + + if( !console_output_id_bounds( state, &first, &last ) ) { + wnd->output_selection_active = 0; + wnd->output_selecting = 0; + return 0; + } + + U64 selection_start = min<U64>( + wnd->output_selection_anchor, + wnd->output_selection_cursor + ); + + U64 selection_end = max<U64>( + wnd->output_selection_anchor, + wnd->output_selection_cursor + ); + + if( selection_end < first || selection_start > last ) { + wnd->output_selection_active = 0; + wnd->output_selecting = 0; + return 0; + } + + wnd->output_selection_anchor = max<U64>( first, + min<U64>( wnd->output_selection_anchor, last ) + ); + wnd->output_selection_cursor = max<U64>( first, + min<U64>( wnd->output_selection_cursor, last ) + ); + return 1; +} + +inline U8 console_output_line_selected( + const GUI_CONSOLEWINDOW* wnd, + U64 source_id +) { + if( !wnd || !wnd->output_selection_active ) + return 0; + + U64 start = min<U64>( + wnd->output_selection_anchor, + wnd->output_selection_cursor + ); + U64 end = max<U64>( + wnd->output_selection_anchor, + wnd->output_selection_cursor + ); + return source_id >= start && source_id <= end; +} + +inline void console_push_wrapped_range( + GUI_CONSOLEWINDOW* wnd, + const char* text, + U32 start, U32 end, + U64 source_id +) { + CONSOLE_WRAPPED_LINE wrapped = { + .text = STR( end - start, text + start ), + .source_id = source_id + }; + wnd->wrapped_lines.push( wrapped ); +} + +inline CONSOLE_WRAP_RANGE console_next_wrap_range( + const STR& line, + U32 start, + I32 max_w, + GL_FONT* font +) { + CONSOLE_WRAP_RANGE range = { + line.size, + line.size + }; + + I32 last_space = -1; + U32 cursor = start; + F32 width = 0.f; + + while( cursor < line.size ) { + U8 c = (U8)line.data[cursor]; + F32 advance = font->glyphs[c].advance_x; + + if( width + advance > max_w ) + break; + + width += advance; + + if( c == ' ' && cursor > start ) + last_space = (I32)cursor; + ++cursor; + } + + if( cursor == line.size ) + return range; + + if( cursor == start ) { + range.next = start + 1; + range.end = start + 1; + return range; + } + + if( last_space > (I32)start ) { + range.end = (U32)last_space; + range.next = range.end + 1; + + while( range.next < line.size + && line.data[range.next] == ' ' + ) ++range.next; + + return range; + } + + range.next = cursor; + range.end = cursor; + return range; +} + +void console_wrap_line( + GUI_CONSOLEWINDOW* wnd, + const STR& line, + U64 source_id, + I32 max_w, + GL_FONT* font +) { + if( !wnd || !font || max_w <= 0 ) + return; + + if( !line.size ) { + console_push_wrapped_range( + wnd, line.data, + 0, 0, source_id + ); + return; + } + + for( U32 start = 0; start < line.size; ) { + CONSOLE_WRAP_RANGE range = console_next_wrap_range( + line, start, + max_w, font + ); + console_push_wrapped_range( + wnd, line.data, + start, range.end, + source_id + ); + + start = range.next; + } +} + +void console_rebuild_wrapped_lines( + GUI_CONSOLEWINDOW* wnd, + const CONSOLE_DATA* state, + I32 width +) { + if( !wnd || !state || width <= 0 ) + return; + + U8 lines_removed = wnd->seen_revision != state->lines.revision; + + if( !lines_removed + && wnd->wrapped_width == width + && wnd->wrapped_revision == state->revision + ) return; + + GL_FONT* font = _gui.fonts.jpn12.glfnt; + if( !font ) + return; + + I32 old_count = (I32)wnd->wrapped_lines.size; + I32 old_offset = wnd->scroll_offset; + U8 width_changed = wnd->wrapped_width != width; + + wnd->wrapped_lines.clear(); + + U64 first_source_id = state->lines.total_pushed - state->lines.count; + for( U32 idx = 0; idx < state->lines.count; ++idx ) { + console_wrap_line( + wnd, + state->lines[idx], + first_source_id + idx, + width, + font + ); + } + + if( state->partial_stdout_line.size ) { + console_wrap_line( + wnd, + state->partial_stdout_line, + state->lines.total_pushed, + width, + font + ); + } + + I32 new_count = (I32)wnd->wrapped_lines.size; + + if( lines_removed ) + wnd->scroll_offset = 0; + else if( old_offset > 0 && !width_changed ) { + wnd->scroll_offset = max<I32>( 0, + old_offset + new_count - old_count + ); + } + wnd->wrapped_width = width; + wnd->wrapped_revision = state->revision; + wnd->seen_revision = state->lines.revision; +} + +void draw_console_output( GUI_CONSOLEWINDOW* wnd ) { + VEC2 pos = get_console_output_pos( wnd ); + VEC2 sz = get_console_output_sz( wnd ); + if( sz.x <= 0 || sz.y <= 0 ) + return; + + console_rebuild_wrapped_lines( wnd, &game->console, (I32)sz.x ); + console_validate_output_selection( wnd, &game->console ); + + I32 line_height = console_output_line_height(); + if( line_height <= 0 ) + return; + + wnd->visible_rows = (I32)sz.y / line_height; + I32 row_count = (I32)wnd->wrapped_lines.size; + I32 max_scroll = max<I32>( 0, row_count - wnd->visible_rows ); + wnd->scroll_offset = m_clamp<I32>( wnd->scroll_offset, 0, max_scroll ); + gui_draw_push_clip( + (I32)pos.x, (I32)pos.y, + (I32)sz.x, (I32)sz.y + ); + I32 y = (I32)( pos.y + sz.y ) - line_height; + I32 index = row_count - 1 - wnd->scroll_offset; + + while( index >= 0 && y >= pos.y ) { + CONSOLE_WRAPPED_LINE* row = &wnd->wrapped_lines[index]; + + if( console_output_line_selected( wnd, row->source_id ) ) + gui_draw_frect( (I32)pos.x, y, (I32)sz.x, line_height, ui_clr.selection ); + gui_draw_str( + pos.x, y, ALIGN_L, + FNT_JPN12, ui_clr.txt, + "%s", row->text.data + ); + y -= line_height; + --index; + } + gui_draw_pop_clip(); +} + +inline LIST<CVAR*> console_cvar_list( const char* query, U32 query_length ) { + LIST<CVAR*> matches; + + if( !varl || !query || !query_length ) + return matches; + + matches.reserve( varl->vars.size ); + for( U32 idx = 0; idx < varl->vars.size; ++idx ) { + CVAR* var = varl->vars[idx]; + if( !var || !var->name.data ) + continue; + + U32 name_length = (U32)strlen( var->name.data ); + + if( name_length >= query_length + && !strncmp( var->name.data, query, query_length ) + ) matches.push( var ); + } + return matches; +} + +inline void console_close_input_menu( GUI_CONSOLEWINDOW* wnd ) { + if( !wnd ) + return; + + if( wnd->menus.input.menu ) { + gui_hide_contextmenu( wnd->menus.input.menu ); + wnd->menus.input.menu = 0; + } + + if( wnd->menus.input.items.size ) + wnd->menus.input.items.clear(); + + if( wnd->menus.input.cvars.size ) + wnd->menus.input.cvars.clear(); + + wnd->menus.input.mode = CONSOLE_MENU_INPUT_NONE; + wnd->menus.input.history_size = 0; + wnd->menus.input.cvar_count = 0; + wnd->menus.input.query[0] = 0; + wnd->menus.input.suppressed = 0; + wnd->menus.input.suppressed_value[0] = 0; +} + +inline void console_close_output_menu( GUI_CONSOLEWINDOW* wnd ) { + if( wnd && wnd->menus.output.menu ) + gui_hide_contextmenu( wnd->menus.output.menu ); +} + +inline U8 console_mouse_over_menu( + GUI_CONTEXTMENU* menu, + VEC2 mouse +) { + if( !menu || !gui_contextmenu_open( menu ) ) + return 0; + I32 x = gui_relx( menu ); + I32 y = gui_rely( menu ); + return mouse.x >= x + && mouse.x < x + menu->w + && mouse.y >= y + && mouse.y < y + menu->h; +} + +void console_copy_output_selection( void* data ) { + GUI_CONSOLEWINDOW* wnd = (GUI_CONSOLEWINDOW*)data; + CONSOLE_DATA* state = game ? &game->console : 0; + + if( !console_validate_output_selection( wnd, state ) ) + return; + + U64 selection_start = min<U64>( + wnd->output_selection_anchor, + wnd->output_selection_cursor + ); + U64 selection_end = max<U64>( + wnd->output_selection_anchor, + wnd->output_selection_cursor + ); + + U64 first_complete = state->lines.total_pushed - state->lines.count; + U8 copied_line = 0; + STR copied; + + for( U64 source_id = selection_start;; ++source_id ) { + const STR* line = 0; + + if( source_id < state->lines.total_pushed + && source_id >= first_complete + ) { + U64 logical_index = source_id - first_complete; + if( logical_index < state->lines.count ) + line = &state->lines[(U32)logical_index]; + } + else if( source_id == state->lines.total_pushed + && state->partial_stdout_line.size + ) line = &state->partial_stdout_line; + + if( line ) { + if( copied_line ) + copied.push( '\n' ); + copied.append( *line ); + copied_line = 1; + } + + if( source_id == selection_end ) + break; + } + + if( copied_line ) + SDL_SetClipboardText( copied.data ); +} + +inline void console_open_output_menu( + GUI_CONSOLEWINDOW* wnd, + I32 mouse_x, I32 mouse_y +) { + if( !wnd || !wnd->output_selection_active ) + return; + + console_close_input_menu( wnd ); + console_close_output_menu( wnd ); + wnd->menus.output.menu = gui_show_contextmenu( + mouse_x, mouse_y, + &wnd->menus.output.items, + wnd, 0, 1, &wnd->menus.output.menu + ); + + if( !wnd->menus.output.menu ) + return; + + I32 canvas_w = game->gl->canvas_size[0]; + I32 canvas_h = game->gl->canvas_size[1]; + + I32 screen_x = m_clamp<I32>( mouse_x, + 0, + max<I32>( 0, + canvas_w - wnd->menus.output.menu->w + ) + ); + I32 screen_y = m_clamp<I32>( mouse_y, + 0, + max<I32>( 0, + canvas_h - wnd->menus.output.menu->h + ) + ); + wnd->menus.output.menu->x = screen_x - gui_relx( wnd ); + wnd->menus.output.menu->y = screen_y - gui_rely( wnd ); +} + +inline I32 console_input_menu_visible_rows( + GUI_CONTEXTMENU* menu, + I32 item_count, + I32 available_h +) { + I32 rows = min<I32>( + menu->max_visible_rows, + item_count + ); + + while( rows > 0 + && gui_contextmenu_height( + rows, menu->title + ) > available_h + ) --rows; + return rows; +} + +inline void console_reset_input_menu_view( + GUI_CONSOLEWINDOW* wnd, + GUI_CONTEXTMENU* menu, + I32 item_count +) { + if( menu->selected_idx >= 0 ) { + gui_contextmenu_reveal_selection( menu ); + return; + } + menu->first_visible = wnd->menus.input.mode == CONSOLE_MENU_INPUT_HIST + ? max<I32>( 0, item_count - menu->visible_rows ) + : 0; +} + +inline void console_position_input_menu( GUI_CONSOLEWINDOW* wnd ) { + if( !wnd || !wnd->input || !wnd->menus.input.menu ) + return; + + GUI_CONTEXTMENU* menu = wnd->menus.input.menu; + if( !menu->items || !menu->items->size ) + return; + + I32 canvas_w = max<I32>( 1, game->gl->canvas_size[0] ); + I32 canvas_h = max<I32>( 1, game->gl->canvas_size[1] ); + + I32 input_x = gui_relx( wnd->input ); + I32 input_bottom = gui_rely( wnd->input ) + + GUI_TEXTBOX_TITLE_OFFSET + + wnd->input->h; + I32 item_count = (I32)menu->items->size; + + menu->visible_rows = console_input_menu_visible_rows( + menu, + item_count, + max<I32>( 0, canvas_h - input_bottom ) + ); + menu->enabled = menu->visible_rows > 0; + if( !menu->enabled ) + return; + + menu->h = gui_contextmenu_height( menu->visible_rows, menu->title ); + + console_reset_input_menu_view( + wnd, menu, + item_count + ); + + menu->w = min<I32>( + max<I32>( 1, wnd->input->w ), + max<I32>( 1, canvas_w - input_x ) + ); + menu->y = input_bottom - gui_rely( wnd ); + menu->x = input_x - gui_relx( wnd ); + menu->xbound = menu->w; + menu->ybound = menu->h; +} + +inline U8 console_show_input_menu( GUI_CONSOLEWINDOW* wnd ) { + if( !wnd || !wnd->input || !wnd->menus.input.items.size ) + return 0; + + I32 max_rows = min<I32>( + GUI_CONSOLE_HISTORY_MENU_ROWS, + (I32)wnd->menus.input.items.size + ); + + I32 input_x = gui_relx( wnd->input ); + I32 input_bottom = gui_rely( wnd->input ) + + GUI_TEXTBOX_TITLE_OFFSET + + wnd->input->h; + wnd->menus.input.menu = gui_show_contextmenu( + input_x, input_bottom, + &wnd->menus.input.items, + wnd, 0, max_rows, + &wnd->menus.input.menu + ); + + if( !wnd->menus.input.menu ) + return 0; + + console_position_input_menu( wnd ); + return 1; +} + +inline CONSOLE_INPUT_SEGMENT console_input_segment( GUI_TEXTBOX* textbox ) { + CONSOLE_INPUT_SEGMENT segment{}; + + if( !textbox ) + return segment; + + U32 total_length = (U32)strlen( textbox->value ); + U8 in_string = 0; + + for( U32 idx = 0; idx < total_length; ++idx ) { + char c = textbox->value[idx]; + + if( c == '"' ) + in_string ^= 1; + else if( c == ';' && !in_string ) + segment.start = idx + 1; + } + + while( segment.start < total_length + && textbox->value[segment.start] == ' ' + ) ++segment.start; + + segment.length = total_length - segment.start; + segment.valid = !in_string; + return segment; +} + +inline const char* console_input_menu_completion( + GUI_CONSOLEWINDOW* wnd, + const CONSOLE_INPUT_SEGMENT& segment, + CONSOLE_MENU_INPUT_MODE* selected_mode +) { + if( !wnd || !wnd->input || !wnd->menus.input.menu || !selected_mode ) + return 0; + + I32 selected = wnd->menus.input.menu->selected_idx; + *selected_mode = wnd->menus.input.mode; + + if( *selected_mode == CONSOLE_MENU_INPUT_HIST ) { + if( segment.length + || !game + || selected < 0 + || selected >= (I32)game->console.history.size + ) return 0; + + return game->console.history[selected].data; + } + + if( *selected_mode != CONSOLE_MENU_INPUT_CVAR + || !segment.length + || selected < 0 + || selected >= (I32)wnd->menus.input.cvars.size + ) return 0; + + U32 cached_length = (U32)strlen( wnd->menus.input.query ); + + if( cached_length != segment.length + || strncmp( + wnd->menus.input.query, + wnd->input->value + segment.start, + segment.length + ) + ) return 0; + + CVAR* var = wnd->menus.input.cvars[selected]; + return var && var->name.data + ? var->name.data + : 0; +} + +inline U8 console_apply_input_completion( + GUI_TEXTBOX* textbox, + U32 start, + const char* completion +) { + if( !textbox || !completion ) + return 0; + + U32 capacity = min<U32>( + textbox->maxlen, + GUI_TEXTBOX_MAX + ); + + if( !capacity || start >= capacity - 1 ) + return 0; + + snprintf( + textbox->value + start, + capacity - start, + "%s", completion + ); + + U32 length = (U32)strlen( textbox->value ); + + textbox->active = 1; + textbox->blink = 1; + textbox->blinktime = gui_time(); + textbox->cursorpxin = 0; + textbox->cursorpxout = 0; + textbox->cursorin = length; + textbox->cursorout = length; + textbox->cursor_goal_valid = 0; + return 1; +} + +inline void console_suppress_input_menu( + GUI_CONSOLEWINDOW* wnd, + const char* value +) { + wnd->menus.input.suppressed = 1; + snprintf( + wnd->menus.input.suppressed_value, + sizeof( wnd->menus.input.suppressed_value ), + "%s", value + ); +} + +inline U8 console_apply_input_menu_selection( + GUI_CONSOLEWINDOW* wnd +) { + if( !wnd || !wnd->input || !wnd->menus.input.menu ) + return 0; + + if( !gui_contextmenu_selected( wnd->menus.input.menu ) ) + return 0; + + CONSOLE_INPUT_SEGMENT segment = console_input_segment( wnd->input ); + if( !segment.valid ) + return 0; + + CONSOLE_MENU_INPUT_MODE selected_mode = wnd->menus.input.mode; + const char* completion = console_input_menu_completion( + wnd, segment, + &selected_mode + ); + if( !completion ) + return 0; + + U8 applied = console_apply_input_completion( + wnd->input, + segment.start, + completion + ); + + char completed_value[GUI_TEXTBOX_MAX]{}; + + if( applied && selected_mode == CONSOLE_MENU_INPUT_CVAR ) { + snprintf( + completed_value, + sizeof( completed_value ), + "%s", wnd->input->value + ); + } + + console_close_input_menu( wnd ); + + if( applied && selected_mode == CONSOLE_MENU_INPUT_CVAR ) + console_suppress_input_menu( wnd, completed_value ); + + return 1; +} + +void console_input_menu_item_cb( void* data ) { + console_apply_input_menu_selection( + (GUI_CONSOLEWINDOW*)data + ); +} + +inline void console_open_cvar_menu( + GUI_CONSOLEWINDOW* wnd, + const char* query, + U32 query_length +) { + console_close_input_menu( wnd ); + + if( !wnd || !wnd->input || !query || !query_length ) + return; + + wnd->menus.input.mode = CONSOLE_MENU_INPUT_CVAR; + wnd->menus.input.cvar_count = varl + ? varl->vars.size + : 0; + + I32 copy_length = (I32)min<U32>( + query_length, + GUI_TEXTBOX_MAX - 1 + ); + + snprintf( + wnd->menus.input.query, + sizeof( wnd->menus.input.query ), + "%.*s", copy_length, query + ); + + wnd->menus.input.cvars = console_cvar_list( query, query_length ); + if( !wnd->menus.input.cvars.size ) + return; + + wnd->menus.input.items.reserve( + wnd->menus.input.cvars.size + ); + + for( U32 idx = 0; idx < wnd->menus.input.cvars.size; ++idx ) { + CVAR* var = wnd->menus.input.cvars[idx]; + if( !var || !var->name.data ) + continue; + + GUI_CONTEXTMENU_ITEM* item = wnd->menus.input.items.push( {} ); + gui_contextmenu_set_item( + item, + var->name.data, + console_input_menu_item_cb, + wnd + ); + } + console_show_input_menu( wnd ); +} + +inline void console_open_history_menu( GUI_CONSOLEWINDOW* wnd ) { + console_close_input_menu( wnd ); + + if( !wnd || !wnd->input || !game || !game->console.history.size ) + return; + + wnd->menus.input.mode = CONSOLE_MENU_INPUT_HIST; + wnd->menus.input.history_size = game->console.history.size; + wnd->menus.input.items.reserve( game->console.history.size ); + + for( U32 idx = 0; idx < game->console.history.size; ++idx ) { + GUI_CONTEXTMENU_ITEM* item = wnd->menus.input.items.push( {} ); + gui_contextmenu_set_item( + item, + game->console.history[idx].data, + console_input_menu_item_cb, + wnd + ); + } + + console_show_input_menu( wnd ); +} + +inline U8 console_input_menu_suppressed( + GUI_CONSOLEWINDOW* wnd, + GUI_TEXTBOX* textbox +) { + if( !wnd->menus.input.suppressed ) + return 0; + + if( !strcmp( + wnd->menus.input.suppressed_value, + textbox->value + ) + ) return 1; + + wnd->menus.input.suppressed = 0; + wnd->menus.input.suppressed_value[0] = 0; + return 0; +} + +inline void console_sync_history_menu( + GUI_CONSOLEWINDOW* wnd +) { + if( !game->console.history.size ) { + console_close_input_menu( wnd ); + return; + } + + U8 stale = wnd->menus.input.mode != CONSOLE_MENU_INPUT_HIST + || wnd->menus.input.history_size != game->console.history.size + || !wnd->menus.input.items.size; + + if( stale ) { + console_open_history_menu( wnd ); + return; + } + + if( !wnd->menus.input.menu ) + console_show_input_menu( wnd ); + console_position_input_menu( wnd ); +} + +inline void console_sync_cvar_menu( + GUI_CONSOLEWINDOW* wnd, + const CONSOLE_INPUT_SEGMENT& segment +) { + const char* query = wnd->input->value + segment.start; + U32 cvar_count = varl ? varl->vars.size : 0; + U32 cached_length = (U32)strlen( wnd->menus.input.query ); + + U8 stale = wnd->menus.input.mode != CONSOLE_MENU_INPUT_CVAR + || wnd->menus.input.cvar_count != cvar_count + || cached_length != segment.length; + + if( !stale + && strncmp( + wnd->menus.input.query, + query, + segment.length + ) + ) stale = 1; + + if( stale ) { + console_open_cvar_menu( + wnd, + query, + segment.length + ); + return; + } + + if( !wnd->menus.input.menu && wnd->menus.input.items.size ) + console_show_input_menu( wnd ); + console_position_input_menu( wnd ); +} + +inline void console_sync_input_menu( GUI_CONSOLEWINDOW* wnd ) { + if( !wnd ) + return; + + U8 menu_interacting = wnd->menus.input.menu + && wnd->menus.input.menu->held; + if( !wnd->enabled + || !wnd->input + || ( !wnd->input->active && !menu_interacting ) + ) { + console_close_input_menu( wnd ); + return; + } + + if( console_input_menu_suppressed( wnd, wnd->input ) ) + return; + + CONSOLE_INPUT_SEGMENT segment = console_input_segment( wnd->input ); + if( !segment.valid ) { + console_close_input_menu( wnd ); + return; + } + + if( !segment.length ) + console_sync_history_menu( wnd ); + else + console_sync_cvar_menu( wnd, segment ); +} + +inline U8 console_textbox_key_pressed( GUI_TEXTBOX* textbox, U32 key ) { + U32 idx = key & 0xff; + return textbox->keys[idx] && !textbox->prevkeys[idx]; +} + +inline U8 console_handle_input_menu_keys( GUI_CONSOLEWINDOW* wnd ) { + if( !wnd || !wnd->input || !wnd->menus.input.menu ) + return 0; + + GUI_CONTEXTMENU* menu = wnd->menus.input.menu; + GUI_TEXTBOX* textbox = wnd->input; + + if( !gui_contextmenu_open( menu ) ) + return 0; + + CONSOLE_INPUT_SEGMENT segment = console_input_segment( textbox ); + if( !segment.valid ) + return 0; + + U8 valid_mode = 0; + if( wnd->menus.input.mode == CONSOLE_MENU_INPUT_HIST ) + valid_mode = !segment.length; + else if( wnd->menus.input.mode == CONSOLE_MENU_INPUT_CVAR + && segment.length + ) { + U32 cached_length = (U32)strlen( wnd->menus.input.query ); + valid_mode = cached_length == segment.length + && !strncmp( + wnd->menus.input.query, + textbox->value + segment.start, + segment.length + ); + } + + if( !valid_mode ) + return 0; + + U8 moved = 0; + if( console_textbox_key_pressed( textbox, SDLK_UP ) ) { + gui_contextmenu_move_selection( menu, -1, 1 ); + moved = 1; + } + else if( console_textbox_key_pressed( textbox, SDLK_DOWN ) ) { + gui_contextmenu_move_selection( menu, 1, 1 ); + moved = 1; + } + + if( moved ) { + textbox->active = 1; + textbox->blink = 1; + textbox->blinktime = gui_time(); + } + + if( !console_textbox_key_pressed( textbox, SDLK_RETURN ) + && !console_textbox_key_pressed( textbox, SDLK_KP_ENTER ) + ) return moved; + return console_apply_input_menu_selection( wnd ); +} + +void gui_consolewindow_draw_fn( void* ptr ) { + GUI_CONSOLEWINDOW* wnd = (GUI_CONSOLEWINDOW*)ptr; + I32 max_x = game->gl->canvas_size[0]; + I32 max_y = game->gl->canvas_size[1]; + + if( constrain_console( wnd, max_x, max_y ) + && wnd->active_edges != RESIZE_NONE + ) store_console_drag_origin( wnd ); + layout_console_window( wnd ); + console_position_input_menu( wnd ); + + I32 x = gui_relx( wnd ); + I32 y = gui_rely( wnd ); + + CLR clr = gui_is_fg_window( wnd ) ? ui_clr.border : ui_clr.border_inactive; + gui_draw_frect( x + wnd->w, y + 4, 4, wnd->h, ui_clr.shadow ); + gui_draw_frect( x + 4, y + wnd->h, wnd->w - 2, 4, CLR::BLACK() ); + gui_draw_frect( x, y, wnd->w, wnd->h, clr ); + gui_draw_frect( + x + GUI_CONSOLE_BORDER_WIDTH, + y + GUI_CONSOLE_BORDER_WIDTH, + wnd->w - GUI_CONSOLE_BORDER_WIDTH * 2, + wnd->h - GUI_CONSOLE_BORDER_WIDTH * 2, + ui_clr.bg + ); + gui_draw_frect( x, y, wnd->w, GUI_TITLE_HEIGHT, clr ); + gui_draw_frect( + x + GUI_CONSOLE_BORDER_WIDTH, + y + GUI_CONSOLE_BORDER_WIDTH, + wnd->w - GUI_CONSOLE_BORDER_WIDTH * 2, + GUI_TITLE_HEIGHT - GUI_CONSOLE_BORDER_WIDTH * 2, + ui_clr.bg + ); + + draw_console_output( wnd ); + + wnd->children.each( fn( GUI_BASE** ptr ) { + GUI_BASE* it = *ptr; + if( !it->enabled ) return; + if( it->draw_fn ) it->draw_fn( it ); + else dlog( "gui_editorwindow_draw_fn(): child %p has no draw_fn", it ); + } ); +} + +inline void console_accept_input( GUI_CONSOLEWINDOW* wnd ) { + GUI_TEXTBOX* textbox = wnd->input; + if( !textbox ) + return; + U32 enter = SDLK_RETURN & 0xff; + U32 kp_enter = SDLK_KP_ENTER & 0xff; + + U8 enter_pressed = + ( textbox->keys[enter] && !textbox->prevkeys[enter] ) || + ( textbox->keys[kp_enter] && !textbox->prevkeys[kp_enter] ); + + if( !enter_pressed ) + return; + + if( !console_submit( &game->console, textbox->value ) ) { + textbox->active = 1; + return; + } + + textbox->blinktime = gui_time(); + textbox->cursorpxout = 0; + textbox->cursorpxin = 0; + textbox->cursorout = 0; + textbox->cursorin = 0; + textbox->value[0] = 0; + textbox->active = 1; + textbox->blink = 1; + + wnd->scroll_offset = 0; +} + +inline I32 console_max_scroll( GUI_CONSOLEWINDOW* wnd ) { + if( !wnd ) + return 0; + I32 row_count = (I32)wnd->wrapped_lines.size; + I32 visible = max<I32>( 1, wnd->visible_rows ); + return max<I32>( 0, row_count - visible ); +} + +inline U8 console_mouse_over_output( + GUI_CONSOLEWINDOW* wnd, + VEC2 mouse +) { + if( !wnd ) + return 0; + + VEC2 pos = get_console_output_pos( wnd ); + VEC2 sz = get_console_output_sz( wnd ); + + return sz.x > 0 + && sz.y > 0 + && mouse.x >= pos.x + && mouse.x < pos.x + sz.x + && mouse.y >= pos.y + && mouse.y < pos.y + sz.y; +} + +inline I32 console_output_hit_test( + GUI_CONSOLEWINDOW* wnd, + VEC2 mouse +) { + if( !console_mouse_over_output( wnd, mouse ) ) + return -1; + + VEC2 pos = get_console_output_pos( wnd ); + VEC2 sz = get_console_output_sz( wnd ); + I32 line_height = console_output_line_height(); + if( line_height <= 0 ) + return -1; + + console_rebuild_wrapped_lines( + wnd, + &game->console, + (I32)sz.x + ); + + wnd->visible_rows = (I32)sz.y / line_height; + if( wnd->visible_rows <= 0 ) + return -1; + + wnd->scroll_offset = m_clamp<I32>( + wnd->scroll_offset, + 0, + console_max_scroll( wnd ) + ); + + I32 from_bottom = ( + (I32)( pos.y + sz.y ) + - 1 + - (I32)mouse.y + ) / line_height; + + if( from_bottom < 0 || from_bottom >= wnd->visible_rows ) + return -1; + + I32 index = (I32)wnd->wrapped_lines.size + - 1 + - wnd->scroll_offset + - from_bottom; + if( index < 0 || index >= (I32)wnd->wrapped_lines.size ) + return -1; + return index; +} + +inline U8 console_handle_output_left_mouse( + GUI_CONSOLEWINDOW* wnd, + U8 left_down, + U8 left_pressed, + U8 over_output, + I32 hit +) { + if( !left_down ) + wnd->output_selecting = 0; + + if( left_pressed && !over_output ) { + console_close_output_menu( wnd ); + wnd->output_selection_active = 0; + wnd->output_selecting = 0; + return 1; + } + + if( !left_pressed ) { + if( left_down && wnd->output_selecting && hit >= 0 ) + wnd->output_selection_cursor = wnd->wrapped_lines[hit].source_id; + return 0; + } + + console_close_output_menu( wnd ); + console_close_input_menu( wnd ); + + if( wnd->input ) + wnd->input->active = 0; + + if( hit < 0 ) { + wnd->output_selection_active = 0; + wnd->output_selecting = 0; + return 1; + } + + U64 source_id = wnd->wrapped_lines[hit].source_id; + U8 extend = input.keys[SDLK_LSHIFT & 0xff] + || input.keys[SDLK_RSHIFT & 0xff]; + + if( !extend || !wnd->output_selection_active ) + wnd->output_selection_anchor = source_id; + + wnd->output_selection_cursor = source_id; + wnd->output_selection_active = 1; + wnd->output_selecting = 1; + return 0; +} + +inline void console_handle_output_right_mouse( + GUI_CONSOLEWINDOW* wnd, + U8 right_pressed, + U8 over_output, + I32 hit, + VEC2 mouse +) { + if( !right_pressed || !over_output || hit < 0 ) + return; + + U64 source_id = wnd->wrapped_lines[hit].source_id; + + if( !console_output_line_selected( wnd, source_id ) ) { + wnd->output_selection_anchor = source_id; + wnd->output_selection_cursor = source_id; + wnd->output_selection_active = 1; + } + + wnd->output_selecting = 0; + + if( wnd->input ) + wnd->input->active = 0; + + console_open_output_menu( + wnd, + (I32)mouse.x, + (I32)mouse.y + ); +} + +inline void console_handle_output_mouse( + GUI_CONSOLEWINDOW* wnd, + U8 left_down, + U8 left_pressed, + U8 right_pressed +) { + if( !wnd ) + return; + + if( wnd->output_selection_active + && !console_validate_output_selection( wnd, &game->console ) + ) console_close_output_menu( wnd ); + + VEC2 mouse = input.mouse.pos; + + if( console_mouse_over_menu( wnd->menus.input.menu, mouse ) + || console_mouse_over_menu( wnd->menus.output.menu, mouse ) + ) { + if( !left_down ) + wnd->output_selecting = 0; + return; + } + + U8 over_output = console_mouse_over_output( wnd, mouse ); + I32 hit = over_output + ? console_output_hit_test( wnd, mouse ) + : -1; + + if( console_handle_output_left_mouse( + wnd, + left_down, + left_pressed, + over_output, + hit + ) + ) return; + + console_handle_output_right_mouse( + wnd, + right_pressed, + over_output, + hit, + mouse + ); +} + +inline void console_handle_output_copy_key( GUI_CONSOLEWINDOW* wnd ) { + if( !wnd ) + return; + + U8 c_down = input.keys['c']; + U8 c_pressed = c_down && !wnd->copy_key_held; + + wnd->copy_key_held = c_down; + + U8 ctrl_down = input.keys[SDLK_LCTRL & 0xff] + || input.keys[SDLK_RCTRL & 0xff]; + + if( c_pressed && ctrl_down && wnd->output_selection_active ) + console_copy_output_selection( wnd ); +} + +void console_handle_scroll( GUI_CONSOLEWINDOW* wnd ) { + if( !wnd + || !input.mouse.wheel + || !console_mouse_over_output( wnd, input.mouse.pos ) + ) return; + + I32 delta = 0; + + if( input.mouse.wheel == 255 ) + delta = -1; + else if( input.mouse.wheel == 1 ) + delta = 1; + else + return; + + wnd->scroll_offset = m_clamp<I32>( + wnd->scroll_offset + delta, + 0, + console_max_scroll( wnd ) + ); +} + +inline U8 console_handle_resize_input( + GUI_CONSOLEWINDOW* wnd, + I32 max_x, + I32 max_y, + U8 left_down, + U8 left_pressed, + RESIZE_EDGES* hover_edges +) { + *hover_edges = RESIZE_NONE; + + if( constrain_console( wnd, max_x, max_y ) + && wnd->active_edges != RESIZE_NONE + ) store_console_drag_origin( wnd ); + layout_console_window( wnd ); + + if( wnd->active_edges != RESIZE_NONE ) { + U8 lost_focus = SDL_GetKeyboardFocus() != game->gl->window; + + if( !left_down || lost_focus ) + console_resize_end( wnd ); + else { + resize_console_window( wnd, max_x, max_y ); + set_console_cursor( wnd->active_edges ); + return 1; + } + } + *hover_edges = get_console_active_edges( + wnd, input.mouse.pos, + max_x, max_y + ); + if( left_pressed && *hover_edges != RESIZE_NONE ) { + console_resize_begin( wnd, *hover_edges ); + set_console_cursor( *hover_edges ); + return 1; + } + return 0; +} + +void gui_consolewindow_input_fn( void* ptr ) { + GUI_CONSOLEWINDOW* wnd = (GUI_CONSOLEWINDOW*)ptr; + I32 max_x = game->gl->canvas_size[0]; + I32 max_y = game->gl->canvas_size[1]; + + U8 left_down = input.mouse.left; + U8 left_pressed = left_down && !wnd->lmb_held; + wnd->lmb_held = left_down; + + U8 right_down = input.mouse.right; + U8 right_pressed = right_down && !wnd->rmb_held; + wnd->rmb_held = right_down; + + console_handle_output_copy_key( wnd ); + + if( input.mouselock ) + input_capture_mouse( 0 ); + else + input_reset_mouse_accumulator(); + + RESIZE_EDGES active_edges = RESIZE_NONE; + if( console_handle_resize_input( wnd, + max_x, max_y, + left_down, left_pressed, + &active_edges + ) + ) return; + + U8 over_input = is_over_console_input( wnd, input.mouse.pos ); + console_handle_scroll( wnd ); + console_handle_output_mouse( wnd, left_down, left_pressed, right_pressed ); + + RESIZE_EDGES cursor_edges = + !left_down || left_pressed + ? active_edges + : RESIZE_NONE; + set_console_cursor( cursor_edges, over_input ); + + console_sync_input_menu( wnd ); + + gui_base_input_fn( wnd ); + + if( !wnd->enabled ) + return; + + U8 menu_consumed = console_handle_input_menu_keys( wnd ); + if( !menu_consumed ) + console_accept_input( wnd ); + + console_sync_input_menu( wnd ); + + constrain_console( wnd, max_x, max_y ); + layout_console_window( wnd ); + console_position_input_menu( wnd ); +} + +inline void console_set_input_focused( + GUI_CONSOLEWINDOW* wnd, + U8 focused +) { + if( !wnd || !wnd->input ) + return; + + GUI_TEXTBOX* textbox = wnd->input; + textbox->active = focused; + textbox->heldkey = 0; + textbox->m1held = 0; + + memcpy( + textbox->keys, + input.keys, + sizeof( textbox->keys ) + ); + memcpy( + textbox->prevkeys, + input.keys, + sizeof( textbox->prevkeys ) + ); + if( focused ) { + textbox->blink = 1; + textbox->blinktime = gui_time(); + } +} + +void console_close_cb( void* ptr ) { + GUI_BUTTON* btn = (GUI_BUTTON*)ptr; + GUI_VIEW* view = (GUI_VIEW*)btn->parent; + GUI_CONSOLEWINDOW* wnd = (GUI_CONSOLEWINDOW*)view->parent; + console_close_output_menu( wnd ); + console_close_input_menu( wnd ); + console_resize_end( wnd ); + wnd->output_selecting = 0; + wnd->copy_key_held = 0; + console_set_input_focused( wnd, 0 ); + wnd->enabled = 0; +} + +GUI_CONSOLEWINDOW* gui_consolewindow_create( I32 x, I32 y, I32 w, I32 h ) { + GUI_CONSOLEWINDOW* wnd = new GUI_CONSOLEWINDOW{}; + wnd->x = x; + wnd->y = y; + wnd->w = w; + wnd->h = h; + wnd->locked = 0; + wnd->draw_fn = gui_consolewindow_draw_fn; + wnd->input_fn = gui_consolewindow_input_fn; + strcpy( wnd->name, "CONSOLE" ); + _gui.windows.push( wnd ); + gui_set_view( wnd ); + gui_view( + GUI_CONSOLE_BORDER_WIDTH, + GUI_CONSOLE_BORDER_WIDTH, + w - GUI_CONSOLE_BORDER_WIDTH * 2, + h - GUI_CONSOLE_BORDER_WIDTH * 2 + ); + return wnd; +} + +GUI_CONSOLEWINDOW* gui_consolewindow( I32 x, I32 y, I32 w, I32 h ) { + GUI_CONSOLEWINDOW* wnd = gui_consolewindow_create( x, y, w, h ); + + wnd->close = gui_button( + get_close_x( wnd->w ), + GUI_CONSOLE_PADDING, + GUI_BUTTON_SIZE, + GUI_BUTTON_SIZE, + "X", + console_close_cb + ); + wnd->input = gui_textbox( + GUI_CONSOLE_PADDING, + get_input_y( wnd->h ), + get_input_w( wnd->w ), + GUI_CONSOLE_INPUT_HEIGHT, + "", 0x400, 0 + ); + wnd->input->active = 1; + wnd->title = gui_title( "console" ); + + GUI_CONTEXTMENU_ITEM* copy = wnd->menus.output.items.push( {} ); + gui_contextmenu_set_item( copy, "copy", console_copy_output_selection, wnd ); + + layout_console_window( wnd ); + return wnd; +} + +U8 gui_consolewindow_input_focused() { + GUI_CONSOLEWINDOW* wnd = game + ? game->console_window + : 0; + + return wnd + && wnd->input + && wnd->enabled + && wnd->input->active; +} + +U8 gui_consolewindow_is_open() { + GUI_CONSOLEWINDOW* wnd = game + ? game->console_window + : 0; + return wnd + && wnd->enabled; +} + +void gui_consolewindow_toggle() { + GUI_CONSOLEWINDOW* wnd = game + ? game->console_window + : 0; + if( !wnd ) + return; + + set_console_cursor( RESIZE_NONE ); + + if( wnd->enabled ) { + console_close_output_menu( wnd ); + console_close_input_menu( wnd ); + console_resize_end( wnd ); + wnd->output_selecting = 0; + wnd->copy_key_held = 0; + console_set_input_focused( wnd, 0 ); + } + + wnd->enabled ^= 1; + + if( wnd->enabled ) { + gui_bring_to_top( wnd ); + console_set_input_focused( wnd, 1 ); + wnd->copy_key_held = input.keys['c']; + wnd->rmb_held = input.mouse.right; + wnd->lmb_held = input.mouse.left; + } +} diff --git a/src/gui/console.h b/src/gui/console.h index e69de29..0730f15 100644 --- a/src/gui/console.h +++ b/src/gui/console.h @@ -0,0 +1,67 @@ +#pragma once +#include "base.h" + +enum RESIZE_EDGES : U8 { + RESIZE_NONE = 0, + RESIZE_LEFT = 1 << 0, + RESIZE_RIGHT = 1 << 1, + RESIZE_BOTTOM = 1 << 2 +}; + +enum CONSOLE_MENU_INPUT_MODE : U8 { + CONSOLE_MENU_INPUT_NONE = 0, + CONSOLE_MENU_INPUT_HIST = 1, + CONSOLE_MENU_INPUT_CVAR = 2 +}; + +struct CVAR; + +struct CONSOLE_WRAPPED_LINE { + STR text{}; + U64 source_id{}; +}; + +struct GUI_CONSOLEWINDOW : GUI_WINDOW { + struct GUI_CONSOLEWINDOW_MENU { + struct GUI_CONSOLEWINDOW_INPUT_MENU { + char suppressed_value[GUI_TEXTBOX_MAX]{}; + LIST<GUI_CONTEXTMENU_ITEM> items{}; + CONSOLE_MENU_INPUT_MODE mode{}; + char query[GUI_TEXTBOX_MAX]{}; + GUI_CONTEXTMENU* menu{}; + LIST<CVAR*> cvars{}; + U32 history_size{}; + U32 cvar_count{}; + U8 suppressed{}; + } input; + + struct GUI_CONSOLEWINDOW_OUTPUT_MENU { + LIST<GUI_CONTEXTMENU_ITEM> items{}; + GUI_CONTEXTMENU* menu{}; + } output; + } menus; + + LIST<CONSOLE_WRAPPED_LINE> wrapped_lines{}; + U64 wrapped_revision{(U64)-1}; + I32 wrapped_width{-1}; + U64 seen_revision{}; + I32 scroll_offset{}; + I32 visible_rows{}; + + U64 output_selection_anchor{}; + U64 output_selection_cursor{}; + U8 output_selection_active{}; + U8 output_selecting{}; + U8 copy_key_held{}; + + GUI_TEXTBOX* input{}; + GUI_BUTTON* close{}; + GUI_TITLE* title{}; + + RESIZE_EDGES active_edges = RESIZE_NONE; + VEC2 drag_start_mouse{}; + VEC2 drag_start_pos{}; + VEC2 drag_start_sz{}; + U8 lmb_held{}; + U8 rmb_held{}; +}; diff --git a/src/gui/contextmenu.cpp b/src/gui/contextmenu.cpp new file mode 100644 index 0000000..73cbafd --- /dev/null +++ b/src/gui/contextmenu.cpp @@ -0,0 +1,437 @@ +#include "base.h" + +U8 gui_contextmenu_item_enabled( GUI_CONTEXTMENU_ITEM* item ) { + if( !item ) + return 0; + + if( item->enabled_cb ) + return item->enabled_cb( item ); + + return item->enabled; +} + +I32 gui_contextmenu_title_h( GUI_CONTEXTMENU* menu ) { + return ( menu && menu->title[0] ) ? 18 : 0; +} + +I32 gui_contextmenu_row_h() { + return 20; +} + +I32 gui_contextmenu_width( LIST<GUI_CONTEXTMENU_ITEM>* items, const char* title ) { + I32 width = 0; + if( title && title[0] ) { + I32 text_w = 0; + gui_draw_get_str_bounds( &text_w, 0, FNT_JPN12, "%s", title ); + width = max( width, text_w + 18 ); + } + + if( !items ) + return width; + + for( I32 i = 0; i < (I32)items->size; ++i ) { + I32 text_w = 0; + gui_draw_get_str_bounds( &text_w, 0, FNT_JPN12, "%s", ( *items )[i].text ); + width = max( width, text_w + 20 ); + } + + return width; +} + +I32 gui_contextmenu_height( I32 rows, const char* title ) { + I32 height = 4 + gui_contextmenu_row_h() * rows; + if( title && title[0] ) + height += 18 + 1; + return max( height, gui_contextmenu_row_h() + 4 ); +} + +static I32 gui_contextmenu_items_y( GUI_CONTEXTMENU* menu ) { + if( !menu ) + return 0; + I32 y = gui_rely( menu ) + 2; + if( menu->title[0] ) + y += gui_contextmenu_title_h( menu ) + 1; + return y; +} + +I32 gui_contextmenu_hit_test( GUI_CONTEXTMENU* menu, I32 mx, I32 my ) { + if( !menu || !menu->items ) + return -1; + + I32 x = gui_relx( menu ); + I32 y = gui_rely( menu ); + if( mx < x || mx >= x + menu->w || my < y || my >= y + menu->h ) + return -1; + + I32 items_y = gui_contextmenu_items_y( menu ); + if( my < items_y ) + return -1; + + I32 row = ( my - items_y ) / gui_contextmenu_row_h(); + if( row < 0 || row >= menu->visible_rows ) + return -1; + + I32 idx = menu->first_visible + row; + if( idx < 0 || idx >= (I32)menu->items->size ) + return -1; + + return idx; +} + +static I32 gui_contextmenu_draw_frame( + GUI_CONTEXTMENU* menu, + I32 x, I32 y, + CLR border +) { + gui_draw_frect( x, y, menu->w, menu->h, border ); + gui_draw_frect( + x + 1, + y + 1, + max( 1, menu->w - 2 ), + max( 1, menu->h - 2 ), + ui_clr.bg_sec + ); + + I32 item_y = y + 2; + + if( menu->title[0] ) { + gui_draw_str( + x + 8, item_y + 1, + ALIGN_L, FNT_JPN12, + ui_clr.txt, + "%s", menu->title + ); + item_y += gui_contextmenu_title_h( menu ); + gui_draw_line( + x + 1, item_y, + x + menu->w - 2, item_y, + border + ); + ++item_y; + } + return item_y; +} + +static void gui_contextmenu_draw_rows( + GUI_CONTEXTMENU* menu, + I32 x, I32 y, + I32 hit, + U8 left_down +) { + I32 item_count = (I32)menu->items->size; + I32 first = m_clamp<I32>( + menu->first_visible, + 0, item_count + ); + I32 rows = min<I32>( + menu->visible_rows, + item_count - first + ); + + CLR hover = CLR::blend( ui_clr.border, ui_clr.bg, 0.7f ); + CLR active = CLR::blend( ui_clr.bg_sec, ui_clr.border, 0.22f ); + + gui_draw_push_clip( + x + 1, y, + max<I32>( 0, menu->w - 2 ), + max<I32>( 0, rows * gui_contextmenu_row_h() ) + ); + + for( I32 row = 0; row < rows; ++row ) { + I32 idx = first + row; + GUI_CONTEXTMENU_ITEM* item = &( *menu->items )[idx]; + U8 enabled = gui_contextmenu_item_enabled( item ); + U8 pressed = menu->held + && menu->held_idx == idx + && left_down + && enabled; + CLR fill = pressed + ? active + : ( hit == idx || menu->selected_idx == idx ) + ? hover + : ui_clr.bg_sec; + I32 row_y = y + row * gui_contextmenu_row_h(); + + gui_draw_frect( + x + 1, row_y, + max<I32>( 1, menu->w - 2 ), + gui_contextmenu_row_h(), + fill + ); + gui_draw_str( + x + 8, row_y + 2, + ALIGN_L, FNT_JPN12, + enabled ? ui_clr.txt : ui_clr.txt_inactive, + "%s", item->text + ); + } + gui_draw_pop_clip(); +} + +void gui_contextmenu_draw_fn( void* ptr ) { + GUI_CONTEXTMENU* menu = (GUI_CONTEXTMENU*)ptr; + if( !menu || !menu->items || !menu->items->size ) + return; + + I32 x = gui_relx( menu ); + I32 y = gui_rely( menu ); + I32 mx = 0, my = 0; + gui_cursor_pos( &mx, &my ); + + CLR border = gui_is_fg_window( menu ) + ? ui_clr.border + : ui_clr.border_inactive; + + I32 item_y = gui_contextmenu_draw_frame( + menu, + x, y, + border + ); + + gui_contextmenu_draw_rows( + menu, x, item_y, + gui_contextmenu_hit_test( menu, mx, my ), + gui_mbutton_down( GUI_MBTNLEFT ) + ); +} + +void gui_contextmenu_input_fn( void* ptr ) { + GUI_CONTEXTMENU* menu = (GUI_CONTEXTMENU*)ptr; + if( !menu || !menu->items || !menu->items->size ) + return; + + U8 m1 = gui_mbutton_down( GUI_MBTNLEFT ); + U8 m2 = gui_mbutton_down( GUI_MBTNRIGHT ); + if( menu->initheld ) { + if( m1 || m2 ) + return; + menu->initheld = 0; + return; + } + + I32 mx = 0, my = 0; + gui_cursor_pos( &mx, &my ); + I32 hit = gui_contextmenu_hit_test( menu, mx, my ); + + if( m1 || m2 ) { + if( !menu->held ) { + menu->held = 1; + menu->held_idx = hit; + } + return; + } + + if( !menu->held ) + return; + + if( hit == -1 ) { + gui_hide_contextmenu( menu ); + return; + } + + if( hit == menu->held_idx ) { + GUI_CONTEXTMENU_ITEM* item = &( *menu->items )[hit]; + if( gui_contextmenu_item_enabled( item ) && item->cb ) { + menu->selected_idx = hit; + + GUI_BASE* parent = menu->parent; + item->cb( item->data ); + + if( parent + && parent->children.idx_of( (GUI_BASE*)menu ) != -1 + ) gui_hide_contextmenu( menu ); + return; + } + } + + menu->held = 0; + menu->held_idx = -1; +} + +void gui_hide_contextmenu( GUI_CONTEXTMENU* menu ) { + if( !menu ) + return; + + if( menu->owner_ref && *menu->owner_ref == menu ) + *menu->owner_ref = 0; + + if( menu->parent ) { + I32 idx = menu->parent->children.idx_of( (GUI_BASE*)menu ); + if( idx != -1 ) + menu->parent->children.erase( idx ); + } + + gui_free( menu ); +} + +U8 gui_contextmenu_open( GUI_CONTEXTMENU *menu ) { + return !menu ? 0 : menu->enabled; +} + +void gui_contextmenu_set_item( + GUI_CONTEXTMENU_ITEM* item, + const char* text, + GUI_CONTEXTMENU_CALLBACK cb, + void* data, + GUI_CONTEXTMENU_ENABLED_CALLBACK enabled_cb +) { + if( !item ) + return; + + item->items.clear(); + snprintf( item->text, sizeof( item->text ), "%s", text ? text : "" ); + item->cb = cb; + item->data = data; + item->enabled = 1; + item->enabled_cb = enabled_cb; +} + +GUI_CONTEXTMENU_ITEM* gui_contextmenu_selected( GUI_CONTEXTMENU* menu ) { + if( !menu || !menu->items ) + return 0; + + if( menu->selected_idx < 0 || menu->selected_idx >= (I32)menu->items->size ) + return 0; + + return &( *menu->items )[menu->selected_idx]; +} + +static I32 gui_contextmenu_next_enabled( + GUI_CONTEXTMENU* menu, + I32 delta, + U8 wrap +) { + I32 item_count = (I32)menu->items->size; + I32 direction = delta < 0 ? -1 : 1; + I32 candidate = menu->selected_idx; + U8 has_candidate = candidate >= 0 + && candidate < item_count; + for( I32 scanned = 0; scanned < item_count; ++scanned ) { + if( !has_candidate ) { + candidate = direction < 0 + ? item_count - 1 + : 0; + has_candidate = 1; + } + else { + candidate += direction; + + if( candidate < 0 || candidate >= item_count ) { + if( !wrap ) + return -1; + + candidate = direction < 0 + ? item_count - 1 + : 0; + } + } + + if( gui_contextmenu_item_enabled( + &( *menu->items )[candidate] + ) ) return candidate; + } + return -1; +} + +void gui_contextmenu_reveal_selection( + GUI_CONTEXTMENU* menu +) { + if( !menu || !menu->items || menu->visible_rows <= 0 ) + return; + + I32 selected = menu->selected_idx; + + if( selected < 0 || selected >= (I32)menu->items->size ) + return; + + if( selected < menu->first_visible ) + menu->first_visible = selected; + else if( selected >= menu->first_visible + menu->visible_rows ) + menu->first_visible = selected - menu->visible_rows + 1; + + I32 max_first = max<I32>( 0, + (I32)menu->items->size - menu->visible_rows + ); + + menu->first_visible = m_clamp<I32>( + menu->first_visible, + 0, max_first + ); +} + +void gui_contextmenu_move_selection( + GUI_CONTEXTMENU* menu, + I32 delta, + U8 wrap +) { + if( !menu || !menu->items || !menu->items->size || !delta ) + return; + + I32 selected = gui_contextmenu_next_enabled( + menu, + delta, + wrap + ); + + if( selected < 0 ) + return; + + menu->selected_idx = selected; + gui_contextmenu_reveal_selection( menu ); +} + +GUI_CONTEXTMENU* gui_show_contextmenu( + I32 x, I32 y, + LIST<GUI_CONTEXTMENU_ITEM>* items, + GUI_BASE* parent, + const char* title, + I32 max_visible_rows, + GUI_CONTEXTMENU** owner_ref +) { + if( !parent || !items || !items->size ) + return 0; + + GUI_WINDOW* owner_window = gui_get_parent_wnd( parent ); + if( !owner_window ) + return 0; + + if( owner_ref && *owner_ref ) + gui_hide_contextmenu( *owner_ref ); + + GUI_CONTEXTMENU* menu = new GUI_CONTEXTMENU; + menu->items = items; + menu->draw_fn = gui_contextmenu_draw_fn; + menu->input_fn = gui_contextmenu_input_fn; + menu->enabled = 1; + menu->initheld = 1; + menu->held = 0; + menu->held_idx = -1; + menu->selected_idx = -1; + menu->parent = owner_window; + menu->owner_ref = owner_ref; + I32 item_count = (I32)items->size; + menu->max_visible_rows = max_visible_rows > 0 + ? min<I32>( max_visible_rows, item_count ) + : item_count; + menu->visible_rows = menu->max_visible_rows; + menu->first_visible = max<I32>( 0, + item_count - menu->visible_rows + ); + + snprintf( menu->name, sizeof( menu->name ), "GUI_CONTEXTMENU" ); + snprintf( menu->title, sizeof( menu->title ), "%s", title ? title : "" ); + + menu->w = gui_contextmenu_width( items, title ); + menu->h = gui_contextmenu_height( menu->visible_rows, title ); + menu->xbound = menu->w; + menu->ybound = menu->h; + + menu->x = x - gui_relx( owner_window ); + menu->y = y - gui_rely( owner_window ); + owner_window->children.push( menu ); + + if( owner_ref ) + *owner_ref = menu; + + return menu; +} diff --git a/src/gui/textbox.cpp b/src/gui/textbox.cpp index c367193..0a9745f 100644 --- a/src/gui/textbox.cpp +++ b/src/gui/textbox.cpp @@ -1,51 +1,363 @@ #include "base.h" +#include "../render/gl_2d_font.h" #include "../util/input.h" #include <SDL_keycode.h> const U32 TEXTBOX_TITLE_OFFSET = 15; -U8 is_printable_char( U8 key ) { - return key >= 32 && key < 127; +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<I32>( 1, (I32)font->char_height ) + : 1; } -void gui_textbox_draw_newline_str( GUI_TEXTBOX* tb, I32 x, I32 y, CLR clr, I32* endposx, I32* endposy ) { - char linebuf[GUI_TEXTBOX_MAX]; - char* start = tb->value; - I32 tw, th; +static U32 gui_textbox_line_start( const char* text, U32 cursor ) { + while( cursor > 0 && text[cursor - 1] != '\n' ) + --cursor; + return cursor; +} - for( char* c = start; !!*c; ++c ) { - if( *c == '\n' ) { - U32 len = (U32)( c - start ); - memcpy( linebuf, start, len ); - linebuf[len] = 0; +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; +} - gui_draw_str( x, y, ALIGN_L, FNT_JPN12, clr, linebuf ); - gui_draw_get_str_bounds( &tw, &th, FNT_JPN12, linebuf ); - y += th + 1; +static U32 gui_textbox_selection_start( const GUI_TEXTBOX* tb ) { + return min<U32>( tb->cursorin, tb->cursorout ); +} - *endposx = x + tw; - *endposy = y + th; +static U32 gui_textbox_selection_end( const GUI_TEXTBOX* tb ) { + return max<U32>( tb->cursorin, tb->cursorout ); +} - if( *(c+1) == 0 ) - break; +static U8 gui_textbox_shift_down( const GUI_TEXTBOX* tb ) { + return tb->keys[SDLK_LSHIFT & 0xff] + || tb->keys[SDLK_RSHIFT & 0xff]; +} - start = ++c; - } - else if( (*c+1) == 0 ) { - U32 len = (U32)( c - start ); - memcpy( linebuf, start, len ); - linebuf[len] = 0; +static I32 gui_textbox_range_width( const char* text, U32 start, U32 end ) { + GL_FONT* font = _gui.fonts.jpn12.glfnt; + if( !font ) + return 0; - gui_draw_str( x, y, ALIGN_L, FNT_JPN12, clr, linebuf ); - gui_draw_get_str_bounds( &tw, &th, FNT_JPN12, linebuf ); - y += th + 1; - *endposx = x + tw; - *endposy = y + th; + 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<U32>( tb->cursorin, length ); + tb->cursorout = min<U32>( 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<U32>( 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<I32>( 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<I32>( 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<U32>( + gui_textbox_selection_start( tb ), + length + ); + U32 end = min<U32>( + 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<U32>( tb->maxlen, GUI_TEXTBOX_MAX ); + + U32 start = min<U32>( + gui_textbox_selection_start( tb ), + length + ); + U32 end = min<U32>( + 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<U32>( 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<U32>( 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<U32>( 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 ) { @@ -56,154 +368,574 @@ void gui_textbox_draw_blinker( GUI_TEXTBOX* tb, I32 x, I32 y, CLR clr ) { tb->blinktime = gui_time(); } - if( tb->blink ) - gui_draw_str( x, y, ALIGN_L, FNT_JPN12, clr, "|" ); + if( tb->blink ) { + I32 caret_h = max<I32>( 1, + gui_textbox_line_height() - 2 + ); + gui_draw_frect( + x, y + 1, + 1, caret_h, + clr + ); + } } -void gui_textbox_draw_fn( void* ptr ) { - GUI_TEXTBOX* tb = (GUI_TEXTBOX*)ptr; +static I32 gui_textbox_newline_width() { + GL_FONT* font = _gui.fonts.jpn12.glfnt; - I32 x = gui_relx( tb ); - I32 y = gui_rely( tb ); + if( !font ) + return 1; - gui_draw_str( x, y, ALIGN_L, FNT_JPN12, ui_clr.txt, tb->name ); - y += TEXTBOX_TITLE_OFFSET; + return max<I32>( 1, + (I32)( font->glyphs[(U8)' '].advance_x + 0.5f ) + ); +} - CLR col = gui_is_fg_window( tb )? ui_clr.border : ui_clr.border_inactive; +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; - gui_draw_frect( x, y, tb->w, tb->h, col ); - gui_draw_frect( x+1, y+1, tb->w-2, tb->h-2, ui_clr.bg_sec ); + U32 length = (U32)strlen( tb->value ); + U32 selection_start = min<U32>( + gui_textbox_selection_start( tb ), length + ); + U32 selection_end = min<U32>( + 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<U32>( + selection_start, line_start + ); + U32 range_end = min<U32>( + 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 + ); + } - CLR clr = tb->active? ui_clr.txt : ui_clr.txt_inactive; - I32 ypos = y + 3; - I32 xpos = x + 2; + if( line_end >= length || line_end >= selection_end ) + break; - I32 endposx, endposy; + line_start = line_end + 1; + ++row; + } +} - if( tb->allow_newl ) { - gui_textbox_draw_newline_str( tb, xpos, ypos, clr, &endposx, &endposy ); - } else { - gui_draw_str( xpos, ypos, ALIGN_L, FNT_JPN12, clr, tb->value ); +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<I32>( 0, tb->w - 2 ), + max<I32>( 0, tb->h - 2 ), + ui_clr.bg_sec + ); +} - I32 tw, th; - gui_draw_get_str_bounds( &tw, &th, FNT_JPN12, tb->value ); - endposx = xpos + tw; - endposy = ypos; +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<I32>( 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<I32>( 0, + max<I32>( text_w, tb->cursorpxout + 1 ) - content_w + ); + + tb->scrollpx = m_clamp<I32>( + tb->scrollpx, 0, max_scroll_x + ); + + if( !tb->allow_newl ) { + tb->cursorpy = 0; + tb->scrollpy = 0; + return; } - gui_textbox_draw_blinker( tb, endposx, endposy, clr ); + 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<I32>( + tb->scrollpy, + 0, + max<I32>( 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<I32>( 0, tb->w - 2 ), + max<I32>( 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<I32>( 1, tb->w - 4 ), + max<I32>( 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; - I32 w = tb->w; - I32 h = tb->h; - U8 m1 = gui_mbutton_down( 0 ); - I32 mx, my; + U8 m1 = gui_mbutton_down( GUI_MBTNLEFT ); + I32 mx = 0; + I32 my = 0; gui_cursor_pos( &mx, &my ); - U8 inbounds = mx >= x && mx <= x + w && my >= y && my <= y + h; + U8 inbounds = mx >= x + && mx < x + tb->w + && my >= y + && my < y + tb->h; - if( m1 ) { - if( inbounds ) - tb->m1held = 1; - else - tb->active = 0; - } else { - if( inbounds && tb->m1held ) { - tb->active = 1; - tb->blink = 1; - tb->blinktime = gui_time(); - } + 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 len = strlen( tb->value ); - - for( U32 i = 0; i < 0xff; ++i ) { - U8 key = tb->keys[i]; - U8 prev = tb->prevkeys[i]; - - if( !key && prev && i == tb->heldkey ) - tb->heldkey = 0; - else if( key && !prev ) { - if( is_printable_char( i ) && len < tb->maxlen - 1 ) { - if( tb->keys[SDLK_LSHIFT & 0xff] || tb->keys[SDLK_RSHIFT & 0xff] ) - tb->value[len] = (char)i + ('a' - 'A'); - else - tb->value[len] = (char)i; - tb->value[++len] = 0; - } - else if( i == '\b' ) { - if( len > 0 ) { - tb->value[--len] = 0; - } - } - else if( i == '\r' ) { - if( tb->allow_newl ) { - tb->value[len] = '\n'; - tb->value[++len] = 0; - } - else { - tb->active = 0; - } - } else { - if( i == '\x1b' ) - tb->active = 0; - if( tb->cb ) - tb->cb( tb ); - continue; - } - - tb->heldkey = i; - tb->heldtime = gui_time(); - if( tb->cb ) - tb->cb( tb ); + U32 length = strlen( tb->value ); + tb->cursorin = min<U32>( tb->cursorin, length ); + tb->cursorout = min<U32>( 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( !tb->heldkey ) + if( !gui_textbox_key_repeatable( tb, tb->heldkey ) ) return; - F32 delta = gui_time() - tb->heldtime; - if( delta <= 0.75f ) + F32 now = gui_time(); + if( now - tb->heldtime <= 0.75f + || now - tb->repeattime <= 0.05f ) return; - if( gui_time() - tb->repeattime > 0.05f ) { - U32 len = strlen( tb->value ); - if( is_printable_char( tb->heldkey ) && len < (tb->maxlen - 1) ) { - if( tb->pre_cb ) tb->pre_cb( tb ); + 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( tb->keys[SDLK_LSHIFT & 0xff] || tb->keys[SDLK_RSHIFT & 0xff] ) - tb->value[len] = tb->heldkey + ('a' - 'A'); - else - tb->value[len] = tb->heldkey; - tb->value[++len] = 0; + if( handled ) + gui_textbox_apply_key_result( tb, result, now ); - if( tb->cb ) tb->cb( tb ); - } - else if( tb->heldkey == '\b' && len > 0 ) { - if( tb->pre_cb ) tb->pre_cb( tb ); - tb->value[--len] = 0; - if( tb->cb ) tb->cb( tb ); + 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; + } - tb->repeattime = gui_time(); + 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; - memcpy( tb->prevkeys, tb->keys, 0xff ); - memcpy( tb->keys, input.keys, 0xff ); + 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 ); @@ -217,7 +949,7 @@ void gui_textbox_input_fn( void* ptr ) { } 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; + GUI_TEXTBOX* tb = new GUI_TEXTBOX{}; tb->x = x; tb->y = y; @@ -234,13 +966,22 @@ GUI_TEXTBOX* gui_textbox( I32 x, I32 y, I32 w, I32 h, const char* title, U32 max 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, 0xff ); - memset( tb->prevkeys, 0, 0xff ); + memset( tb->keys, 0, 0x100 ); + memset( tb->prevkeys, 0, 0x100 ); + + tb->blink = 1; + tb->blinktime = gui_time(); gui_get_view()->children.push( tb ); |
