summaryrefslogtreecommitdiff
path: root/src/gui/console.cpp
diff options
context:
space:
mode:
author2lag <96544487+2lag@users.noreply.github.com>2026-08-08 17:33:36 +0200
committer2lag <96544487+2lag@users.noreply.github.com>2026-08-08 17:33:36 +0200
commit2973df584978d9aae6f164ce0204179c8a28b3ae (patch)
treec1b793c8e0d17aeb66c5197bc28acca84b2c98ab /src/gui/console.cpp
parente540910745aad31378452eec25aacfb3f346191a (diff)
console, textbox changes, some other shit
Diffstat (limited to 'src/gui/console.cpp')
-rw-r--r--src/gui/console.cpp1779
1 files changed, 1779 insertions, 0 deletions
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;
+ }
+}