From 2973df584978d9aae6f164ce0204179c8a28b3ae Mon Sep 17 00:00:00 2001 From: 2lag <96544487+2lag@users.noreply.github.com> Date: Sat, 8 Aug 2026 17:33:36 +0200 Subject: console, textbox changes, some other shit --- src/editor/editor.cpp | 6 +- src/editor/editor.h | 36 +- src/editor/editor_contextmenu.cpp | 254 ------ src/editor/editor_menubar.cpp | 58 +- src/editor/editor_window.cpp | 12 +- src/game.cpp | 39 +- src/game.h | 3 + src/game/assets.cpp | 12 + src/game/assets.h | 3 + src/game/physics/movement.cpp | 16 +- src/game/player.cpp | 2 +- src/game/vars.cpp | 218 +++-- src/game/vars.h | 47 +- src/gui/base.cpp | 70 +- src/gui/base.h | 86 +- src/gui/console.cpp | 1779 +++++++++++++++++++++++++++++++++++++ src/gui/console.h | 67 ++ src/gui/contextmenu.cpp | 437 +++++++++ src/gui/textbox.cpp | 1011 ++++++++++++++++++--- src/main.cpp | 2 + src/render/gl.cpp | 41 +- src/util/aabb.h | 12 +- src/util/allocator.h | 14 +- src/util/anim.h | 4 +- src/util/config/config.cpp | 2 +- src/util/console.cpp | 12 + src/util/console.h | 883 ++++++++++++++++++ src/util/input.cpp | 20 + src/util/input.h | 7 +- src/util/profiler.cpp | 2 +- src/util/screen.h | 4 +- src/util/thread.h | 10 +- src/util/vector.h | 16 +- 33 files changed, 4586 insertions(+), 599 deletions(-) delete mode 100644 src/editor/editor_contextmenu.cpp create mode 100644 src/gui/console.cpp create mode 100644 src/gui/contextmenu.cpp create mode 100644 src/util/console.cpp create mode 100644 src/util/console.h (limited to 'src') diff --git a/src/editor/editor.cpp b/src/editor/editor.cpp index 2e76039..18ae062 100644 --- a/src/editor/editor.cpp +++ b/src/editor/editor.cpp @@ -648,6 +648,7 @@ GAME_EDITOR* editor_create( GAME_DATA* game ) { gui_init( game ); e->wnd = gui_editorwindow( 800, 600 ); + game->console_window = gui_consolewindow( 10, 10, 256, 128 ); gui_end(); return e; @@ -663,6 +664,7 @@ STAT editor_close( GAME_EDITOR* e ) { I32 w = e->wnd->w, h = e->wnd->h; gui_free( e->wnd ); + e->wnd = 0; editor_clear_gui_state_refs( e ); e->wnd = gui_editorwindow( w, h ); } ); @@ -701,8 +703,8 @@ STAT editor_new_map( GAME_EDITOR* e, const char* mapname ) { m->props.push( { .clr = { 1.f, 1.f, 1.f, 1.f } } ); m->walls.push( { .propid = 0, - .start = { 0 , 0, -40.f }, - .end = { 10.f, 0, 80.f }, + .start = { 0.f, 0.f, -40.f }, + .end = { 10.f, 0.f, 80.f }, } ); CFG_SECTION* map_cfg = map_serialize( m ); diff --git a/src/editor/editor.h b/src/editor/editor.h index 3d27951..6d7d747 100644 --- a/src/editor/editor.h +++ b/src/editor/editor.h @@ -70,19 +70,6 @@ enum EditorObjectType_t { EDITOR_OBJECT_ENTITY = 1 }; -typedef void( *EDITOR_CONTEXTMENU_CALLBACK )( void* data ); -struct EDITOR_CONTEXTMENU_ITEM; -typedef U8( *EDITOR_CONTEXTMENU_ENABLED_CALLBACK )( EDITOR_CONTEXTMENU_ITEM* item ); - -struct EDITOR_CONTEXTMENU_ITEM { - char text[64]{}; - void* data{}; - EDITOR_CONTEXTMENU_CALLBACK cb{}; - EDITOR_CONTEXTMENU_ENABLED_CALLBACK enabled_cb{}; - U8 enabled{1}; - LIST items{}; -}; - struct GAME_EDITOR; typedef void( *EDITOR_GRID_DEP_CALLBACK )( GAME_EDITOR* e ); @@ -184,7 +171,7 @@ struct GAME_EDITOR { I32 view_mode{}; I32 view2d_type{}; - struct GUI_EDITOR_CONTEXTMENU* contextmenu{}; + struct GUI_CONTEXTMENU* contextmenu{}; GUI_BASE* header_toolbar{}; GUI_LABEL* header_viewtype_label{}; GUI_BUTTON* header_back{}; @@ -271,17 +258,6 @@ extern void editor_undo_record_change_prop( GAME_EDITOR* e, FNV1A hash, void* ol extern U8 editor_undo( GAME_EDITOR* e ); extern U8 editor_redo( GAME_EDITOR* e ); -extern void editor_show_contextmenu( I32 x, I32 y, LIST* items, const char* title = 0 ); -extern void editor_hide_contextmenu(); -extern U8 editor_contextmenu_open(); -extern void editor_contextmenu_set_item( - EDITOR_CONTEXTMENU_ITEM* item, - const char* text, - EDITOR_CONTEXTMENU_CALLBACK cb = 0, - void* data = 0, - EDITOR_CONTEXTMENU_ENABLED_CALLBACK enabled_cb = 0 -); - struct GUI_EDITORWINDOW : GUI_WINDOW {}; struct GUI_EDITOR_3DVIEW : GUI_VIEW { U8 heldoutbounds; @@ -374,19 +350,11 @@ struct GUI_EDITOR_VIEWTYPE_SEGMENT : GUI_BASE { I32 held_seg{-1}; }; -struct GUI_EDITOR_CONTEXTMENU : GUI_BASE { - LIST* items{}; - U8 initheld{1}; - U8 held{}; - I32 held_idx{-1}; - char title[64]{}; -}; - struct GUI_EDITOR_TOOLBAR : GUI_BASE { U8 held{}; I32 held_root{-1}; I32 active_root{-1}; - LIST entries{}; + LIST entries{}; }; extern GUI_EDITORWINDOW* gui_editorwindow( I32 w, I32 h ); diff --git a/src/editor/editor_contextmenu.cpp b/src/editor/editor_contextmenu.cpp deleted file mode 100644 index 4f9dbe6..0000000 --- a/src/editor/editor_contextmenu.cpp +++ /dev/null @@ -1,254 +0,0 @@ -#include "editor_gui_internal.h" - -U8 editor_contextmenu_item_enabled( EDITOR_CONTEXTMENU_ITEM* item ) { - if( !item ) - return 0; - - if( item->enabled_cb ) - return item->enabled_cb( item ); - - return item->enabled; -} - -I32 editor_contextmenu_title_h( GUI_EDITOR_CONTEXTMENU* menu ) { - return ( menu && menu->title[0] ) ? 18 : 0; -} - -I32 editor_contextmenu_row_h() { - return 20; -} - -I32 editor_contextmenu_width( LIST* items, const char* title ) { - I32 width = EDITOR_TOOLBAR_DROPDOWN_W; - 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 editor_contextmenu_height( LIST* items, const char* title ) { - I32 height = 4 + editor_contextmenu_row_h() * ( items ? (I32)items->size : 0 ); - if( title && title[0] ) - height += 18 + 1; - return max( height, editor_contextmenu_row_h() + 4 ); -} - -static I32 editor_contextmenu_items_y( GUI_EDITOR_CONTEXTMENU* menu ) { - I32 y = gui_rely( menu ) + 2; - if( menu->title[0] ) - y += editor_contextmenu_title_h( menu ) + 1; - return y; -} - -I32 editor_contextmenu_hit_test( GUI_EDITOR_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 = editor_contextmenu_items_y( menu ); - if( my < items_y ) - return -1; - - I32 idx = ( my - items_y ) / editor_contextmenu_row_h(); - if( idx < 0 || idx >= (I32)menu->items->size ) - return -1; - - return idx; -} - -void gui_editor_contextmenu_draw_fn( void* ptr ) { - GUI_EDITOR_CONTEXTMENU* menu = (GUI_EDITOR_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; - if( editor_menu_hover_mask_active ) { - mx = editor_menu_hover_real_x; - my = editor_menu_hover_real_y; - } else { - gui_cursor_pos( &mx, &my ); - } - I32 hit = editor_contextmenu_hit_test( menu, mx, my ); - U8 m1 = gui_mbutton_down( GUI_MBTNLEFT ); - CLR hover_fill = CLR::blend( ui_clr.border, ui_clr.bg, 0.70f ); - CLR active_fill = CLR::blend( ui_clr.bg_sec, ui_clr.border, 0.22f ); - - CLR border = gui_is_fg_window( menu ) ? ui_clr.border : ui_clr.border_inactive; - 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 cy = y + 2; - if( menu->title[0] ) { - gui_draw_str( x + 8, cy + 1, ALIGN_L, FNT_JPN12, ui_clr.txt, "%s", menu->title ); - cy += editor_contextmenu_title_h( menu ); - gui_draw_line( x + 1, cy, x + menu->w - 2, cy, border ); - cy += 1; - } - - for( I32 i = 0; i < (I32)menu->items->size; ++i ) { - EDITOR_CONTEXTMENU_ITEM* item = &( *menu->items )[i]; - U8 enabled = editor_contextmenu_item_enabled( item ); - U8 hovered = hit == i; - U8 active = menu->held && menu->held_idx == i && m1 && enabled; - - CLR fill = ui_clr.bg_sec; - if( active ) - fill = active_fill; - else if( hovered ) - fill = hover_fill; - - gui_draw_frect( x + 1, cy + i * editor_contextmenu_row_h(), max( 1, menu->w - 2 ), editor_contextmenu_row_h(), fill ); - gui_draw_str( - x + 8, - cy + i * editor_contextmenu_row_h() + 2, - ALIGN_L, - FNT_JPN12, - enabled ? ui_clr.txt : ui_clr.txt_inactive, - "%s", - item->text - ); - } -} - -void gui_editor_contextmenu_input_fn( void* ptr ) { - GUI_EDITOR_CONTEXTMENU* menu = (GUI_EDITOR_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 = editor_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 ) { - editor_hide_contextmenu(); - return; - } - - if( hit == menu->held_idx ) { - EDITOR_CONTEXTMENU_ITEM* item = &( *menu->items )[hit]; - if( editor_contextmenu_item_enabled( item ) && item->cb ) { - item->cb( item->data ); - editor_hide_contextmenu(); - return; - } - } - - menu->held = 0; - menu->held_idx = -1; -} - -void editor_hide_contextmenu() { - if( !editor || !editor->gui.contextmenu ) - return; - - GUI_EDITOR_CONTEXTMENU* menu = editor->gui.contextmenu; - editor->gui.contextmenu = 0; - - if( menu->parent ) { - I32 idx = menu->parent->children.idx_of( (GUI_BASE*)menu ); - if( idx != -1 ) - menu->parent->children.erase( idx ); - } - - GUI_EDITOR_TOOLBAR* bar = (GUI_EDITOR_TOOLBAR*)editor->gui.header_toolbar; - if( bar ) - bar->active_root = -1; - - gui_free( menu ); -} - -U8 editor_contextmenu_open() { - return editor && editor->gui.contextmenu && editor->gui.contextmenu->enabled; -} - - -void editor_contextmenu_set_item( - EDITOR_CONTEXTMENU_ITEM* item, - const char* text, - EDITOR_CONTEXTMENU_CALLBACK cb, - void* data, - EDITOR_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; -} - -void editor_show_contextmenu( I32 x, I32 y, LIST* items, const char* title ) { - editor_hide_contextmenu(); - if( !editor || !editor->wnd || !items || !items->size ) - return; - - GUI_EDITOR_CONTEXTMENU* menu = new GUI_EDITOR_CONTEXTMENU; - menu->items = items; - menu->draw_fn = gui_editor_contextmenu_draw_fn; - menu->input_fn = gui_editor_contextmenu_input_fn; - menu->enabled = 1; - menu->initheld = 1; - menu->held = 0; - menu->held_idx = -1; - menu->parent = editor->wnd; - snprintf( menu->name, sizeof( menu->name ), "EDITOR_CONTEXTMENU" ); - snprintf( menu->title, sizeof( menu->title ), "%s", title ? title : "" ); - - menu->w = editor_contextmenu_width( items, title ); - menu->h = editor_contextmenu_height( items, title ); - menu->xbound = menu->w; - menu->ybound = menu->h; - - if( x + menu->w > editor->wnd->w - 2 ) - x = max( 1, editor->wnd->w - menu->w - 2 ); - if( y + menu->h > editor->wnd->h - 2 ) - y = max( 1, editor->wnd->h - menu->h - 2 ); - if( x < 1 ) x = 1; - if( y < 1 ) y = 1; - - menu->x = x; - menu->y = y; - editor->wnd->children.push( menu ); - editor->gui.contextmenu = menu; -} diff --git a/src/editor/editor_menubar.cpp b/src/editor/editor_menubar.cpp index cef2650..2b2a155 100644 --- a/src/editor/editor_menubar.cpp +++ b/src/editor/editor_menubar.cpp @@ -1,3 +1,4 @@ +#include "editor.h" #include "editor_gui_internal.h" U8 editor_menu_hover_mask_active = 0; @@ -36,7 +37,15 @@ void editor_apply_view_mode( GAME_EDITOR* e ) { } void editor_toolbar_close_menu() { - editor_hide_contextmenu(); + if( !editor ) + return; + + GUI_EDITOR_TOOLBAR* tb = (GUI_EDITOR_TOOLBAR*)editor->gui.header_toolbar; + + if( tb ) + tb->active_root = -1; + + gui_hide_contextmenu( editor->gui.contextmenu ); } void editor_set_view_mode( I32 mode ) { @@ -89,11 +98,11 @@ void editor_contextmenu_set_tool_cb( void* data ) { editor_settool( (U8)(I64)data ); } -U8 editor_contextmenu_undo_enabled( EDITOR_CONTEXTMENU_ITEM* ) { +U8 editor_contextmenu_undo_enabled( GUI_CONTEXTMENU_ITEM* ) { return editor && editor->undo_actions.size > 0; } -U8 editor_contextmenu_redo_enabled( EDITOR_CONTEXTMENU_ITEM* ) { +U8 editor_contextmenu_redo_enabled( GUI_CONTEXTMENU_ITEM* ) { return editor && editor->redo_actions.size > 0; } @@ -104,31 +113,31 @@ void editor_toolbar_init_entries( GUI_EDITOR_TOOLBAR* bar ) { bar->entries.clear(); bar->entries.resize( 4 ); - editor_contextmenu_set_item( &bar->entries[0], "file" ); + gui_contextmenu_set_item( &bar->entries[0], "file" ); bar->entries[0].items.resize( 1 ); - editor_contextmenu_set_item( &bar->entries[0].items[0], "save", editor_contextmenu_save_cb ); + gui_contextmenu_set_item( &bar->entries[0].items[0], "save", editor_contextmenu_save_cb ); - editor_contextmenu_set_item( &bar->entries[1], "edit" ); + gui_contextmenu_set_item( &bar->entries[1], "edit" ); bar->entries[1].items.resize( 2 ); - editor_contextmenu_set_item( &bar->entries[1].items[0], "undo", editor_contextmenu_undo_cb, 0, editor_contextmenu_undo_enabled ); - editor_contextmenu_set_item( &bar->entries[1].items[1], "redo", editor_contextmenu_redo_cb, 0, editor_contextmenu_redo_enabled ); + gui_contextmenu_set_item( &bar->entries[1].items[0], "undo", editor_contextmenu_undo_cb, 0, editor_contextmenu_undo_enabled ); + gui_contextmenu_set_item( &bar->entries[1].items[1], "redo", editor_contextmenu_redo_cb, 0, editor_contextmenu_redo_enabled ); - editor_contextmenu_set_item( &bar->entries[2], "view" ); + gui_contextmenu_set_item( &bar->entries[2], "view" ); bar->entries[2].items.resize( 3 ); - editor_contextmenu_set_item( &bar->entries[2].items[0], "2d view", editor_contextmenu_set_view_mode_cb, (void*)(I64)EDITOR_VIEWMODE_2D ); - editor_contextmenu_set_item( &bar->entries[2].items[1], "3d view", editor_contextmenu_set_view_mode_cb, (void*)(I64)EDITOR_VIEWMODE_3D ); - editor_contextmenu_set_item( &bar->entries[2].items[2], "simulation", editor_contextmenu_set_view_mode_cb, (void*)(I64)EDITOR_VIEWMODE_SIM ); + gui_contextmenu_set_item( &bar->entries[2].items[0], "2d view", editor_contextmenu_set_view_mode_cb, (void*)(I64)EDITOR_VIEWMODE_2D ); + gui_contextmenu_set_item( &bar->entries[2].items[1], "3d view", editor_contextmenu_set_view_mode_cb, (void*)(I64)EDITOR_VIEWMODE_3D ); + gui_contextmenu_set_item( &bar->entries[2].items[2], "simulation", editor_contextmenu_set_view_mode_cb, (void*)(I64)EDITOR_VIEWMODE_SIM ); - editor_contextmenu_set_item( &bar->entries[3], "tools" ); + gui_contextmenu_set_item( &bar->entries[3], "tools" ); bar->entries[3].items.resize( 5 ); - editor_contextmenu_set_item( &bar->entries[3].items[0], "none", editor_contextmenu_set_tool_cb, (void*)(I64)EDITOR_TOOL_NONE ); - editor_contextmenu_set_item( &bar->entries[3].items[1], "select", editor_contextmenu_set_tool_cb, (void*)(I64)EDITOR_TOOL_SELECT ); - editor_contextmenu_set_item( &bar->entries[3].items[2], "wall", editor_contextmenu_set_tool_cb, (void*)(I64)EDITOR_TOOL_WALL ); - editor_contextmenu_set_item( &bar->entries[3].items[3], "poly", editor_contextmenu_set_tool_cb, (void*)(I64)EDITOR_TOOL_POLY ); - editor_contextmenu_set_item( &bar->entries[3].items[4], "object", editor_contextmenu_set_tool_cb, (void*)(I64)EDITOR_TOOL_OBJECT ); + gui_contextmenu_set_item( &bar->entries[3].items[0], "none", editor_contextmenu_set_tool_cb, (void*)(I64)EDITOR_TOOL_NONE ); + gui_contextmenu_set_item( &bar->entries[3].items[1], "select", editor_contextmenu_set_tool_cb, (void*)(I64)EDITOR_TOOL_SELECT ); + gui_contextmenu_set_item( &bar->entries[3].items[2], "wall", editor_contextmenu_set_tool_cb, (void*)(I64)EDITOR_TOOL_WALL ); + gui_contextmenu_set_item( &bar->entries[3].items[3], "poly", editor_contextmenu_set_tool_cb, (void*)(I64)EDITOR_TOOL_POLY ); + gui_contextmenu_set_item( &bar->entries[3].items[4], "object", editor_contextmenu_set_tool_cb, (void*)(I64)EDITOR_TOOL_OBJECT ); } -I32 editor_toolbar_root_width( const EDITOR_CONTEXTMENU_ITEM* entry ) { +I32 editor_toolbar_root_width( const GUI_CONTEXTMENU_ITEM* entry ) { if( !entry ) return 44; @@ -196,7 +205,7 @@ void gui_editor_toolbar_draw_fn( void* ptr ) { U8 hovered = hit.root == i; U8 active = bar->held && bar->held_root == i && m1; - U8 open = editor_contextmenu_open() && bar->active_root == i; + U8 open = gui_contextmenu_open( editor->gui.contextmenu ) && bar->active_root == i; CLR fill = ui_clr.bg_sec; if( active ) @@ -228,13 +237,14 @@ void gui_editor_toolbar_input_fn( void* ptr ) { } if( bar->held && bar->held_root == hit.root && hit.root >= 0 ) { - if( editor_contextmenu_open() && bar->active_root == hit.root ) { - editor_hide_contextmenu(); + if( gui_contextmenu_open( editor->gui.contextmenu ) && bar->active_root == hit.root ) { + editor_toolbar_close_menu(); } else { I32 rx = 0; editor_toolbar_root_rect( bar, hit.root, &rx, 0, 0, 0 ); - editor_show_contextmenu( rx, gui_rely( bar ) + bar->h, &bar->entries[hit.root].items ); - bar->active_root = hit.root; + LIST* items = &bar->entries[hit.root].items; + gui_show_contextmenu( rx, gui_rely( bar ) + bar->h, items, editor->wnd, 0, items->size, &editor->gui.contextmenu ); + bar->active_root = editor->gui.contextmenu ? hit.root : -1; } } diff --git a/src/editor/editor_window.cpp b/src/editor/editor_window.cpp index f81b1a8..5cb04c8 100644 --- a/src/editor/editor_window.cpp +++ b/src/editor/editor_window.cpp @@ -7,7 +7,7 @@ void gui_editorwindow_draw_fn( void* ptr ) { editor_raise_header_toolbar( editor ); GUI_BASE* contextmenu = ( editor && editor->map ) ? (GUI_BASE*)editor->gui.contextmenu : 0; - U8 menu_open = editor_contextmenu_open(); + U8 menu_open = gui_contextmenu_open( (GUI_CONTEXTMENU*)contextmenu ); F32 saved_mx = input.mouse.pos.x; F32 saved_my = input.mouse.pos.y; @@ -34,11 +34,12 @@ void gui_editorwindow_draw_fn( void* ptr ) { else dlog( "gui_editorwindow_draw_fn(): child %p has no draw_fn", it ); } ); + input.mouse.pos.x = saved_mx; + input.mouse.pos.y = saved_my; + if( contextmenu && contextmenu->enabled && contextmenu->draw_fn ) contextmenu->draw_fn( contextmenu ); - input.mouse.pos.x = saved_mx; - input.mouse.pos.y = saved_my; editor_menu_hover_mask_active = 0; } @@ -48,14 +49,14 @@ static void gui_editorwindow_input_fn( void* ptr ) { GUI_BASE* toolbar = ( editor && editor->map ) ? editor->gui.header_toolbar : 0; GUI_BASE* contextmenu = ( editor && editor->map ) ? (GUI_BASE*)editor->gui.contextmenu : 0; - U8 menu_was_open = editor_contextmenu_open(); + U8 menu_was_open = gui_contextmenu_open( (GUI_CONTEXTMENU*)contextmenu ); if( toolbar && toolbar->enabled && toolbar->input_fn ) toolbar->input_fn( toolbar ); if( menu_was_open && editor && editor->gui.contextmenu && editor->gui.contextmenu->input_fn ) editor->gui.contextmenu->input_fn( editor->gui.contextmenu ); - if( menu_was_open || editor_contextmenu_open() ) + if( menu_was_open || gui_contextmenu_open( editor ? editor->gui.contextmenu : 0 ) ) return; wnd->children.each( fn( GUI_BASE** childptr ) { @@ -88,6 +89,7 @@ GUI_EDITORWINDOW* gui_editorwindow( I32 w, I32 h ) { GUI_EDITORWINDOW* wnd = gui_editorwindow_create( w, h ); GAME_EDITOR* e = editor; LIST* map_list = editor_get_map_list( e ); + gui_list( wnd->w / 2 - 100, EDITOR_LAYOUT_CONTENT_Y, 200, 200, "map list", map_list, &editor->gui.map_select ); gui_button( wnd->w / 2 - 100, EDITOR_LAYOUT_CONTENT_Y + 220, 95, 25, "new map", editor_new_map_cb ); gui_button( wnd->w / 2 + 5, EDITOR_LAYOUT_CONTENT_Y + 220, 95, 25, "load map", editor_load_map_cb ); diff --git a/src/game.cpp b/src/game.cpp index 7b526ae..a9cffec 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -3,6 +3,7 @@ #include "game.h" #include "SDL_keycode.h" +#include "SDL_mouse.h" #include "game/physics/movement.h" #include "game/world/bsp.h" #include "game/world/draw.h" @@ -17,24 +18,30 @@ #include "game/world/map.h" #include "gui.h" +#include "gui/base.h" #include "render/gl_batch.h" #include "util.h" +#include "util/console.h" #include "util/input.h" #include "util/profiler.h" #include "util/time.h" -CVAR* g_timescale = var_new( "g_timescale", 1.f ); +CVAR* g_timescale = var_new( "g_timescale", "game timescale", 1.f ); +I32 game_quit( CVAR* self, LIST cmdline ); +CVAR* g_quit = var_new( "quit", "quit the game", &game_quit ); +U8 quit = 0; static void game_realtime_resize_repaint( void* userdata ); GAME_DATA* game_init( GL_DATA* gl ) { - GAME_DATA* game = (GAME_DATA*)malloc( sizeof( GAME_DATA ) ); - memset( game, 0, sizeof(GAME_DATA) ); + GAME_DATA* game = new GAME_DATA{}; game->gl = gl; + console_capture_stdout_begin( &game->console ); + profiler_init(); - VEC2 screensize = { (F32)gl->canvas_size[0], (F32)gl->canvas_size[1] }; + VEC2 screensize = { gl->canvas_size[0], gl->canvas_size[1] }; game->shaders.gl2d = gl_2d_init( gl, screensize, "2d" ); game->shaders.gl3d = gl_3d_init( gl, screensize, "3d" ); @@ -172,6 +179,8 @@ U8 game_main_loop( GAME_DATA* game ) { _profiled if( !OK( gl_poll_events( gl ) ) ) return 1; + console_pump_stdout( &game->console ); + if( !assets_on_frame( game ) ) return 0; @@ -205,7 +214,7 @@ U8 game_main_loop( GAME_DATA* game ) { _profiled if( !OK( gl_endframe( gl ) ) ) return 1; - return input_is_key_down( SDLK_ESCAPE ); + return quit; } void game_on_tick( GAME_DATA* game ) { _profiled @@ -224,14 +233,30 @@ void game_on_tick( GAME_DATA* game ) { _profiled } void game_destroy( GAME_DATA *game ) { + if( !game ) + return; + + console_capture_stdout_end( &game->console ); + gui_shutdown(); + game->console_window = 0; #if IS_EDITOR - editor_destroy( game->editor ); + if( game->editor ) { + editor_destroy( game->editor ); + game->editor = 0; + } #endif if( game->state.map ) map_free( game, game->state.map ); + assets_destroy( game ); gl_batch_destroy( game->render.batch_2d ); gl_batch_destroy( game->render.batch_3d ); gl_destroy( game->gl ); - free( game ); + game->gl = 0; + delete game; +} + +I32 game_quit( CVAR* self, LIST cmdline ) { + quit = 1; + return 1; } diff --git a/src/game.h b/src/game.h index a615e18..bb90a5d 100644 --- a/src/game.h +++ b/src/game.h @@ -6,6 +6,7 @@ #include "render/gl.h" #include "render/gl_2d.h" #include "render/gl_3d.h" +#include "util/console.h" #ifdef IS_EDITOR #include "editor/editor.h" @@ -44,6 +45,8 @@ struct GAME_DATA { GAME_STATE state; GAME_RENDER render; + CONSOLE_DATA console{}; + GUI_CONSOLEWINDOW* console_window{}; #ifdef IS_EDITOR GAME_EDITOR* editor; #endif diff --git a/src/game/assets.cpp b/src/game/assets.cpp index 62663a9..6ee7aff 100644 --- a/src/game/assets.cpp +++ b/src/game/assets.cpp @@ -9,6 +9,18 @@ void assets_init( GAME_DATA *game ) { game->assets.test = gl_texture_from_file( game->gl, "uvtest.png" ); game->assets.fonts_init = 0; + + for( I32 idx = 0; idx < SDL_NUM_SYSTEM_CURSORS; ++idx ) + game->assets.cursors[idx] = SDL_CreateSystemCursor( (SDL_SystemCursor)idx ); +} + +void assets_destroy( GAME_DATA* game ) { + for( I32 idx = 0; idx < SDL_NUM_SYSTEM_CURSORS; ++idx ) { + if( !game->assets.cursors[idx] ) + continue; + SDL_FreeCursor( game->assets.cursors[idx] ); + game->assets.cursors[idx] = 0; + } } void assets_create_fonts( struct GAME_DATA *game ) { diff --git a/src/game/assets.h b/src/game/assets.h index d0fe270..9841f40 100644 --- a/src/game/assets.h +++ b/src/game/assets.h @@ -1,9 +1,11 @@ #pragma once #include "../util/typedef.h" #include "../util/file.h" +#include "SDL_mouse.h" typedef struct { + SDL_Cursor* cursors[SDL_NUM_SYSTEM_CURSORS]; struct GL_FONT* font; struct GL_FONT* jpn12; struct GL_FONT* jpn16; @@ -13,6 +15,7 @@ typedef struct { } GAME_ASSETS; extern void assets_init( struct GAME_DATA* game ); +extern void assets_destroy( struct GAME_DATA* game ); extern void assets_create_fonts( struct GAME_DATA* game ); // extensions is a null-terminated array of null-terminated strings. extern LIST assets_get_files_by_ext_dir( const char** extensions, const char* dir ); diff --git a/src/game/physics/movement.cpp b/src/game/physics/movement.cpp index 4cb669f..ac4ab02 100644 --- a/src/game/physics/movement.cpp +++ b/src/game/physics/movement.cpp @@ -6,14 +6,14 @@ #include "../../game/objlist.h" GAME_MOVEMENT* gmove; -CVAR* mv_gravity = var_new( "mv_gravity", 800.f ); -CVAR* mv_friction = var_new( "mv_friction", 4.f ); -CVAR* mv_accelerate = var_new( "mv_accelerate", 5.f ); -CVAR* mv_airaccelerate = var_new( "mv_airaccelerate", 12.f ); -CVAR* mv_wallboost = var_new( "mv_wallboost", 0 ); -CVAR* mv_maxspeed = var_new( "mv_maxspeed", 3500.f ); -CVAR* mv_stopspeed = var_new( "mv_stopspeed", 80.f ); -CVAR* mv_jump_impulse = var_new( "mv_jump_impulse", 301.993377f ); // csgo - sqrt(2 * 800 * 57 - jump height) +CVAR* mv_gravity = var_new( "mv_gravity", "world gravity", 800.f ); +CVAR* mv_friction = var_new( "mv_friction", "ground friction", 4.f ); +CVAR* mv_accelerate = var_new( "mv_accelerate", "ground acceleration", 5.f ); +CVAR* mv_airaccelerate = var_new( "mv_airaccelerate", "air acceleration", 12.f ); +CVAR* mv_wallboost = var_new( "mv_wallboost", "preserve speed when clipping vel against wall", 0 ); +CVAR* mv_maxspeed = var_new( "mv_maxspeed", "max vel per axis", 3500.f ); +CVAR* mv_stopspeed = var_new( "mv_stopspeed", "min speed to calc ground friction", 80.f ); +CVAR* mv_jump_impulse = var_new( "mv_jump_impulse", "jump force", 301.993377f ); // csgo - sqrt(2 * 800 * 57 - jump height) const U32 NON_JUMP_VELOCITY = 140.f; diff --git a/src/game/player.cpp b/src/game/player.cpp index e4c0390..a1dc3b8 100644 --- a/src/game/player.cpp +++ b/src/game/player.cpp @@ -34,7 +34,7 @@ void capture_mouse( PLAYER* p ) { F32* yaw = &p->input.cam.pos.y; F32* pitch = &p->input.cam.pos.x; - if( input.keys[SDL_SCANCODE_F1] ) { + if( input.mouselock && input.keys[SDLK_ESCAPE] ) { input_capture_mouse( 0 ); } diff --git a/src/game/vars.cpp b/src/game/vars.cpp index 443699d..bc12887 100644 --- a/src/game/vars.cpp +++ b/src/game/vars.cpp @@ -1,5 +1,6 @@ #include "vars.h" #include "../util.h" + #include VAR_LIST* varl; @@ -14,12 +15,35 @@ VAR_LIST* vars_init() { return ret; } -CVAR* var_new( const char* name, F32 v ) { +inline F32 var_clamp( CVAR* var, F32 value ) { + if( !( var->flags & CVAR_FLAG_MINMAX ) ) + return value; + + return m_clamp( + value, + var->min_v.fl_v, + var->max_v.fl_v + ); +} + +inline I32 var_clamp( CVAR* var, I32 value ) { + if( !( var->flags & CVAR_FLAG_MINMAX ) ) + return value; + + return m_clamp( + value, + var->min_v.i_v, + var->max_v.i_v + ); +} + +CVAR* var_new( const char* name, STR desc, F32 v ) { vars_check_global_list(); - CVAR* ret = new CVAR; - memset( ret, 0 , sizeof( CVAR ) ); + CVAR* ret = new CVAR{}; ret->type = CVAR_TYPE_FLOAT; + ret->default_v.fl_v = v; + ret->desc = desc; ret->name = name; ret->fl_v = v; @@ -27,25 +51,85 @@ CVAR* var_new( const char* name, F32 v ) { return ret; } -CVAR* var_new( const char* name, I32 i ) { +CVAR* var_new( + const char* name, + STR desc, + F32 v, + F32 min_v, + F32 max_v +) { + if( min_v > max_v ) { + dlog( "var_new() : invalid bounds for %s\n", name ); + return 0; + } + vars_check_global_list(); - CVAR* ret = new CVAR; - memset( ret, 0 , sizeof( CVAR ) ); + CVAR* ret = new CVAR{}; + ret->flags = CVAR_FLAG_MINMAX; + ret->type = CVAR_TYPE_FLOAT; + ret->min_v.fl_v = min_v; + ret->max_v.fl_v = max_v; + ret->desc = desc; + ret->name = name; + + ret->fl_v = var_clamp( ret, v ); + ret->default_v.fl_v = ret->fl_v; + + varl->vars.push( ret ); + return ret; +} + +CVAR* var_new( const char* name, STR desc, I32 v ) { + vars_check_global_list(); + + CVAR* ret = new CVAR{}; ret->type = CVAR_TYPE_INT; + ret->default_v.i_v = v; + ret->desc = desc; ret->name = name; - ret->i_v = i; + ret->i_v = v; varl->vars.push( ret ); return ret; } -CVAR* var_new( const char* name, CLR c ) { +CVAR* var_new( + const char* name, + STR desc, + I32 v, + I32 min_v, + I32 max_v +) { + if( min_v > max_v ) { + dlog( "var_new() : invalid bounds for %s\n", name ); + return 0; + } + vars_check_global_list(); - CVAR* ret = new CVAR; - memset( ret, 0 , sizeof( CVAR ) ); + CVAR* ret = new CVAR{}; + ret->flags = CVAR_FLAG_MINMAX; + ret->type = CVAR_TYPE_INT; + ret->min_v.i_v = min_v; + ret->max_v.i_v = max_v; + ret->desc = desc; + ret->name = name; + + ret->i_v = var_clamp( ret, v ); + ret->default_v.i_v = ret->i_v; + + varl->vars.push( ret ); + return ret; +} + +CVAR* var_new( const char* name, STR desc, CLR c ) { + vars_check_global_list(); + + CVAR* ret = new CVAR{}; ret->type = CVAR_TYPE_COLOR; + ret->default_v.clr_v = c; + ret->desc = desc; ret->name = name; ret->clr_v = c; @@ -53,25 +137,34 @@ CVAR* var_new( const char* name, CLR c ) { return ret; } -CVAR* var_new( const char* name, CVAR_FUNC func ) { +CVAR* var_new( const char* name, STR desc, CVAR_FUNC func ) { vars_check_global_list(); - CVAR* ret = new CVAR; - memset( ret, 0 , sizeof( CVAR ) ); + CVAR* ret = new CVAR{}; ret->type = CVAR_TYPE_FN; ret->name = name; ret->fn_v = func; + ret->desc = desc; varl->vars.push( ret ); return ret; } -CVAR* var_new( const char* name, const char* v ) { +CVAR* var_new( const char* name, STR desc, const char* v ) { vars_check_global_list(); - CVAR* ret = new CVAR; - memset( ret, 0, sizeof( CVAR ) ); + CVAR* ret = new CVAR{}; + + if( !v || strlen( v ) >= sizeof( ret->str_v ) ) { + dlog( "var_new() : invalid initial string for %s\n", name ); + delete ret; + return 0; + } + + strcpy( ret->default_v.str_v, v ); ret->type = CVAR_TYPE_STRING; + strcpy( ret->str_v, v ); + ret->desc = desc; ret->name = name; varl->vars.push( ret ); @@ -80,22 +173,22 @@ CVAR* var_new( const char* name, const char* v ) { CVAR* var_find( const char* name ) { if( !varl ) { - dlog( "var_new() : error: varl null\n" ); + dlog( "var_find() : error: varl null\n" ); return 0; } - U32 len = strlen( name ); + U32 len = (U32)strlen( name ); for( U32 i = 0; i < varl->vars.size; ++i ) { - U32 len2 = strlen( varl->vars[i]->name ); - if( strncmp( varl->vars[i]->name, name, min( len, len2 ) ) == 0 ) { - return varl->vars[i]; - } + CVAR* cvar = varl->vars[i]; + U32 len2 = (U32)strlen( cvar->name ); + if( strncmp( cvar->name, name, min( len, len2 ) ) ) + continue; + return cvar; } - return 0; } -I32 var_call( CVAR *cmdvar, const char *cmdline ) { +I32 var_call( CVAR *cmdvar, LIST cmdline ) { if( cmdvar->type != CVAR_TYPE_FN ) { dlog( "var_call() : error: not a function\n" ); return -1; @@ -128,29 +221,34 @@ CLR var_getclr( CVAR *var ) { } } -const char* var_gets( CVAR *var ) { +STR var_gets( const CVAR *var ) { + if( !var ) + return {}; + switch( var->type ) { - case CVAR_TYPE_STRING: return var->str_v; - case CVAR_TYPE_FLOAT: - sprintf( var->str_v + sizeof(F32), "%f", var->fl_v ); - return var->str_v + sizeof(F32); - case CVAR_TYPE_INT: - sprintf( var->str_v + sizeof(I32), "%d", var->i_v ); - return var->str_v + sizeof(I32); + case CVAR_TYPE_STRING: return STR( "%s", var->str_v ); case CVAR_TYPE_COLOR: - sprintf( var->str_v + sizeof(CLR), "%f %f %f %f", var->clr_v.r, var->clr_v.g, var->clr_v.b, var->clr_v.a ); - return var->str_v + sizeof(CLR); - default: - return ""; + return STR( + "%#.9g %#.9g %#.9g %#.9g", + (F64)var->clr_v.r, + (F64)var->clr_v.g, + (F64)var->clr_v.b, + (F64)var->clr_v.a + ); + case CVAR_TYPE_FLOAT: return STR( "%#.9g", var->fl_v ); + case CVAR_TYPE_INT: return STR( "%d", var->i_v ); + case CVAR_TYPE_FN: + return STR( "", var->name.data ); + default: return {}; } } void var_set( CVAR* var, F32 v ) { switch( var->type ) { case CVAR_TYPE_FLOAT: - var->fl_v = v; break; + var->fl_v = var_clamp( var, v ); break; case CVAR_TYPE_INT: - var->i_v = (I32)v; break; + var->i_v = var_clamp( var, (I32)v ); break; case CVAR_TYPE_COLOR: var->clr_v = (CLR){ v, v, v, 1.f }; break; case CVAR_TYPE_STRING: @@ -162,9 +260,9 @@ void var_set( CVAR* var, F32 v ) { void var_set( CVAR* var, I32 v ) { switch( var->type ) { case CVAR_TYPE_FLOAT: - var->fl_v = (F32)v; break; + var->fl_v = var_clamp( var, (F32)v ); break; case CVAR_TYPE_INT: - var->i_v = v; break; + var->i_v = var_clamp( var, v ); break; case CVAR_TYPE_COLOR: var->clr_v = (CLR){ ((F32)v / 255), ((F32)v / 255), ((F32)v / 255), 1.f }; break; case CVAR_TYPE_STRING: @@ -179,35 +277,29 @@ void var_set( CVAR* var, CLR v ) { var->clr_v = v; break; case CVAR_TYPE_STRING: strcpy( var->str_v, to_str( v ) ); break; - default: dlog( "attempted to set color to %s, but it's not a color value", var->name.data ); break; + default: dlog( "attempted to set color to %s, but it's not a color value\n", var->name.data ); break; } } void var_set( CVAR* var, STR v ) { - if( v.size >= 64 ) { - dlog( "attempted to set string to %s, but it's too long (%d/64 bytes)", var->name.data, v.size ); + if( var->type != CVAR_TYPE_STRING ) { + dlog( + "attempted to set string to %s, " + "but it is not a string value\n", + var->name.data + ); return; } - switch( var->type ) { - case CVAR_TYPE_FLOAT: - F32 f; sscanf( v.data, "%f", &f ); - var->fl_v = f; break; - case CVAR_TYPE_INT: - var->i_v = atoi( v.data ); break; - case CVAR_TYPE_STRING: - strcpy( var->str_v, v.data ); break; - case CVAR_TYPE_COLOR: { - LIST strings = v.split( " " ); - if( strings.size < 3 || strings.size > 4 ) - break; - - for( U32 i = 0; i < strings.size; ++i ) { - F32 f{}; sscanf( strings[i].data, "%f", &f ); - ( (F32*)( &var->clr_v ) )[i] = f; - } break; - } - default: - break; + if( v.size >= sizeof( var->str_v ) ) { + dlog( + "attempted to set string to %s, " + "but it's too long (%u/64 bytes)\n", + var->name.data, + v.size + ); + return; } + + memcpy( var->str_v, v.data, v.size + 1 ); } diff --git a/src/game/vars.h b/src/game/vars.h index 253effb..754979c 100644 --- a/src/game/vars.h +++ b/src/game/vars.h @@ -10,11 +10,18 @@ enum CvarTypes_t { CVAR_TYPE_LAST }; -typedef I32(*CVAR_FUNC)( struct CVAR* self, const char* cmdline ); +enum CvarFlags_t { + CVAR_FLAG_NONE = 0, + CVAR_FLAG_MINMAX = 1 << 1, +}; + +typedef I32(*CVAR_FUNC)( struct CVAR* self, LIST cmdline ); struct CVAR { STR name; U8 type; + STR desc; + U32 flags; union { F32 fl_v; I32 i_v; @@ -30,6 +37,16 @@ struct CVAR { CVAR_FUNC fn_v; char str_v[64]; } default_v; + + union { + F32 fl_v; + I32 i_v; + } min_v; + + union { + F32 fl_v; + I32 i_v; + } max_v; }; struct VAR_LIST { @@ -38,18 +55,32 @@ struct VAR_LIST { extern VAR_LIST* vars_init(); -extern CVAR* var_new( const char* name, F32 v ); -extern CVAR* var_new( const char* name, I32 v ); -extern CVAR* var_new( const char* name, CLR v ); -extern CVAR* var_new( const char* name, const char* v ); -extern CVAR* var_new( const char* name, CVAR_FUNC fn ); +extern CVAR* var_new( const char* name, STR desc, F32 v ); +extern CVAR* var_new( + const char* name, + STR desc, + F32 v, + F32 min_v, + F32 max_v +); +extern CVAR* var_new( const char* name, STR desc, I32 v ); +extern CVAR* var_new( + const char* name, + STR desc, + I32 v, + I32 min_v, + I32 max_v +); +extern CVAR* var_new( const char* name, STR desc, CLR v ); +extern CVAR* var_new( const char* name, STR desc, const char* v ); +extern CVAR* var_new( const char* name, STR desc, CVAR_FUNC fn ); extern CVAR* var_find( const char* name ); -extern I32 var_call( CVAR* cmdvar, const char* cmdline ); +extern I32 var_call( CVAR* cmdvar, LIST cmdline ); extern F32 var_getf( CVAR* var ); extern I32 var_geti( CVAR* var ); extern CLR var_getclr( CVAR* var ); -extern const char* var_gets( CVAR* var ); +extern STR var_gets( const CVAR* var ); extern void var_set( CVAR* var, F32 v ); extern void var_set( CVAR* var, I32 v ); 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 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 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* entries ); extern struct GUI_CONTEXTBOX* gui_contextbox( I32 x, I32 y, I32 w, I32 h, LIST* entries ); +extern struct GUI_CONTEXTMENU* gui_show_contextmenu( + I32 x, I32 y, + LIST* 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* 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* 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 + +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_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( + 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; + } +} 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 items{}; + CONSOLE_MENU_INPUT_MODE mode{}; + char query[GUI_TEXTBOX_MAX]{}; + GUI_CONTEXTMENU* menu{}; + LIST cvars{}; + U32 history_size{}; + U32 cvar_count{}; + U8 suppressed{}; + } input; + + struct GUI_CONSOLEWINDOW_OUTPUT_MENU { + LIST items{}; + GUI_CONTEXTMENU* menu{}; + } output; + } menus; + + LIST 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* 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( + menu->first_visible, + 0, item_count + ); + I32 rows = min( + 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( 0, menu->w - 2 ), + max( 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( 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( 0, + (I32)menu->items->size - menu->visible_rows + ); + + menu->first_visible = m_clamp( + 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* 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( max_visible_rows, item_count ) + : item_count; + menu->visible_rows = menu->max_visible_rows; + menu->first_visible = max( 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 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( 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( tb->cursorin, tb->cursorout ); +} - *endposx = x + tw; - *endposy = y + th; +static U32 gui_textbox_selection_end( const GUI_TEXTBOX* tb ) { + return max( 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( tb->cursorin, length ); + tb->cursorout = min( tb->cursorout, length ); + + U32 line_start_in = gui_textbox_line_start( tb->value, tb->cursorin ); + U32 line_start_out = gui_textbox_line_start( tb->value, tb->cursorout ); + + tb->cursorpxin = gui_textbox_range_width( + tb->value, + line_start_in, + tb->cursorin + ); + tb->cursorpxout = gui_textbox_range_width( + tb->value, + line_start_out, + tb->cursorout + ); + + tb->cursorpy = gui_textbox_cursor_row( + tb->value, + tb->cursorout + ) * gui_textbox_line_height(); +} + +static void gui_textbox_set_cursor( + GUI_TEXTBOX* tb, + U32 cursor, + U8 extend_selection = 0, + U8 preserve_vertical_goal = 0 +) { + U32 length = (U32)strlen( tb->value ); + tb->cursorout = min( cursor, length ); + + if( !extend_selection ) + tb->cursorin = tb->cursorout; + + gui_textbox_update_cursor_metrics( tb ); + + if( !preserve_vertical_goal ) + tb->cursor_goal_valid = 0; +} + +static U32 gui_textbox_cursor_from_point( + GUI_TEXTBOX* tb, + I32 local_x, I32 local_y +) { + U32 length = (U32)strlen( tb->value ); + I32 line_height = gui_textbox_line_height(); + I32 line_count = gui_textbox_line_count( tb->value, length ); + I32 row = local_y / line_height; + row = m_clamp( row, 0, line_count - 1 ); + U32 line_start = gui_textbox_line_start_from_row( + tb->value, length, row + ); + U32 line_end = gui_textbox_line_end( + tb->value, length, line_start + ); + return gui_textbox_nearest_cursor_x( + tb->value, + line_start, + line_end, + max( 0, local_x ) + ); +} + +static U8 gui_textbox_move_vertical( + GUI_TEXTBOX* tb, + I32 direction, + U8 extend_selection = 0 +) { + if( !direction ) + return 0; + + U32 length = (U32)strlen( tb->value ); + gui_textbox_update_cursor_metrics( tb ); + + if( !tb->cursor_goal_valid ) { + tb->cursor_goal_px = tb->cursorpxout; + tb->cursor_goal_valid = 1; + } + + U32 current_start = gui_textbox_line_start( tb->value, tb->cursorout ); + U32 target_start = 0; + U32 target_end = 0; + + if( direction < 0 ) { + if( current_start == 0 ) + return 0; + + target_end = current_start - 1; + target_start = gui_textbox_line_start( tb->value, target_end ); + } + else { + U32 current_end = gui_textbox_line_end( + tb->value, + length, + tb->cursorout + ); + if( current_end >= length ) + return 0; + + target_start = current_end + 1; + target_end = gui_textbox_line_end( tb->value, length, target_start ); + } + + U32 target = gui_textbox_nearest_cursor_x( + tb->value, + target_start, + target_end, + tb->cursor_goal_px + ); + gui_textbox_set_cursor( tb, target, extend_selection, 1 ); + return 1; +} + +static U8 gui_textbox_delete_selection( GUI_TEXTBOX* tb ) { + if( !gui_textbox_has_selection( tb ) ) + return 0; + + U32 length = (U32)strlen( tb->value ); + U32 start = min( + gui_textbox_selection_start( tb ), + length + ); + U32 end = min( + gui_textbox_selection_end( tb ), + length + ); + + if( start >= end ) { + gui_textbox_set_cursor( tb, start ); + return 0; } + + if( tb->pre_cb ) + tb->pre_cb( tb ); + + memmove( + tb->value + start, + tb->value + end, + length - end + 1 + ); + + gui_textbox_set_cursor( tb, start ); + return 1; +} + +static U32 gui_textbox_insert_bytes( + GUI_TEXTBOX* tb, + const char* source, + U32 count +) { + if( !tb || !source || !count || !tb->maxlen ) + return 0; + + U32 length = (U32)strlen( tb->value ); + U32 limit = min( tb->maxlen, GUI_TEXTBOX_MAX ); + + U32 start = min( + gui_textbox_selection_start( tb ), + length + ); + U32 end = min( + gui_textbox_selection_end( tb ), + length + ); + + U32 selected = end - start; + U32 remaining = length - selected; + + if( limit <= 1 || remaining >= limit - 1 ) + return 0; + + U32 available = limit - 1 - remaining; + count = min( count, available ); + + if( !count ) + return 0; + + if( tb->pre_cb ) + tb->pre_cb( tb ); + + memmove( + tb->value + start + count, + tb->value + end, + length - end + 1 + ); + + memcpy( + tb->value + start, + source, count + ); + + gui_textbox_set_cursor( tb, start + count ); + return count; +} + +static U8 gui_textbox_backspace( GUI_TEXTBOX* tb ) { + if( gui_textbox_delete_selection( tb ) ) + return 1; + + U32 length = (U32)strlen( tb->value ); + U32 cursor = min( tb->cursorout, length ); + if( !cursor ) + return 0; + + if( tb->pre_cb ) + tb->pre_cb( tb ); + + memmove( + tb->value + cursor - 1, + tb->value + cursor, + length - cursor + 1 + ); + + gui_textbox_set_cursor( tb, cursor - 1 ); + return 1; +} + +static U8 gui_textbox_delete_forward( GUI_TEXTBOX* tb ) { + if( gui_textbox_delete_selection( tb ) ) + return 1; + + U32 length = (U32)strlen( tb->value ); + U32 cursor = min( tb->cursorout, length ); + if( cursor >= length ) + return 0; + + if( tb->pre_cb ) + tb->pre_cb( tb ); + + memmove( + tb->value + cursor, + tb->value + cursor + 1, + length - cursor + ); + + gui_textbox_set_cursor( tb, cursor ); + return 1; } void gui_textbox_draw_blinker( GUI_TEXTBOX* tb, I32 x, I32 y, CLR clr ) { @@ -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( 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( 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( + gui_textbox_selection_start( tb ), length + ); + U32 selection_end = min( + gui_textbox_selection_end( tb ), length + ); + + U32 line_start = gui_textbox_line_start( + tb->value, selection_start + ); + + I32 row = gui_textbox_cursor_row( tb->value, line_start ); + + I32 newline_width = gui_textbox_newline_width(); + + for( ;; ) { + U32 line_end = gui_textbox_line_end( + tb->value, length, line_start + ); + + U32 range_start = max( + selection_start, line_start + ); + U32 range_end = min( + selection_end, line_end + ); + + I32 x0 = gui_textbox_range_width( + tb->value, line_start, range_start + ); + I32 x1 = gui_textbox_range_width( + tb->value, line_start, range_end + ); + I32 width = x1 - x0; + + if( line_end < length && selection_end > line_end ) + width += newline_width; + + if( width > 0 ) { + gui_draw_frect( + draw_x + x0, + draw_y + row * line_height, + width, line_height, color + ); + } - 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( 0, tb->w - 2 ), + max( 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( text_h, + tb->cursorpy + line_height + ); + + if( tb->cursorpxout < tb->scrollpx ) + tb->scrollpx = tb->cursorpxout; + else if( tb->cursorpxout + 1 > tb->scrollpx + content_w ) + tb->scrollpx = tb->cursorpxout + 1 - content_w; + + I32 max_scroll_x = max( 0, + max( text_w, tb->cursorpxout + 1 ) - content_w + ); + + tb->scrollpx = m_clamp( + tb->scrollpx, 0, max_scroll_x + ); + + if( !tb->allow_newl ) { + tb->cursorpy = 0; + tb->scrollpy = 0; + return; } - 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( + tb->scrollpy, + 0, + max( 0, text_h - content_h ) + ); +} + +static void gui_textbox_draw_content( + GUI_TEXTBOX* tb, + I32 x, I32 y, + I32 line_height, + CLR text_color +) { + I32 draw_x = x + 2 - tb->scrollpx; + I32 draw_y = y + 3 - tb->scrollpy; + + gui_draw_push_clip( + x + 1, y + 1, + max( 0, tb->w - 2 ), + max( 0, tb->h - 2 ) + ); + + gui_textbox_draw_selection( tb, + draw_x, draw_y, + line_height, + ui_clr.selection + ); + + gui_draw_str( + draw_x, draw_y, ALIGN_L, FNT_JPN12, + text_color, "%s", tb->value + ); + + gui_textbox_draw_blinker( tb, + draw_x + tb->cursorpxout, + draw_y + tb->cursorpy, + text_color + ); + + gui_draw_pop_clip(); +} + +void gui_textbox_draw_fn( void* ptr ) { + GUI_TEXTBOX* tb = (GUI_TEXTBOX*)ptr; + I32 x = gui_relx( tb ); + I32 title_y = gui_rely( tb ); + + gui_draw_str( + x, title_y, ALIGN_L, + FNT_JPN12, ui_clr.txt, + "%s", tb->name + ); + + I32 y = title_y + TEXTBOX_TITLE_OFFSET; + I32 line_height = gui_textbox_line_height(); + + gui_textbox_draw_frame( tb, x, y ); + gui_textbox_update_cursor_metrics( tb ); + gui_textbox_update_scroll( tb, + max( 1, tb->w - 4 ), + max( 1, tb->h - 4 ), + line_height + ); + + CLR text_color = tb->active + ? ui_clr.txt + : ui_clr.txt_inactive; + + gui_textbox_draw_content( tb, x, y, line_height, text_color ); } void gui_textbox_handle_mouse( GUI_TEXTBOX* tb ) { I32 x = gui_relx( tb ); I32 y = gui_rely( tb ) + TEXTBOX_TITLE_OFFSET; - 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( tb->cursorin, length ); + tb->cursorout = min( tb->cursorout, length ); + U8 extend_selection = gui_textbox_shift_down( tb ); + + for( U32 idx = 0; idx < 0x100; ++idx ) { + U8 prev = tb->prevkeys[idx]; + U8 key = tb->keys[idx]; + + if( !key ) { + if( prev && idx == tb->heldkey ) + tb->heldkey = 0; + continue; + } + + if( prev ) + continue; + + GUI_TEXTBOX_KEY_RESULT result{}; + U8 handled = gui_textbox_apply_edit_key( + tb, + idx, + &result + ); + if( !handled ) { + handled = gui_textbox_apply_move_key( + tb, idx, + extend_selection, + &result + ); } + if( !handled ) + continue; + + F32 now = gui_time(); + gui_textbox_apply_key_result( tb, result, now ); + + tb->heldtime = now; + tb->repeattime = now; + tb->heldkey = (U8)idx; } } void gui_textbox_handle_repeat( GUI_TEXTBOX* tb ) { - if( !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 ); diff --git a/src/main.cpp b/src/main.cpp index 451c014..b4d5a88 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -44,5 +44,7 @@ int main(int argc, char* argv[]) { } game_destroy( game ); + free( canvas ); + canvas = 0; return 0; } diff --git a/src/render/gl.cpp b/src/render/gl.cpp index 7425ff4..c4eee26 100644 --- a/src/render/gl.cpp +++ b/src/render/gl.cpp @@ -1,3 +1,5 @@ +#include "SDL_keyboard.h" +#include "SDL_keycode.h" #include "SDL_video.h" #include #define STB_IMAGE_IMPLEMENTATION @@ -5,6 +7,7 @@ #include "gl.h" #include "gl_2d_font.h" +#include "../gui/base.h" #include "../util.h" I32 SAMPLER_INDICES[255]; @@ -224,6 +227,7 @@ GL_DATA* gl_create( I32* _canvas ) { gl_handle_window_size_change( gl, start_w, start_h ); SDL_SetEventFilter( gl_event_filter, gl ); + SDL_StartTextInput(); gl_inst = gl; return gl; } @@ -240,6 +244,7 @@ void gl_gen_buffers( GL_DATA* gl ) { void gl_destroy( GL_DATA *gl ) { SDL_SetEventFilter( 0, 0 ); + SDL_StopTextInput(); while( gl->programs.size ) { gl_program_destroy( gl, gl->programs.pop() ); } while( gl->fonts.size ) { gl_font_destroy( gl, gl->fonts.pop() ); } @@ -486,22 +491,50 @@ STAT gl_beginframe( GL_DATA* gl ) { } STAT gl_poll_events( GL_DATA* gl ) { + U8 suppress_console = 0; SDL_Event e; + while( SDL_PollEvent( &e ) ) { + if( suppress_console && e.type == SDL_TEXTINPUT ) { + suppress_console = 0; + continue; + } + input_on_event( &e ); switch( e.type ) { case SDL_QUIT: return STAT_BREAK; + case SDL_WINDOWEVENT: { if( e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED || e.window.event == SDL_WINDOWEVENT_RESIZED ) { - gl_handle_window_size_change( gl, e.window.data1, e.window.data2 ); + gl_handle_window_size_change( + gl, + e.window.data1, + e.window.data2 + ); } } break; - case SDL_KEYDOWN: - return e.key.keysym.sym == SDLK_ESCAPE ? STAT_BREAK : STAT_OK; - default: break; + + case SDL_KEYDOWN: { + if( e.key.keysym.sym == SDLK_BACKQUOTE + && !e.key.repeat + ) { + gui_consolewindow_toggle(); + U16 modifiers = e.key.keysym.mod; + suppress_console = !( + modifiers & ( + KMOD_CTRL | + KMOD_ALT | + KMOD_GUI + ) + ); + } + } break; + + default: + break; } } diff --git a/src/util/aabb.h b/src/util/aabb.h index 9dc4b20..6e895fc 100644 --- a/src/util/aabb.h +++ b/src/util/aabb.h @@ -15,21 +15,21 @@ inline F32 aabb_support_radius( const AABB& aabb, const VEC3& dir ) { } inline U8 aabb_intersects( const AABB& aabb, const AABB& other ) { - if (aabb.min.x > other.max.x || aabb.max.x < other.min.x) + if(aabb.min.x > other.max.x || aabb.max.x < other.min.x) return 0; - if (aabb.min.y > other.max.y || aabb.max.y < other.min.y) + if(aabb.min.y > other.max.y || aabb.max.y < other.min.y) return 0; - if (aabb.min.z > other.max.z || aabb.max.z < other.min.z) + if(aabb.min.z > other.max.z || aabb.max.z < other.min.z) return 0; return 1; } inline U8 aabb_contains_point( const AABB& aabb, const VEC3& point ) { - if (point.x < aabb.min.x || point.x > aabb.max.x) + if(point.x < aabb.min.x || point.x > aabb.max.x) return 0; - if (point.y < aabb.min.y || point.y > aabb.max.y) + if(point.y < aabb.min.y || point.y > aabb.max.y) return 0; - if (point.z < aabb.min.z || point.z > aabb.max.z) + if(point.z < aabb.min.z || point.z > aabb.max.z) return 0; return 1; } diff --git a/src/util/allocator.h b/src/util/allocator.h index 63fc94a..5fc5aa6 100644 --- a/src/util/allocator.h +++ b/src/util/allocator.h @@ -312,14 +312,16 @@ struct LIST { if( idx >= size ) return; - if constexpr( !__is_trivially_destructible(T) ) { - data[idx].~T(); - memset( &data[idx], 0, sizeof(T) ); - } - - for( U32 i = idx; i < size - 1; i++ ) + for( U32 i = idx; i + 1 < size; ++i ) data[i] = data[i + 1]; + U32 last = size - 1; + + if constexpr( !__is_trivially_destructible(T) ) + data[last].~T(); + + memset( &data[last], 0, sizeof(T) ); + if( --size < capacity / 4 ) shrink(); } diff --git a/src/util/anim.h b/src/util/anim.h index dbc2c11..4c314d9 100644 --- a/src/util/anim.h +++ b/src/util/anim.h @@ -9,8 +9,8 @@ inline T lerp( T a, T b, F32 t ) { template < typename t > inline t clamp( t a, t min, t max ) { - if ( a < min ) return min; - if ( a > max ) return max; + if( a < min ) return min; + if( a > max ) return max; return a; } diff --git a/src/util/config/config.cpp b/src/util/config/config.cpp index 00a2512..63e042a 100644 --- a/src/util/config/config.cpp +++ b/src/util/config/config.cpp @@ -16,7 +16,7 @@ inline U8 is_whitespace( char c ) { } inline void trim_whitespace( char* buf ) { - if (!buf || !*buf) return; + if(!buf || !*buf) return; U32 start = 0; while( buf[start] && is_whitespace( buf[start] ) ) start++; diff --git a/src/util/console.cpp b/src/util/console.cpp new file mode 100644 index 0000000..eee9030 --- /dev/null +++ b/src/util/console.cpp @@ -0,0 +1,12 @@ +#include "console.h" +#include "../game.h" + +extern GAME_DATA* game; + +I32 console_clear_cb( CVAR *self, LIST cmdline ) { + if( !game ) + return 0; + return console_clear_output( &game->console ); +} + +CVAR* g_clear = var_new( "clear", "clear console output window", &console_clear_cb ); diff --git a/src/util/console.h b/src/util/console.h new file mode 100644 index 0000000..4a62561 --- /dev/null +++ b/src/util/console.h @@ -0,0 +1,883 @@ +#pragma once + +#include "../game/vars.h" +#include "string.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +enum CONSOLE_ROUTE : U8 { + CONSOLE_ROUTE_NONE = 0, + CONSOLE_ROUTE_VIEW = 1 << 0, + CONSOLE_ROUTE_STDOUT = 1 << 1, + CONSOLE_ROUTE_BOTH = CONSOLE_ROUTE_VIEW | CONSOLE_ROUTE_STDOUT +}; + +inline constexpr U64 CONSOLE_BYTES_MAX = 10 * 1024 * 1024; + +struct CONSOLE_DATA_RING { + LIST slots{64}; + + U32 first{}; + U32 count{}; + + U64 max_bytes{CONSOLE_BYTES_MAX}; + U64 revision{}; + U64 total_pushed{}; + U64 bytes{}; + + static U64 line_bytes( const STR& line ) { + return (U64)line.size + 1; + } + + STR& operator[]( U32 logical_idx ) { + U32 physical_idx = ( first + logical_idx ) % slots.capacity; + return slots.data[physical_idx]; + } + + const STR& operator[]( U32 logical_idx ) const { + U32 physical_idx = ( first + logical_idx ) % slots.capacity; + return slots.data[physical_idx]; + } + + void grow() { + U32 new_capacity = slots.capacity + ? slots.capacity * 2 + : 64; + + LIST grown{new_capacity}; + + for( U32 idx = 0; idx < count; ++idx ) + grown.push( (*this)[idx] ); + + slots = grown; + first = 0; + } + + void pop_front() { + if( !count ) + return; + + STR& oldest = slots.data[first]; + bytes -= line_bytes( oldest ); + oldest = STR(); + + first = ( first + 1 ) % slots.capacity; + + --count; + if( !count ) + first = 0; + } + + U8 make_room_for( U64 required_bytes ) { + if( required_bytes > max_bytes ) + return 0; + + U8 removed = 0; + + while( count && bytes > max_bytes - required_bytes ) { + pop_front(); + removed = 1; + } + + if( removed ) + ++revision; + + return bytes <= max_bytes - required_bytes; + } + + void push( const STR& src ) { + if( !max_bytes ) + return; + + U64 src_bytes = line_bytes( src ); + + if( src_bytes > max_bytes ) { + U32 keep = max_bytes > 1 + ? (U32)( max_bytes - 1 ) + : 0; + + keep = keep < src.size + ? keep + : src.size; + + STR clipped( keep, src.data + src.size - keep ); + push( clipped ); + return; + } + + if( !make_room_for( src_bytes ) ) + return; + + if( count == slots.capacity ) + grow(); + + U32 tail = ( first + count ) % slots.capacity; + + if( tail < slots.size ) + slots[tail] = src; + else + slots.push( src ); + + ++count; + ++total_pushed; + bytes += src_bytes; + } + + void clear() { + slots.clear(); + slots.reserve( 64 ); + bytes = 0; + count = 0; + first = 0; + } +}; + +struct CONSOLE_STDOUT_CAPTURE { + I32 saved_stdout{-1}; + I32 read_fd{-1}; + U8 active{}; +}; + +struct CONSOLE_DATA { + CONSOLE_STDOUT_CAPTURE stdout_capture{}; + CONSOLE_DATA_RING lines{}; + STR partial_stdout_line{}; + U8 pending_carriage{}; + LIST history{}; + U64 revision{}; +}; + +struct CONSOLE_COMMAND { + U8 unterminated; + LIST args; +}; + +inline U8 console_append_partial_bytes( + CONSOLE_DATA* state, + const char* text, + U32 length +) { + if( !state || !text ) + return 0; + + if( !length ) + return 1; + + STR& partial = state->partial_stdout_line; + U64 required_bytes = (U64)partial.size + (U64)length + 1; + + if( !state->lines.make_room_for( required_bytes ) ) + return 0; + + for( U32 idx = 0; idx < length; ++idx ) + partial.push( text[idx] ); + + return 1; +} + +inline void console_clear_partial_line( CONSOLE_DATA* state ) { + if( !state ) + return; + + STR& partial = state->partial_stdout_line; + partial.size = 0; + + if( partial.data ) + partial.data[0] = 0; +} + +inline I32 console_clear_output( CONSOLE_DATA* state ) { + if( !state ) + return 0; + state->lines.clear(); + console_clear_partial_line( state ); + state->pending_carriage = 0; + ++state->revision; + return 1; +} +extern I32 console_clear_cb( CVAR* self, LIST cmdline ); + +inline void console_finish_partial_line( CONSOLE_DATA* state ) { + if( !state ) + return; + + state->lines.push( state->partial_stdout_line ); + console_clear_partial_line( state ); +} + +inline STR console_command_token( + const char* text, + U32 start, + U32 end +) { + STR token; + token.reserve( end - start + 1 ); + + for( U32 idx = start; idx < end; ++idx ) { + if( text[idx] == '"' ) + continue; + token.push( text[idx] ); + } + return token; +} + +inline void console_parse_command( + const char* text, + CONSOLE_COMMAND* command +) { + if( !command ) + return; + + command->args.clear(); + command->unterminated = 0; + + if( !text ) + return; + + U32 token_start = 0; + U8 in_string = 0; + U32 cursor = 0; + + while( text[cursor] ) { + char c = text[cursor]; + + if( c == '\"' ) + in_string ^= 1; + + if( c == ' ' && !in_string ) { + if( cursor > token_start ) { + command->args.push( + console_command_token( + text, token_start, cursor + ) + ); + } + + do { + ++cursor; + } while( text[cursor] == ' ' ); + + token_start = cursor; + continue; + } + + ++cursor; + } + + if( cursor > token_start ) { + command->args.push( + console_command_token( + text, token_start, cursor + ) + ); + } + + command->unterminated = in_string; +} + +inline void console_append_text( + CONSOLE_DATA* state, + const char* text, + U32 len +) { + if( !state || !text || !len ) + return; + + ++state->revision; + + for( U32 idx = 0; idx < len; ++idx ) { + char c = text[idx]; + + if( state->pending_carriage ) { + state->pending_carriage = 0; + + if( c == '\n' ) { + console_finish_partial_line( state ); + continue; + } + console_clear_partial_line( state ); + } + + if( c == '\r' ) { + state->pending_carriage = 1; + continue; + } + + if( c == '\n' ) { + console_finish_partial_line( state ); + continue; + } + + if( c == '\t' ) { + console_append_partial_bytes( + state, " ", 2 + ); + continue; + } + + console_append_partial_bytes( state, &c, 1 ); + } +} + +inline void console_write_fd( I32 fd, const char* text, U32 len ) { + if( fd < 0 || !text ) + return; + U32 written = 0; + while( written < len ) { + ssize_t result = write( fd, text + written, len - written ); + if( result > 0 ) { + written += (U32)result; + continue; + } + if( result < 0 && errno == EINTR ) + continue; + break; + } +} + +inline void console_write_original_stdout( + CONSOLE_DATA* state, + const char* text, + U32 len +) { + I32 fd = state && state->stdout_capture.active + ? state->stdout_capture.saved_stdout + : STDOUT_FILENO; + console_write_fd( fd, text, len ); +} + +inline void console_write( + CONSOLE_DATA* state, + U8 routes, + const char* text, + U32 len +) { + if( !text || !len ) + return; + if( routes & CONSOLE_ROUTE_VIEW ) + console_append_text( state, text, len ); + if( routes & CONSOLE_ROUTE_STDOUT ) + console_write_original_stdout( state, text, len ); +} + +inline void console_print( + CONSOLE_DATA* state, + U8 routes, + const char* fmt, + ... +) { + if( !state || !fmt ) + return; + + va_list args; + va_list count_args; + va_start( args, fmt ); + va_copy( count_args, args ); + I32 len = vsnprintf( 0, 0, fmt, count_args ); + va_end( count_args ); + + if( len < 0 ) { + va_end( args ); + return; + } + + STR text; + text.reserve( (U32)len + 1 ); + vsnprintf( + text.data, (U32)len + 1, + fmt, args + ); + va_end( args ); + + text.size = (U32)len; + text.data[text.size] = 0; + console_write( + state, + routes, + text.data, + text.size + ); +} + +inline U8 console_parse_i32( + const STR& text, + I32* output +) { + if( !output || !text.data || !text.size ) + return 0; + + char* end = 0; + errno = 0; + long parsed = strtol( text.data, &end, 10 ); + + if( errno == ERANGE + || end == text.data + || *end + || parsed < INT_MIN + || parsed > INT_MAX + ) return 0; + + *output = (I32)parsed; + return 1; +} + +inline U8 console_parse_f32( + const STR& text, + F32* output +) { + if( !output || !text.data || !text.size ) + return 0; + + char* end = 0; + errno = 0; + F32 parsed = strtof( text.data, &end ); + + if( errno == ERANGE + || end == text.data + || *end + || !isfinite( parsed ) + ) return 0; + + *output = parsed; + return 1; +} + +inline U8 console_parse_color_component( + const STR& text, + F32* output +) { + I32 integer = 0; + if( console_parse_i32( text, &integer ) ) { + if( integer < 0 || integer > 255 ) + return 0; + *output = (F32)integer / 255.f; + return 1; + } + + F32 decimal = 0.f; + if( !console_parse_f32( text, &decimal ) + || decimal < 0.f + || decimal > 1.f + ) return 0; + + *output = decimal; + return 1; +} + +inline U8 console_execute_cvar( + CONSOLE_DATA* state, + CVAR* cvar, + LIST& args +) { + if( cvar->type != CVAR_TYPE_FN && args.size == 0 ) + return 0; + + switch( cvar->type ) { + case CVAR_TYPE_FN: { + if( var_call( cvar, args ) ) + return 1; + + console_print( + state, CONSOLE_ROUTE_BOTH, + "%s", "[error] failed to run cvar\n" + ); + return 0; + } + case CVAR_TYPE_INT: { + I32 value = 0; + + if( args.size != 1 + || !console_parse_i32( args[0], &value ) + ) break; + + var_set( cvar, value ); + return 1; + } + case CVAR_TYPE_FLOAT: { + F32 value = 0.f; + + if( args.size != 1 + || !console_parse_f32( args[0], &value ) + ) break; + + var_set( cvar, value ); + return 1; + } break; + case CVAR_TYPE_COLOR: { + if( args.size == 1 ) { + F32 value = 0.f; + + if( !console_parse_color_component( args[0], &value ) ) + break; + + var_set( cvar, value ); + return 1; + } + + if( args.size != 3 && args.size != 4 ) + break; + + F32 components[4] = { + cvar->clr_v.r, + cvar->clr_v.g, + cvar->clr_v.b, + cvar->clr_v.a + }; + + for( U32 idx = 0; idx < args.size; ++idx ) { + if( + !console_parse_color_component( + args[idx], + &components[idx] + ) + ) break; + + if( idx + 1 != args.size ) + continue; + + var_set( cvar, + CLR( + components[0], + components[1], + components[2], + components[3] + ) + ); + return 1; + } + } break; + case CVAR_TYPE_STRING: { + if( args.size != 1 + || args[0].size >= sizeof( cvar->str_v ) + ) break; + + var_set( cvar, args[0] ); + return 1; + } + default: { + console_print( + state, CONSOLE_ROUTE_BOTH, + "%s", "[error] unhandled cvar type\n" + ); + return 0; + } + } + console_print( + state, CONSOLE_ROUTE_BOTH, + "[error] failed to set %s\n", + cvar->name.data + ); + return 0; +} + +inline U8 console_print_i32_help( + CONSOLE_DATA* state, + CVAR* cvar +) { + if( !state || !cvar || cvar->type != CVAR_TYPE_INT ) + return 0; + + if( cvar->flags & CVAR_FLAG_MINMAX ) { + console_print( + state, CONSOLE_ROUTE_BOTH, + "%s - %s. default(%d) current(%d) min(%d) max(%d)\n", + cvar->name.data, + cvar->desc.data, + cvar->default_v.i_v, + cvar->i_v, + cvar->min_v.i_v, + cvar->max_v.i_v + ); + } else { + console_print( + state, CONSOLE_ROUTE_BOTH, + "%s - %s. default(%d) current(%d)\n", + cvar->name.data, + cvar->desc.data, + cvar->default_v.i_v, + cvar->i_v + ); + } + return 1; +} +inline U8 console_print_f32_help( + CONSOLE_DATA* state, + CVAR* cvar +) { + if( !state || !cvar || cvar->type != CVAR_TYPE_FLOAT ) + return 0; + + if( cvar->flags & CVAR_FLAG_MINMAX ) { + console_print( + state, CONSOLE_ROUTE_BOTH, + "%s - %s. default(%f) current(%f) min(%f) max(%f)\n", + cvar->name.data, + cvar->desc.data, + cvar->default_v.fl_v, + cvar->fl_v, + cvar->min_v.fl_v, + cvar->max_v.fl_v + ); + } else { + console_print( + state, CONSOLE_ROUTE_BOTH, + "%s - %s. default(%f) current(%f)\n", + cvar->name.data, + cvar->desc.data, + cvar->default_v.fl_v, + cvar->fl_v + ); + } + return 1; +} + +inline U8 console_print_cvar_help( + CONSOLE_DATA* state, + CVAR* cvar +) { + switch( cvar->type ) { + case CVAR_TYPE_FN: return 1; + case CVAR_TYPE_INT: + return console_print_i32_help( state, cvar ); + case CVAR_TYPE_FLOAT: + return console_print_f32_help( state, cvar ); + case CVAR_TYPE_COLOR: { + console_print( + state, CONSOLE_ROUTE_BOTH, + "%s - %s. default(%f %f %f %f) current(%f %f %f %f)\n", + cvar->name.data, + cvar->desc.data, + cvar->default_v.clr_v.r, + cvar->default_v.clr_v.g, + cvar->default_v.clr_v.b, + cvar->default_v.clr_v.a, + cvar->clr_v.r, + cvar->clr_v.g, + cvar->clr_v.b, + cvar->clr_v.a + ); + } break; + case CVAR_TYPE_STRING: { + console_print( + state, CONSOLE_ROUTE_BOTH, + "%s - %s. default(%s) current(%s)\n", + cvar->name.data, + cvar->desc.data, + cvar->default_v.str_v, + cvar->str_v + ); + } break; + default: { + console_print( + state, CONSOLE_ROUTE_BOTH, + "%s", "[error] unhandled cvar type\n" + ); + return 0; + } + } + return 1; +} + +inline U8 console_execute_command( + CONSOLE_DATA* state, + CONSOLE_COMMAND* command +) { + if( !command || !command->args.size ) + return 1; + + if( command->unterminated ) { + console_print( + state, CONSOLE_ROUTE_BOTH, + "%s", "[error] unterminated string\n" + ); + return 0; + } + + STR command_name = command->args.pop_front(); + CVAR* cvar = var_find( command_name.data ); + + if( !cvar ) { + console_print( + state, CONSOLE_ROUTE_BOTH, + "[error] unknown command: %s\n", + command_name.data + ); + return 0; + } + + if( !command->args.size && cvar->type != CVAR_TYPE_FN ) + return console_print_cvar_help( state, cvar ); + return console_execute_cvar( + state, + cvar, + command->args + ); +} + +inline U8 console_execute_segment( + CONSOLE_DATA* state, + const char* text, + U32 length +) { + if( !length ) + return 1; + + STR segment( length, text ); + CONSOLE_COMMAND command{}; + + console_parse_command( + segment.data, + &command + ); + + return console_execute_command( state, &command ); +} + +inline void console_execute( CONSOLE_DATA* state, const char* cmd ) { + if( !state || !cmd ) + return; + + U32 command_start = 0; + U8 in_string = 0; + + for( U32 cursor = 0;; ++cursor ) { + char c = cmd[cursor]; + + if( c == '"' ) + in_string ^= 1; + + U8 at_end = !c; + U8 at_separator = c == ';' && !in_string; + + if( !at_end && !at_separator ) + continue; + + if( + !console_execute_segment( + state, + cmd + command_start, + cursor - command_start + ) + ) return; + + if( at_end ) + return; + + command_start = cursor + 1; + } +} + +inline U8 console_submit( + CONSOLE_DATA* state, + const char* cmd +) { + if( !state || !cmd || !cmd[0] ) + return 0; + + STR submitted( "%s", cmd ); + state->history.push( submitted ); + + console_print( + state, + CONSOLE_ROUTE_BOTH, + "] %s\n", + submitted.data + ); + + console_execute( state, submitted.data ); + return 1; +} + +inline U8 console_capture_stdout_begin( CONSOLE_DATA* state ) { + if( !state ) + return 0; + + CONSOLE_STDOUT_CAPTURE& capture = state->stdout_capture; + + if( capture.active ) + return 1; + + I32 saved_stdout = dup( STDOUT_FILENO ); + if( saved_stdout < 0 ) + return 0; + + I32 pipe_fds[2]; + if( pipe( pipe_fds ) ) { + close( saved_stdout ); + return 0; + } + + I32 flags = fcntl( pipe_fds[0], F_GETFL, 0 ); + if( flags < 0 + || fcntl( pipe_fds[0], F_SETFL, flags | O_NONBLOCK ) < 0 ) { + close( pipe_fds[0] ); + close( pipe_fds[1] ); + close( saved_stdout ); + return 0; + } + + fflush( stdout ); + setvbuf( stdout, 0, _IONBF, 0 ); + if( dup2( pipe_fds[1], STDOUT_FILENO ) < 0 ) { + close( pipe_fds[0] ); + close( pipe_fds[1] ); + close( saved_stdout ); + return 0; + } + close( pipe_fds[1] ); + capture.saved_stdout = saved_stdout; + capture.read_fd = pipe_fds[0]; + capture.active = 1; + return 1; +} + +inline void console_pump_stdout( CONSOLE_DATA* state ) { + if( !state ) + return; + + CONSOLE_STDOUT_CAPTURE& capture = state->stdout_capture; + if( !capture.active || capture.read_fd < 0 ) + return; + char buffer[4096]; + for( ;; ) { + ssize_t count = read( capture.read_fd, buffer, sizeof( buffer ) ); + if( count > 0 ) { + console_write_fd( + capture.saved_stdout, + buffer, (U32)count + ); + console_append_text( state, buffer, (U32)count ); + continue; + } + if( count < 0 && errno == EINTR ) + continue; + if( count < 0 + && ( errno == EAGAIN || errno == EWOULDBLOCK ) + ) break; + break; + } +} + +inline void console_capture_stdout_end( CONSOLE_DATA* state ) { + if( !state ) + return; + + CONSOLE_STDOUT_CAPTURE& capture = state->stdout_capture; + if( !capture.active ) + return; + + fflush( stdout ); + dup2( capture.saved_stdout, STDOUT_FILENO ); + console_pump_stdout( state ); + close( capture.read_fd ); + close( capture.saved_stdout ); + capture.read_fd = capture.saved_stdout = -1; + capture.active = 0; +} diff --git a/src/util/input.cpp b/src/util/input.cpp index fa308e2..46bc5f9 100644 --- a/src/util/input.cpp +++ b/src/util/input.cpp @@ -1,6 +1,8 @@ #include "input.h" #include "SDL_events.h" +#include + INPUT_DATA input; void input_on_event( SDL_Event* e ) { @@ -30,6 +32,22 @@ void input_on_event( SDL_Event* e ) { input.mouse.pos_delta.x += (F32)e->motion.xrel * input.mpitch; input.mouse.pos_delta.y += (F32)e->motion.yrel * input.myaw; } break; + case SDL_TEXTINPUT: { + U32 incoming = strlen( e->text.text ); + U32 available = (U32)sizeof( input.text ) - 1 - input.textlen; + U32 count = incoming < available + ? incoming + : available; + if( !count ) + break; + memcpy( + input.text + input.textlen, + e->text.text, + count + ); + input.textlen += count; + input.text[input.textlen] = 0; + } break; case SDL_KEYDOWN: { input.keys[e->key.keysym.sym & 0xff] = 1; } break; @@ -77,6 +95,8 @@ void input_on_mouse( I32 type, I32 x, I32 y ) { void input_frame_end() { input.mouse.wheel = 0; + input.textlen = 0; + input.text[0] = 0; } U8 input_is_key_down( U32 key ) { diff --git a/src/util/input.h b/src/util/input.h index af5e258..f982844 100644 --- a/src/util/input.h +++ b/src/util/input.h @@ -34,9 +34,11 @@ struct INPUT_KEYBINDS { using ON_INPUT_FN = FN; +constexpr U32 INPUT_TEXT_MAX = 1024; + struct INPUT_DATA { MOUSE_DATA mouse; - U8 keys[0xff]; + U8 keys[0x100]; U8 mouselock; F32 msens = .3f; F32 myaw = 1.f; @@ -44,7 +46,8 @@ struct INPUT_DATA { INPUT_KEYBINDS binds; LIST on_input; - + char text[INPUT_TEXT_MAX]; + U32 textlen; }; extern INPUT_DATA input; diff --git a/src/util/profiler.cpp b/src/util/profiler.cpp index 86c1810..5f9cd25 100644 --- a/src/util/profiler.cpp +++ b/src/util/profiler.cpp @@ -4,7 +4,7 @@ #include "../game/vars.h" #include "string.h" -CVAR* prof_overlay = var_new( "prof_overlay", 0 ); +CVAR* prof_overlay = var_new( "prof_overlay", "show profiler overlay", 0, 0, 1 ); PROFILER_GLOBAL gprof; void __profiler_intern_draw_tree( PROFILER_LIST_ENTRY* e, GL_FONT* font, I32* x, I32* y ) { diff --git a/src/util/screen.h b/src/util/screen.h index 34effb3..6d30231 100644 --- a/src/util/screen.h +++ b/src/util/screen.h @@ -25,7 +25,7 @@ inline VEC2 s_c( VEC2 offset = { 0.f, 0.f } ) { } inline VEC2 s_tc( VEC2 offset = { 0.f, 0.f } ) { - return { offset.x + (F32)canvas[0] / 2, 0 }; + return { offset.x + (F32)canvas[0] / 2, 0.0f }; } inline VEC2 s_bc( VEC2 offset = { 0.f, 0.f } ) { @@ -33,7 +33,7 @@ inline VEC2 s_bc( VEC2 offset = { 0.f, 0.f } ) { } inline VEC2 s_cl( VEC2 offset = { 0.f, 0.f } ) { - return { 0, offset.y + (F32)canvas[1] / 2 }; + return { 0.0f, offset.y + (F32)canvas[1] / 2 }; } inline VEC2 s_cr( VEC2 offset = { 0.f, 0.f } ) { diff --git a/src/util/thread.h b/src/util/thread.h index 72ef5fb..b46ade0 100644 --- a/src/util/thread.h +++ b/src/util/thread.h @@ -544,13 +544,13 @@ get the count, it might have changed by another thread calling consume or produc union thread_mutex_t { void* align; - char data[ 64 ]; + char data[64]; }; union thread_signal_t { void* align; - char data[ 116 ]; + char data[116]; }; union thread_atomic_int_t @@ -567,7 +567,7 @@ union thread_atomic_ptr_t union thread_timer_t { void* data; - char d[ 8 ]; + char d[8]; }; struct thread_queue_t @@ -1418,7 +1418,7 @@ int thread_queue_produce( thread_queue_t* queue, void* value, int timeout_ms ) return 0; } int tail = thread_atomic_int_inc( &queue->tail ); - queue->values[ tail % queue->size ] = value; + queue->values[tail % queue->size] = value; if( thread_atomic_int_inc( &queue->count ) == 0 ) thread_signal_raise( &queue->data_ready ); return 1; @@ -1439,7 +1439,7 @@ void* thread_queue_consume( thread_queue_t* queue, int timeout_ms ) return NULL; } int head = thread_atomic_int_inc( &queue->head ); - void* retval = queue->values[ head % queue->size ]; + void* retval = queue->values[head % queue->size]; if( thread_atomic_int_dec( &queue->count ) == queue->size ) thread_signal_raise( &queue->space_open ); return retval; diff --git a/src/util/vector.h b/src/util/vector.h index 294b627..c88ccee 100644 --- a/src/util/vector.h +++ b/src/util/vector.h @@ -12,8 +12,9 @@ struct VEC2 { F32 y; VEC2() { x = y = 0.0f; } - VEC2( F32 X, F32 Y ) { x = X; y = Y; } VEC2( const VEC2& v ) = default; + template + VEC2( T1 X, T2 Y ) { x = (F32)X; y = (F32)Y; } bool operator==( const VEC2& v ) const { return ( x == v.x && y == v.y ); } F32& operator[]( I32 i ) { return ( (F32*)this )[i]; } @@ -46,9 +47,9 @@ struct VEC3 { F32 z; VEC3() { x = y = z = 0.0f; } - VEC3( F32 X, F32 Y, F32 Z ) { x = X; y = Y; z = Z; } VEC3( const VEC3& v ) = default; - + template + VEC3( T1 X, T2 Y, T3 Z ) { x = (F32)X; y = (F32)Y; z = (F32)Z; } VEC3( const VEC2& v ) { x = v.x; y = v.y; z = 0.f; } bool operator==( const VEC3& v ) const { return ( x == v.x && y == v.y && z == v.z ); } @@ -81,8 +82,9 @@ struct VEC4 { F32 x, y, z, w; VEC4() { x = y = z = w = 0; } - VEC4( F32 X, F32 Y, F32 Z, F32 W ) { x = X; y = Y; z = Z; w = W; } VEC4( const VEC4& v ) = default; + template + VEC4( T1 X, T2 Y, T3 Z, T4 W ) { x = (F32)X; y = (F32)Y; z = (F32)Z; w = (F32)W; } VEC4( const VEC3& v ) { x = v.x; y = v.y; z = v.z; w = 1.f; } VEC4( const VEC2& v ) { x = v.x; y = v.y; z = 0.f; w = 1.f; } @@ -227,19 +229,19 @@ inline void angle_vectors( const VEC3& angles, VEC3* forward, VEC3* right, VEC3* sr = sinf( angles.z * PIRAD ); cr = cosf( angles.z * PIRAD ); - if ( forward ) { + if( forward ) { forward->x = cp * cy; forward->y = cp * sy; forward->z = -sp; } - if ( right ) { + if( right ) { right->x = -1 * sr * sp * cy + -1 * cr * -sy; right->y = -1 * sr * sp * sy + -1 * cr * cy; right->z = -1 * sr * cp; } - if ( up ) { + if( up ) { up->x = cr * sp * cy + -sr * -sy; up->y = cr * sp * sy + -sr * cy; up->z = cr * cp; -- cgit v1.2.3