#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 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( 0, width ), (F32)max( 0, bottom - top ) }; } inline U8 constrain_console( GUI_CONSOLEWINDOW* wnd, I32 max_x, I32 max_y ) { max_x = max( 1.0f, max_x ); max_y = max( 1.0f, max_y ); I32 oldx = wnd->x; I32 oldy = wnd->y; I32 oldw = wnd->w; I32 oldh = wnd->h; I32 minh = min( GUI_CONSOLE_MINIMUM_HEIGHT, max_y ); I32 minw = min( GUI_CONSOLE_MINIMUM_WIDTH, max_x ); wnd->w = m_clamp( wnd->w, minw, max_x ); wnd->h = m_clamp( wnd->h, minh, max_y ); wnd->x = m_clamp( wnd->x, 0, max_x - wnd->w ); wnd->y = m_clamp( 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( 1, wnd->w - GUI_CONSOLE_BORDER_WIDTH * 2 ); view->h = max( 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( GUI_CONSOLE_MINIMUM_HEIGHT, max_y ); F32 min_w = (F32)min( 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( 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( 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( 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( 1, (I32)( right - left ) ); wnd->h = max( 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( wnd->output_selection_anchor, wnd->output_selection_cursor ); U64 selection_end = max( 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( first, min( wnd->output_selection_anchor, last ) ); wnd->output_selection_cursor = max( first, min( 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( wnd->output_selection_anchor, wnd->output_selection_cursor ); U64 end = max( 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( 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( 0, row_count - wnd->visible_rows ); wnd->scroll_offset = m_clamp( 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 console_cvar_list( const char* query, U32 query_length ) { LIST 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_contextmenu_hide( 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_contextmenu_hide( 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( wnd->output_selection_anchor, wnd->output_selection_cursor ); U64 selection_end = max( 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( mouse_x, 0, max( 0, canvas_w - wnd->menus.output.menu->w ) ); I32 screen_y = m_clamp( mouse_y, 0, max( 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( 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( 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( 1, game->gl->canvas_size[0] ); I32 canvas_h = max( 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( 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( max( 1, wnd->input->w ), max( 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( 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( 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( 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( 1, wnd->visible_rows ); return max( 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( 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( 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; } }