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