X-Git-Url: https://harrygodden.com/git/?p=vg.git;a=blobdiff_plain;f=vg_imgui.c;fp=vg_imgui.c;h=25104044dfa25b6604009d95112b5c1089ba7084;hp=0000000000000000000000000000000000000000;hb=3b14f3dcd5bf9dd3c85144f2123d667bfa4bb63f;hpb=fce86711735b15bff37de0f70716808410fcf269 diff --git a/vg_imgui.c b/vg_imgui.c new file mode 100644 index 0000000..2510404 --- /dev/null +++ b/vg_imgui.c @@ -0,0 +1,2206 @@ +/* Copyright (C) 2021-2024 Harry Godden (hgn) - All Rights Reserved */ + +/* + * Principles: + * + * 1. layout is defined by subdividing + * 2. a parent node should never be resized by the content after creation + * 3. when the ui is in an interactive state, no controls should ever move + * 4. controls directly reference a memory location and use that as their + * unique id + * 5. a maximum of ONE control per memory location can be drawn at any given + * point. + */ + +#pragma once + +#include "vg_imgui.h" +#include "vg_engine.h" +#include "vg_tex.h" +#include "vg_shader.h" +#include + +ui_px k_ui_widget_height = 28, + k_ui_scale = 1, + k_ui_padding = 8; + +ui_font vg_ui_font_small = { + .glyph_width = 8, + .glyph_height = 14, + .glyph_baseline = 4, + .line_height = 14, + .sheet_size = 256, + .spacing = 8, + .ascii_start = ' ', + .offset_y = 0 +}, +vg_ui_font_big = { + .glyph_width = 12, + .glyph_height = 21, + .glyph_baseline = 6, + .line_height = 21, + .sheet_size = 256, + .spacing = 10, + .ascii_start = ' ', + .offset_y = 84 +}; + +struct vg_imgui vg_ui = { + .scheme = { + [ k_ui_bg+0 ] = UI_RGB( 0x1d2021 ), + [ k_ui_bg+1 ] = UI_RGB( 0x282828 ), + [ k_ui_bg+2 ] = UI_RGB( 0x3c3836 ), + [ k_ui_bg+3 ] = UI_RGB( 0x504945 ), + [ k_ui_bg+4 ] = UI_RGB( 0x665c54 ), + [ k_ui_bg+5 ] = UI_RGB( 0x7c6f64 ), + [ k_ui_bg+6 ] = UI_RGB( 0x928374 ), + [ k_ui_bg+7 ] = UI_RGB( 0xa89984 ), + + [ k_ui_fg+0 ] = UI_RGB( 0xebdbb2 ), + [ k_ui_fg+1 ] = UI_RGB( 0xfbf1c7 ), + [ k_ui_fg+2 ] = UI_RGB( 0xd5c4a1 ), + [ k_ui_fg+3 ] = UI_RGB( 0xbdae93 ), + [ k_ui_fg+4 ] = UI_RGB( 0xa89984 ), + [ k_ui_fg+5 ] = UI_RGB( 0x000000 ), + [ k_ui_fg+6 ] = UI_RGB( 0x000000 ), + [ k_ui_fg+7 ] = UI_RGB( 0x000000 ), + + [ k_ui_red ] = UI_RGB( 0xcc241d ), + [ k_ui_orange ] = UI_RGB( 0xd65d0e ), + [ k_ui_yellow ] = UI_RGB( 0xd79921 ), + [ k_ui_green ] = UI_RGB( 0x98971a ), + [ k_ui_aqua ] = UI_RGB( 0x689d6a ), + [ k_ui_blue ] = UI_RGB( 0x458588 ), + [ k_ui_purple ] = UI_RGB( 0xb16286 ), + [ k_ui_gray ] = UI_RGB( 0x928374 ), + [ k_ui_red + k_ui_brighter ] = UI_RGB( 0xfb4934 ), + [ k_ui_orange + k_ui_brighter ] = UI_RGB( 0xfe8019 ), + [ k_ui_yellow + k_ui_brighter ] = UI_RGB( 0xfabd2f ), + [ k_ui_green + k_ui_brighter ] = UI_RGB( 0xb8bb26 ), + [ k_ui_aqua + k_ui_brighter ] = UI_RGB( 0x8ec07c ), + [ k_ui_blue + k_ui_brighter ] = UI_RGB( 0x83a598 ), + [ k_ui_purple + k_ui_brighter ] = UI_RGB( 0xd3869b ), + [ k_ui_gray + k_ui_brighter ] = UI_RGB( 0xa89984 ), + }, + .font = &vg_ui_font_small, + .colour = {1.0f,1.0f,1.0f,1.0f}, + .bg_inverse_ratio = {1,1} +}; + +static struct vg_shader _shader_ui ={ + .name = "[vg] ui - transparent", + .link = NULL, + .vs = { + .orig_file = NULL, + .static_src = + "layout (location=0) in vec2 a_co;" + "layout (location=1) in vec2 a_uv;" + "layout (location=2) in vec4 a_colour;" + "uniform mat3 uPv;" + "uniform vec2 uBGInverseRatio;" + "" + "out vec4 aTexCoords;" + "out vec4 aColour;" + "" + "void main(){" + "vec4 proj_pos = vec4( uPv * vec3( a_co, 1.0 ), 1.0 );" + "gl_Position = proj_pos;" + "aTexCoords = vec4( a_uv * 0.00390625, " + " (proj_pos.xy*0.5+0.5) * uBGInverseRatio );" + "aColour = a_colour;" + "}", + }, + .fs = { + .orig_file = NULL, + .static_src = + "uniform sampler2D uTexGlyphs;" + "uniform sampler2D uTexBG;" + "uniform vec4 uColour;" + "uniform float uSpread;" + "out vec4 FragColor;" + "" + "in vec4 aTexCoords;" + "in vec4 aColour;" + "" + "vec2 rand_hash22( vec2 p ){" + "vec3 p3 = fract(vec3(p.xyx) * 213.8976123);" + "p3 += dot(p3, p3.yzx+19.19);" + "return fract(vec2((p3.x + p3.y)*p3.z, (p3.x+p3.z)*p3.y));" + "}" + "" + "void main(){" + "vec4 diffuse = aColour;" + + "vec4 avg = vec4(0.0);" + + "if( aColour.a == 0.0 ){" + "avg = aColour;" + "avg.a = texture( uTexGlyphs, aTexCoords.xy ).r;" + "}" + "else{" + "if( uSpread > 0.0001 ){" + "for( int i=0; i<4; i ++ ){" + "vec2 spread = rand_hash22(aTexCoords.zw+vec2(float(i)));" + "avg += texture( uTexBG, aTexCoords.zw + (spread-0.5)*uSpread );" + "}" + "avg *= 0.25;" + "avg.a = 1.0;" + "avg.rgb = mix( avg.rgb, aColour.rgb, aColour.a );" + "}" + "else{" + "avg = aColour;" + "}" + "}" + + "FragColor = avg * uColour;" + "}" + } +}; + +static struct vg_shader _shader_ui_image = { + .name = "[vg] ui_image", + .link = NULL, + .vs = + { + .orig_file = NULL, + .static_src = + "layout (location=0) in vec2 a_co;" + "layout (location=1) in vec2 a_uv;" + "layout (location=2) in vec4 a_colour;" + "uniform mat3 uPv;" + + "out vec2 aTexCoords;" + "out vec4 aColour;" + "out vec2 aWsp;" + + "void main()" + "{" + "gl_Position = vec4( uPv * vec3( a_co, 1.0 ), 1.0 );" + "aTexCoords = a_uv * 0.00390625;" + "aColour = a_colour;" + + "aWsp = a_co;" + "}", + }, + .fs = + { + .orig_file = NULL, + .static_src = + "uniform sampler2D uTexImage;" + "uniform vec4 uColour;" + "out vec4 FragColor;" + + "in vec2 aTexCoords;" + "in vec4 aColour;" + "in vec2 aWsp;" + + "void main()" + "{" + "vec4 colour = texture( uTexImage, aTexCoords );" + "FragColor = colour * uColour;" + "}" + } +}; + +static struct vg_shader _shader_ui_hsv = { + .name = "[vg] ui_hsv", + .link = NULL, + .vs = { + .orig_file = NULL, + .static_src = + "layout (location=0) in vec2 a_co;" + "layout (location=1) in vec2 a_uv;" + "layout (location=2) in vec4 a_colour;" + "uniform mat3 uPv;" + + "out vec2 aTexCoords;" + "out vec4 aColour;" + "out vec2 aWsp;" + + "void main()" + "{" + "gl_Position = vec4( uPv * vec3( a_co, 1.0 ), 1.0 );" + "aTexCoords = a_uv * 0.00390625;" + "aColour = a_colour;" + + "aWsp = a_co;" + "}", + }, + .fs = + { + .orig_file = NULL, + .static_src = + "uniform float uHue;" + "out vec4 FragColor;" + + "in vec2 aTexCoords;" + "in vec4 aColour;" + "in vec2 aWsp;" + + "void main()" + "{" + "vec3 c = vec3( uHue, aTexCoords );" + "vec4 K = vec4(1.0,2.0/3.0,1.0/3.0,3.0);" + "vec3 p = abs(fract(c.xxx+K.xyz)*6.0 - K.www);" + "vec3 colour = c.z*mix(K.xxx,clamp(p-K.xxx,0.0,1.0),c.y);" + "FragColor = vec4( colour, 1.0 );" + "}" + } +}; + +void vg_ui_init(void) +{ + if( !vg_shader_compile( &_shader_ui ) || + !vg_shader_compile( &_shader_ui_image ) || + !vg_shader_compile( &_shader_ui_hsv ) ){ + vg_fatal_error( "Failed to compile ui shader" ); + } + + /* + * Vertex buffer + * ---------------------------------------- + */ + + vg_ui.max_indices = 20000; + vg_ui.max_verts = 30000; + + /* Generate the buffer we are gonna be drawing to */ + glGenVertexArrays( 1, &vg_ui.vao ); + glGenBuffers( 1, &vg_ui.vbo ); + glGenBuffers( 1, &vg_ui.ebo ); + + glBindVertexArray( vg_ui.vao ); + glBindBuffer( GL_ARRAY_BUFFER, vg_ui.vbo ); + + glBufferData( GL_ARRAY_BUFFER, + vg_ui.max_verts * sizeof( struct ui_vert ), + NULL, GL_DYNAMIC_DRAW ); + glBindVertexArray( vg_ui.vao ); + + glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, vg_ui.ebo ); + glBufferData( GL_ELEMENT_ARRAY_BUFFER, + vg_ui.max_indices * sizeof( u16 ), NULL, GL_DYNAMIC_DRAW ); + + VG_CHECK_GL_ERR(); + + /* Set pointers */ + u32 const stride = sizeof( struct ui_vert ); + + /* XY */ + glVertexAttribPointer( 0, 2, GL_SHORT, GL_FALSE, stride, + (void *)offsetof( struct ui_vert, co ) ); + glEnableVertexAttribArray( 0 ); + + /* UV */ + glVertexAttribPointer( 1, 2, GL_UNSIGNED_SHORT, GL_FALSE, stride, + (void *)offsetof( struct ui_vert, uv ) ); + glEnableVertexAttribArray( 1 ); + + /* COLOUR */ + glVertexAttribPointer( 2, 4, GL_UNSIGNED_BYTE, GL_TRUE, stride, + (void *)offsetof( struct ui_vert, colour ) ); + glEnableVertexAttribArray( 2 ); + + VG_CHECK_GL_ERR(); + + /* Alloc RAM default context */ + u32 vert_size = vg_ui.max_verts*sizeof(struct ui_vert), + inds_size = vg_align8( vg_ui.max_indices*sizeof(u16) ); + + vg_ui.vertex_buffer = vg_linear_alloc( vg_mem.rtmemory, vert_size ); + vg_ui.indice_buffer = vg_linear_alloc( vg_mem.rtmemory, inds_size ); + + /* font + * ----------------------------------------------------- + */ + + /* Load default font */ + u32 compressed[] = { + #include "vg/vg_pxfont_thin.h" + }; + + u32 pixels = 0, total = 256*256, data = 0; + u8 image[256*256]; + + while( pixels < total ){ + for( int b = 31; b >= 0; b-- ){ + image[ pixels ++ ] = (compressed[data] & (0x1u << b))? 0xffu: 0x00u; + + if( pixels >= total ){ + total = 0; + break; + } + } + data++; + } + + glGenTextures( 1, &vg_ui.tex_glyphs ); + glBindTexture( GL_TEXTURE_2D, vg_ui.tex_glyphs ); + glTexImage2D( GL_TEXTURE_2D, 0, GL_R8, 256, 256, 0, + GL_RED, GL_UNSIGNED_BYTE, image ); + + VG_CHECK_GL_ERR(); + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE ); + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE ); + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST ); + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST ); + + /* + * Cursors + * --------------------------------------------------------------- + */ + + vg_ui.cursor_map[ k_ui_cursor_default ] = + SDL_CreateSystemCursor( SDL_SYSTEM_CURSOR_ARROW ); + vg_ui.cursor_map[ k_ui_cursor_hand ] = + SDL_CreateSystemCursor( SDL_SYSTEM_CURSOR_HAND ); + vg_ui.cursor_map[ k_ui_cursor_ibeam ] = + SDL_CreateSystemCursor( SDL_SYSTEM_CURSOR_IBEAM ); +} + +void rect_copy( ui_rect a, ui_rect b ){ + for( int i=0; i<4; i++ ) + b[i] = a[i]; +} + +void ui_flush( enum ui_shader shader, f32 w, f32 h ){ + u32 vertex_offset = vg_ui.vert_start*sizeof(ui_vert), + vertex_count = vg_ui.cur_vert-vg_ui.vert_start, + vertex_size = vertex_count*sizeof(ui_vert), + + indice_offset = vg_ui.indice_start*sizeof(u16), + indice_count = vg_ui.cur_indice-vg_ui.indice_start, + indice_size = indice_count * sizeof(u16); + + if( !vertex_size || !indice_size ) + return; + + glBindVertexArray( vg_ui.vao ); + glBindBuffer( GL_ARRAY_BUFFER, vg_ui.vbo ); + glBufferSubData( GL_ARRAY_BUFFER, vertex_offset, vertex_size, + vg_ui.vertex_buffer+vg_ui.vert_start ); + + glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, vg_ui.ebo ); + glBufferSubData( GL_ELEMENT_ARRAY_BUFFER, indice_offset, indice_size, + vg_ui.indice_buffer+vg_ui.indice_start ); + + glDisable( GL_DEPTH_TEST ); + glEnable( GL_BLEND ); + glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA ); + glBlendEquation( GL_FUNC_ADD ); + glDisable( GL_CULL_FACE ); + + m3x3f view = M3X3_IDENTITY; + m3x3_translate( view, (v3f){ -1.0f, 1.0f, 0.0f } ); + m3x3_scale( view, (v3f){ 1.0f/(w*0.5f), + -1.0f/(h*0.5f), 1.0f } ); + + if( shader == k_ui_shader_colour ){ + glUseProgram( _shader_ui.id ); + + glUniformMatrix3fv( glGetUniformLocation( _shader_ui.id, "uPv" ), 1, + GL_FALSE, (float *)view ); + glUniform4fv( glGetUniformLocation( _shader_ui.id, "uColour" ), 1, + vg_ui.colour ); + + glActiveTexture( GL_TEXTURE0 ); + glBindTexture( GL_TEXTURE_2D, vg_ui.tex_glyphs ); + glUniform1i( glGetUniformLocation( _shader_ui.id, "uTexGlyphs" ), 0 ); + + glActiveTexture( GL_TEXTURE1 ); + glBindTexture( GL_TEXTURE_2D, vg_ui.tex_bg ); + glUniform1i( glGetUniformLocation( _shader_ui.id, "uTexBG" ), 1 ); + glUniform1f( glGetUniformLocation( _shader_ui.id, "uSpread" ), + vg_ui.frosting ); + glUniform2fv( glGetUniformLocation( _shader_ui.id, "uBGInverseRatio" ), + 1, vg_ui.bg_inverse_ratio ); + } + else if( shader == k_ui_shader_image ){ + glUseProgram( _shader_ui_image.id ); + glUniformMatrix3fv( glGetUniformLocation( _shader_ui_image.id, "uPv" ), 1, + GL_FALSE, (float *)view ); + glUniform1i( glGetUniformLocation(_shader_ui_image.id,"uTexImage"), 0 ); + glUniform4fv( glGetUniformLocation( _shader_ui_image.id, "uColour" ), 1, + vg_ui.colour ); + } + else if( shader == k_ui_shader_hsv ){ + glUseProgram( _shader_ui_hsv.id ); + glUniformMatrix3fv( glGetUniformLocation( _shader_ui_hsv.id, "uPv" ), 1, + GL_FALSE, (float *)view ); + glUniform1f( glGetUniformLocation(_shader_ui_hsv.id,"uHue"), vg_ui.hue ); + } + else + vg_fatal_error( "Invalid UI shader (%d)\n", shader ); + + glDrawElements( GL_TRIANGLES, indice_count, GL_UNSIGNED_SHORT, + (void *)(vg_ui.indice_start*sizeof(u16)) ); + + glDisable( GL_BLEND ); + + vg_ui.indice_start = vg_ui.cur_indice; + vg_ui.vert_start = vg_ui.cur_vert; +} + +void ui_fill_rect( ui_rect rect, u32 colour, ui_px uv[4] ) +{ + /* this if far from ideal but stops us from crashing */ + if( (vg_ui.cur_vert + 4 > vg_ui.max_verts) || + (vg_ui.cur_indice + 6 > vg_ui.max_indices)) + return; + + struct ui_vert *vertices = &vg_ui.vertex_buffer[ vg_ui.cur_vert ]; + u16 *indices = &vg_ui.indice_buffer[ vg_ui.cur_indice ]; + + for( int i=0; i<4; i++ ){ + vertices[i].colour = colour; + } + + vertices[0].co[0] = rect[0]; + vertices[0].co[1] = rect[1]; + vertices[0].uv[0] = uv[0]; + vertices[0].uv[1] = uv[1]; + vertices[1].co[0] = rect[0]+rect[2]; + vertices[1].co[1] = rect[1]; + vertices[1].uv[0] = uv[2]; + vertices[1].uv[1] = uv[1]; + vertices[2].co[0] = rect[0]+rect[2]; + vertices[2].co[1] = rect[1]+rect[3]; + vertices[2].uv[0] = uv[2]; + vertices[2].uv[1] = uv[3]; + vertices[3].co[0] = rect[0]; + vertices[3].co[1] = rect[1]+rect[3]; + vertices[3].uv[0] = uv[0]; + vertices[3].uv[1] = uv[3]; + u16 ind_start = vg_ui.cur_vert; + + u16 start = vg_ui.cur_vert; + u32 mesh[] = { 0,2,1, 0,3,2 }; + + for( u32 i=0; i vg_ui.max_verts) || + (vg_ui.cur_indice + 24 > vg_ui.max_indices)) + return; + + struct ui_vert *vertices = &vg_ui.vertex_buffer[ vg_ui.cur_vert ]; + u16 *indices = &vg_ui.indice_buffer[ vg_ui.cur_indice ]; + + for( int i=0; i<8; i++ ){ + vertices[i].uv[0] = 4; + vertices[i].uv[1] = 4; + vertices[i].colour = colour; + } + + vertices[0].co[0] = rect[0]; + vertices[0].co[1] = rect[1]; + vertices[1].co[0] = rect[0]+rect[2]; + vertices[1].co[1] = rect[1]; + vertices[2].co[0] = rect[0]+rect[2]; + vertices[2].co[1] = rect[1]+rect[3]; + vertices[3].co[0] = rect[0]; + vertices[3].co[1] = rect[1]+rect[3]; + vertices[4].co[0] = vertices[0].co[0]-thickness; + vertices[4].co[1] = vertices[0].co[1]-thickness; + vertices[5].co[0] = vertices[1].co[0]+thickness; + vertices[5].co[1] = vertices[1].co[1]-thickness; + vertices[6].co[0] = vertices[2].co[0]+thickness; + vertices[6].co[1] = vertices[2].co[1]+thickness; + vertices[7].co[0] = vertices[3].co[0]-thickness; + vertices[7].co[1] = vertices[3].co[1]+thickness; + + u16 start = vg_ui.cur_vert; + u32 mesh[] = { 0,5,4, 0,1,5, 1,6,5, 1,2,6, 2,7,6, 2,3,7, 3,4,7, 3,0,4 }; + + if( !mask ) + mask = UI_TOP|UI_LEFT|UI_BOTTOM|UI_RIGHT; + + u32 c = 0; + for( u32 i=0; i rp ) dir = k_ui_axis_h; + else dir = k_ui_axis_v; + other = dir ^ 0x1; + + d[2+dir] = rect[2+dir]; + d[2+other] = (rect[2+dir] * size[other]) / size[dir]; + + ui_rect_center( rect, d ); +} + +void ui_split_ratio( ui_rect rect, enum ui_axis dir, float ratio, + ui_px gap, ui_rect l, ui_rect r ) +{ + ui_px width = (float)rect[ 2+(dir^0x1) ] * ratio; + ui_split( rect, dir, width, gap, l, r ); +} + +void ui_rect_pad( ui_rect rect, ui_px pad[2] ) +{ + rect[0] += pad[0]; + rect[1] += pad[1]; + rect[2] -= pad[0]*2; + rect[3] -= pad[1]*2; +} + +ui_px ui_text_line_width( const char *str ) +{ + int length = 0; + const char *_c = str; + u8 c; + + while( (c = *(_c ++)) ){ + if( c >= 32 ) length ++; + else if( c == '\n' ) break; + } + + return length * vg_ui.font->spacing; +} + +ui_px ui_text_string_height( const char *str ) +{ + int height = 1; + const char *_c = str; + u8 c; + + while( (c = *(_c ++)) ){ + if( c == '\n' ) height ++; + } + + return height * 14; +} + +ui_px ui_text_aligned_x( const char *str, ui_rect rect, ui_px scale, + enum ui_align align ) +{ + enum ui_align lwr = k_ui_align_lwr & align; + if( lwr == k_ui_align_left ){ + return rect[0]; + } + else{ + ui_px width = ui_text_line_width( str ) * scale; + + if( lwr == k_ui_align_right ) + return rect[0] + rect[2]-width; + else + return rect[0] + (rect[2]-width)/2; + } +} + +static ui_px ui_min( ui_px a, ui_px b ){ return ab?a:b; } +static ui_px ui_clamp( ui_px a, ui_px min, ui_px max ){ + return ui_min( max, ui_max( a, min ) ); +} + +int ui_clip( ui_rect parent, ui_rect child, ui_rect clipped ) +{ + ui_px parent_max[2], child_max[2]; + parent_max[0] = parent[0]+parent[2]; + parent_max[1] = parent[1]+parent[3]; + child_max[0] = child[0]+child[2]; + child_max[1] = child[1]+child[3]; + + clipped[0] = ui_clamp( child[0], parent[0], parent_max[0] ); + clipped[1] = ui_clamp( child[1], parent[1], parent_max[1] ); + clipped[2] = ui_clamp( child_max[0], parent[0], parent_max[0] ); + clipped[3] = ui_clamp( child_max[1], parent[1], parent_max[1] ); + + if( clipped[0] == clipped[2] || + clipped[1] == clipped[3] ) + return 0; + + clipped[2] -= clipped[0]; + clipped[3] -= clipped[1]; + + return 1; +} + +int ui_inside_rect( ui_rect rect, ui_px co[2] ) +{ + if( co[0] >= rect[0] && + co[1] >= rect[1] && + co[0] < rect[0]+rect[2] && + co[1] < rect[1]+rect[3] ){ + return 1; + } + else + return 0; +} + +int ui_click_down( u32 mask ) +{ + if( vg_ui.ignore_input_frames ) return 0; + if( (vg_ui.mouse_state[0] & mask) && + !(vg_ui.mouse_state[1] & mask) ) + return 1; + else + return 0; +} + +int ui_clicking( u32 mask ) +{ + if( vg_ui.ignore_input_frames ) return 0; + return vg_ui.mouse_state[0] & mask; +} + +int ui_click_up( u32 mask ) +{ + if( vg_ui.ignore_input_frames ) return 0; + if( (vg_ui.mouse_state[1] & mask) && + !(vg_ui.mouse_state[0] & mask) ) + return 1; + else + return 0; +} + +void ui_prerender(void) +{ + int x, y; + vg_ui.mouse_state[1] = vg_ui.mouse_state[0]; + vg_ui.mouse_state[0] = SDL_GetMouseState( &x, &y ); + vg_ui.mouse_delta[0] = x-vg_ui.mouse[0]; + vg_ui.mouse_delta[1] = y-vg_ui.mouse[1]; + vg_ui.mouse[0] = x; + vg_ui.mouse[1] = y; + + vg_ui.cur_vert = 0; + vg_ui.cur_indice = 0; + vg_ui.vert_start = 0; + vg_ui.indice_start = 0; + vg_ui.focused_control_hit = 0; + vg_ui.cursor = k_ui_cursor_default; + vg_ui.wants_mouse = 0; + + if( vg_ui.ignore_input_frames ){ + vg_ui.ignore_input_frames --; + return; + } + + if( ui_click_down(UI_MOUSE_LEFT)||ui_click_down(UI_MOUSE_MIDDLE) ){ + vg_ui.mouse_click[0] = vg_ui.mouse[0]; + vg_ui.mouse_click[1] = vg_ui.mouse[1]; + } +} + +u32 ui_colour( enum ui_scheme_colour id ) +{ + return vg_ui.scheme[ id ]; +} + +/* get an appropriately contrasting colour given the base */ +u32 ui_colourcont( enum ui_scheme_colour id ) +{ + if ( id < k_ui_bg+6 ) return ui_colour( k_ui_fg ); + else if( id < k_ui_fg ) return ui_colour( k_ui_bg+1 ); + else if( id < k_ui_hue ) return ui_colour( k_ui_bg+3 ); + else if( id < k_ui_red+k_ui_brighter ) return ui_colour( k_ui_fg ); + else return ui_colour( k_ui_fg+1 ); +} + +void ui_hex_to_norm( u32 hex, v4f norm ) +{ + norm[0] = ((hex ) & 0xff); + norm[1] = ((hex>>8 ) & 0xff); + norm[2] = ((hex>>16) & 0xff); + norm[3] = ((hex>>24) & 0xff); + v4_muls( norm, 1.0f/255.0f, norm ); +} + +u32 v4f_u32_colour( v4f colour ) +{ + u32 r = colour[0] * 255.0f, + g = colour[1] * 255.0f, + b = colour[2] * 255.0f, + a = colour[3] * 255.0f; + + return r | (g<<8) | (b<<16) | (a<<24); +} + +static void ui_text_glyph( const struct ui_font *font, ui_px scale, + u8 glyph, ui_rect out_texcoords ){ + glyph -= font->ascii_start; + + ui_px per_row = font->sheet_size / font->glyph_width, + column = (ui_px)glyph % per_row, + row = (glyph - column) / per_row; + + out_texcoords[0] = column * font->glyph_width; + out_texcoords[1] = row * font->glyph_height + font->offset_y; + out_texcoords[2] = out_texcoords[0] + font->glyph_width; + out_texcoords[3] = out_texcoords[1] + font->glyph_height; +} + +u32 ui_opacity( u32 colour, f32 opacity ) +{ + u32 alpha = opacity * 255.0f; + return (colour & 0x00ffffff) | (alpha << 24); +} + +u32 ui_ntext( ui_rect rect, const char *str, u32 len, ui_px scale, + enum ui_align align, u32 colour ) +{ + ui_rect text_cursor; + if( colour == 0 ) colour = ui_colour( k_ui_fg ); + + colour &= 0x00ffffff; + + const char *_c = str; + u8 c; + + text_cursor[0] = ui_text_aligned_x( str, rect, scale, align ); + text_cursor[1] = rect[1]; + text_cursor[2] = vg_ui.font->glyph_width*scale; + text_cursor[3] = vg_ui.font->glyph_height*scale; + + u32 printed_chars = 0; + + if( align & (k_ui_align_middle|k_ui_align_bottom) ){ + ui_px height = ui_text_string_height( str ) * scale; + + if( align & k_ui_align_bottom ) + text_cursor[1] += rect[3]-height; + else + text_cursor[1] += (rect[3]-height)/2; + } + + while( (c = *(_c ++)) ){ + if( printed_chars >= len ){ + printed_chars = 0; + text_cursor[1] += vg_ui.font->line_height*scale; + text_cursor[0] = ui_text_aligned_x( _c, rect, scale, align ); + text_cursor[0] -= vg_ui.font->spacing*scale; + + ui_rect glyph; + ui_text_glyph( vg_ui.font, scale, '\xb6' /*FIXME*/, glyph ); + ui_fill_rect( text_cursor, 0x00ffffff, glyph ); + text_cursor[0] += vg_ui.font->spacing*scale; + } + + if( c == '\n' ){ + text_cursor[1] += vg_ui.font->line_height*scale; + text_cursor[0] = ui_text_aligned_x( _c, rect, scale, align ); + printed_chars = 0; + continue; + } + else if( c >= 33 ){ + ui_rect glyph; + ui_text_glyph( vg_ui.font, scale, c, glyph ); + + ui_rect cursor_clipped; + if( ui_clip( rect, text_cursor, cursor_clipped ) ){ + ui_fill_rect( cursor_clipped, colour, glyph ); + } + } + else if( c == '\x1B' ){ + /* vt codes */ + _c ++; + u16 colour_id = 0; + for( int i=0; i<3; i ++ ){ + if( _c[i] ){ + if( _c[i] == 'm' ){ + _c = _c + i + 1; + + switch( colour_id ){ + case '0': colour = ui_colour( k_ui_fg ); break; + case '3'|'0'<<8: colour = ui_colour( k_ui_bg ); break; + case '3'|'1'<<8: colour = ui_colour( k_ui_red ); break; + case '3'|'2'<<8: colour = ui_colour( k_ui_green ); break; + case '3'|'3'<<8: colour = ui_colour( k_ui_yellow ); break; + case '3'|'4'<<8: colour = ui_colour( k_ui_blue ); break; + case '3'|'5'<<8: colour = ui_colour( k_ui_purple ); break; + case '3'|'6'<<8: colour = ui_colour( k_ui_aqua ); break; + case '3'|'7'<<8: colour = 0xffffffff; break; + } + + colour &= 0x00ffffff; + break; + } + + colour_id |= _c[i] << (i*8); + } + else{ + _c = _c +i; + break; + } + } + + continue; + } + else if( c == '\t' ){ + text_cursor[0] += vg_ui.font->spacing*scale*4; + printed_chars += 4; + continue; + } + + text_cursor[0] += vg_ui.font->spacing*scale; + printed_chars ++; + } + + return printed_chars; +} + +void ui_text( ui_rect rect, const char *str, ui_px scale, + enum ui_align align, u32 colour ) +{ + ui_ntext( rect, str, 1024, scale, align, colour ); +} + +/* + * Standard layout stuff + * ----------------------------------------------------------------------------- + */ + +void ui_panel( ui_rect in_rect, ui_rect out_panel ) +{ + ui_fill( in_rect, ui_colour( k_ui_bg+1 ) ); + ui_outline( in_rect, 1, ui_colour( k_ui_bg+7 ), 0 ); + rect_copy( in_rect, out_panel ); + ui_rect_pad( out_panel, (ui_px[2]){ k_ui_padding, k_ui_padding } ); +} + +void ui_label( ui_rect rect, const char *text, ui_px size, + ui_px gap, ui_rect r ) +{ + ui_rect l; + ui_px width = (ui_text_line_width(text)+vg_ui.font->spacing) * size; + ui_split( rect, k_ui_axis_v, width, gap, l, r ); + ui_text( l, text, 1, k_ui_align_middle_left, 0 ); +} + +void ui_standard_widget( ui_rect inout_panel, ui_rect out_rect, ui_px count ) +{ + ui_px height = (count * vg_ui.font->glyph_height + 18) * k_ui_scale; + ui_split( inout_panel, k_ui_axis_h, height, k_ui_padding, + out_rect, inout_panel ); +} + +void ui_info( ui_rect inout_panel, const char *text ) +{ + ui_rect box; + ui_standard_widget( inout_panel, box, 1 ); + ui_text( box, text, 1, k_ui_align_middle_left, 0 ); +} + +void ui_image( ui_rect rect, GLuint image ) +{ + ui_flush( k_ui_shader_colour, vg.window_x, vg.window_y ); + glActiveTexture( GL_TEXTURE0 ); + glBindTexture( GL_TEXTURE_2D, image ); + ui_fill_rect( rect, 0xffffffff, (ui_px[4]){ 0,256,256,0 } ); + ui_flush( k_ui_shader_image, vg.window_x, vg.window_y ); +} + +void ui_defocus_all(void) +{ + if( vg_ui.focused_control_type == k_ui_control_textbox ){ + SDL_StopTextInput(); + if( vg_ui.textbox.callbacks.escape ) + vg_ui.textbox.callbacks.escape(); + } + + vg_ui.focused_control_id = NULL; + vg_ui.focused_control_hit = 0; + vg_ui.focused_control_type = k_ui_control_none; +} + +/* TODO: split this out into a formatless button and one that auto fills */ +enum ui_button_state ui_colourbutton( ui_rect rect, + enum ui_scheme_colour colour, + enum ui_scheme_colour hover_colour, + enum ui_scheme_colour hi_colour, + bool const fill ) +{ + int clickup= ui_click_up(UI_MOUSE_LEFT), + click = ui_clicking(UI_MOUSE_LEFT) | clickup, + target = ui_inside_rect( rect, vg_ui.mouse_click ) && click, + hover = ui_inside_rect( rect, vg_ui.mouse ); + + u32 col_base = vg_ui.scheme[ colour ], + col_highlight = vg_ui.scheme[ hi_colour? hi_colour: k_ui_fg ], + col_hover = vg_ui.scheme[ hover_colour? hover_colour: + colour + k_ui_brighter ]; + + if( vg_ui.focused_control_type != k_ui_control_none ){ + clickup = 0; + click = 0; + target = 0; + hover = 0; + } + + if( hover ){ + vg_ui.cursor = k_ui_cursor_hand; + } + + if( click ){ + if( target ){ + if( hover ){ + if( clickup ){ + vg_ui.ignore_input_frames = 2; + ui_defocus_all(); + + if( fill ) { + ui_fill( rect, col_highlight ); + rect_copy( rect, vg_ui.click_fader ); + rect_copy( rect, vg_ui.click_fader_end ); + vg_ui.click_fader_end[3] = 0; + ui_rect_center( rect, vg_ui.click_fader_end ); + vg_ui.click_fade_opacity = 1.0f; + } + + return k_ui_button_click; + } + else{ + if( fill ) ui_fill( rect, col_highlight ); + return k_ui_button_holding_inside; + } + } + else{ + if( fill ) ui_fill( rect, col_base ); + ui_outline( rect, 1, col_highlight, 0 ); + return k_ui_button_holding_outside; + } + } + else{ + if( fill ) ui_fill( rect, col_base ); + return k_ui_button_none; + } + } + else{ + if( hover ){ + if( fill ) ui_fill( rect, col_hover ); + return k_ui_button_hover; + } + else{ + if( fill ) ui_fill( rect, col_base ); + return k_ui_button_none; + } + } +} + +enum ui_button_state ui_colourbutton_text( + ui_rect rect, const char *string, ui_px scale, + enum ui_scheme_colour colour ){ + enum ui_button_state state = ui_colourbutton( rect, colour, 0, 0, 1 ); + ui_rect t = { 0,0, ui_text_line_width( string )*scale, 14*scale }; + ui_rect_center( rect, t ); + + u32 text_colour = ui_colourcont(colour); + if( state == k_ui_button_holding_inside ) + text_colour = colour; + + ui_text( t, string, scale, k_ui_align_left, text_colour ); + return state; +} + +enum ui_button_state ui_button_text( ui_rect rect, + const char *string, ui_px scale ) +{ + return ui_colourbutton_text( rect, string, scale, k_ui_bg+4 ); +} + +enum ui_button_state ui_button( ui_rect inout_panel, const char *string ) +{ + ui_rect rect; + ui_standard_widget( inout_panel, rect, 1 ); + return ui_colourbutton_text( rect, string, 1, k_ui_bg+4 ); +} + +static void ui_enum_post(void); +void ui_postrender(void) +{ + if( vg_ui.click_fade_opacity > 0.0f ){ + float scale = vg_ui.click_fade_opacity; + scale = vg_maxf( 1.0f/255.0f, scale*scale ); + + vg_ui.click_fade_opacity -= vg.time_frame_delta * 3.8f; + u32 colour = (0x00ffffff & ui_colour(k_ui_fg))|0x7f000000; + + v4f begin, end, dest; + for( int i=0; i<4; i++ ){ + begin[i] = vg_ui.click_fader[i]; + end[i] = vg_ui.click_fader_end[i]+1; + } + + v4_lerp( end, begin, scale, dest ); + + ui_rect rect; + for( int i=0; i<4; i++ ){ + rect[i] = dest[i]; + } + + ui_fill( rect, colour ); + } + + if( vg_ui.focused_control_type == k_ui_control_enum ){ + ui_enum_post(); + } + else if( vg_ui.focused_control_type == k_ui_control_modal ){ + ui_rect screen = {0,0,vg.window_x,vg.window_y}; + ui_fill( screen, 0xa0000000 ); + ui_rect box = {0,0,400,200}; + + u32 colour = ui_colour(k_ui_fg), + type = vg_ui.modal.options & UI_MODAL_TYPE_BITS; + if ( type == 1 ) colour = ui_colour(k_ui_green); + else if( type == 2 ) colour = ui_colour(k_ui_red); + else if( type == 3 ) colour = ui_colour(k_ui_yellow); + + ui_rect_center( screen, box ); + ui_fill( box, ui_colour(k_ui_bg) ); + ui_outline( box, -1, colour, 0 ); + + ui_rect message; + rect_copy( box, message ); + message[3] = 100; + ui_rect_center( box, message ); + + ui_rect row0, row1, btn; + ui_split_ratio( message, k_ui_axis_h, 0.5f, 0, row0, row1 ); + row0[0] += vg_ui.font->spacing; + ui_ntext( row0, vg_ui.modal.message, (box[2]/vg_ui.font->spacing)-2, 1, + k_ui_align_left, colour ); + + rect_copy( row1, btn ); + btn[2] = 86; + btn[3] = 28; + ui_rect_center( row1, btn ); + + vg_ui.focused_control_type = k_ui_control_none; /* HACK */ + if( ui_button_text( btn, "OK", 1 ) != 1 ) + vg_ui.focused_control_hit = 1; + vg_ui.focused_control_type = k_ui_control_modal; /* HACK */ + vg_ui.wants_mouse = 1; + } + + ui_flush( k_ui_shader_colour, vg.window_x, vg.window_y ); + + if( !vg_ui.focused_control_hit ){ + ui_defocus_all(); + } + + if( vg_ui.wants_mouse ){ + SDL_SetWindowGrab( vg.window, SDL_FALSE ); + SDL_SetRelativeMouseMode( SDL_FALSE ); + } + else{ + SDL_SetWindowGrab( vg.window, SDL_TRUE ); + SDL_SetRelativeMouseMode( SDL_TRUE ); + } + + SDL_SetCursor( vg_ui.cursor_map[ vg_ui.cursor ] ); + SDL_ShowCursor(1); +} + +/* + * checkbox + * ----------------------------------------------------------------------------- + */ + +int ui_checkbox( ui_rect inout_panel, const char *str_label, i32 *data ) +{ + ui_rect rect, label, box; + ui_standard_widget( inout_panel, rect, 1 ); + + ui_split( rect, k_ui_axis_v, -rect[3], 0, label, box ); + ui_text( label, str_label, k_ui_scale, k_ui_align_middle_left, 0 ); + + int changed = ui_colourbutton( box, k_ui_bg, 0, 0, 1 )==1; + if( changed ) + *data = (*data) ^ 0x1; + + if( *data ){ + ui_rect_pad( box, (ui_px[2]){4,4} ); + ui_fill( box, ui_colour( k_ui_orange ) ); + } + + return changed; +} + +/* + * Dropdown / Enum + * ----------------------------------------------------------------------------- + */ + +/* + * unfortunately no return value since we only find out that event in the + * postrender step. + */ +void ui_enum( ui_rect inout_panel, const char *str_label, + struct ui_enum_opt *options, u32 len, i32 *value ) +{ + ui_rect rect, label, box; + ui_standard_widget( inout_panel, rect, 1 ); + ui_label( rect, str_label, k_ui_scale, 0, box ); + + const char *display = "OUT OF RANGE"; + int valid = 0; + for( u32 i=0; iterminator to start */ + int remaining_length = strlen( vg_ui.textbuf )+1-end; + memmove( &vg_ui.textbuf[ start ], + &vg_ui.textbuf[ end ], + remaining_length ); + return start; +} + +static void _ui_textbox_to_clipboard(void){ + int start, end; + _ui_textbox_make_selection( &start, &end ); + char buffer[512]; + + if( end-start ){ + memcpy( buffer, &vg_ui.textbuf[ start ], end-start ); + buffer[ end-start ] = 0x00; + SDL_SetClipboardText( buffer ); + } +} + +static void _ui_textbox_change_callback(void){ + if( vg_ui.textbox.callbacks.change ){ + vg_ui.textbox.callbacks.change( vg_ui.textbuf, vg_ui.textbox.len ); + + /* we gave permission to modify the buffer in this callback so.. */ + int len = strlen( vg_ui.textbuf ); + vg_ui.textbox.cursor_user = VG_MIN( vg_ui.textbox.cursor_user, len ); + vg_ui.textbox.cursor_pos = VG_MIN( vg_ui.textbox.cursor_pos, len ); + } +} + +void ui_start_modal( const char *message, u32 options ); +static void _ui_textbox_clipboard_paste(void){ + if( !SDL_HasClipboardText() ) + return; + + char *text = SDL_GetClipboardText(); + + if( !text ) + return; + + int datastart = _ui_textbox_delete_char( 0 ); + int length = strlen( text ); + + if( (vg_ui.textbox.len - strlen(vg_ui.textbuf)) < length ){ + ui_start_modal( "Clipboard content exceeds buffer size.", UI_MODAL_BAD ); + return; + } + + int cpylength = _ui_textbox_makeroom( datastart, length ); + + memcpy( vg_ui.textbuf + datastart, text, cpylength); + _ui_textbox_move_cursor( &vg_ui.textbox.cursor_user, + &vg_ui.textbox.cursor_pos, cpylength, 1 ); + SDL_free( text ); + _ui_textbox_change_callback(); +} + +void _ui_textbox_put_char( char c ) +{ + vg_ui.textbox.cursor_user = _ui_textbox_delete_char(0); + if( (vg_ui.textbox.len - strlen(vg_ui.textbuf)) <= 1 ) return; + + if( _ui_textbox_makeroom( vg_ui.textbox.cursor_user, 1 ) ) + vg_ui.textbuf[ vg_ui.textbox.cursor_user ] = c; + + _ui_textbox_move_cursor( &vg_ui.textbox.cursor_user, + &vg_ui.textbox.cursor_pos, 1, 1 ); +} + +/* Receed secondary cursor */ +void _ui_textbox_left_select(void) +{ + _ui_textbox_move_cursor( &vg_ui.textbox.cursor_user, NULL, -1, 0 ); +} + +/* Match and receed both cursors */ +void _ui_textbox_left(void) +{ + int cursor_diff = vg_ui.textbox.cursor_pos - vg_ui.textbox.cursor_user? 0: 1; + + _ui_textbox_move_cursor( &vg_ui.textbox.cursor_user, + &vg_ui.textbox.cursor_pos, -cursor_diff, 1 ); +} + +void _ui_textbox_up(void) +{ + if( vg_ui.textbox.flags & UI_TEXTBOX_MULTILINE ){ + int line_begin = vg_ui.textbox.cursor_user; + + while( line_begin ){ + if( vg_ui.textbuf[ line_begin-1 ] == '\n' ){ + break; + } + + line_begin --; + } + + if( line_begin ){ + int line_above_begin = line_begin-1; + + while( line_above_begin ){ + if( vg_ui.textbuf[ line_above_begin-1 ] == '\n' ){ + break; + } + + line_above_begin --; + } + + int offset = vg_ui.textbox.cursor_user - line_begin, + line_length_above = line_begin - line_above_begin -1; + + offset = VG_MIN( line_length_above, offset ); + + vg_ui.textbox.cursor_user = line_above_begin+offset; + vg_ui.textbox.cursor_pos = line_above_begin+offset; + } + else{ + vg_ui.textbox.cursor_user = line_begin; + vg_ui.textbox.cursor_pos = line_begin; + } + } + else{ + if( vg_ui.textbox.callbacks.up ){ + vg_ui.textbox.callbacks.up( vg_ui.textbuf, vg_ui.textbox.len ); + } + } +} + +void _ui_textbox_down(void) +{ + if( vg_ui.textbox.flags & UI_TEXTBOX_MULTILINE ){ + int line_begin = vg_ui.textbox.cursor_user; + + while( line_begin ){ + if( vg_ui.textbuf[ line_begin-1 ] == '\n' ){ + break; + } + + line_begin --; + } + + int line_below_begin = vg_ui.textbox.cursor_user; + + while(1){ + if( vg_ui.textbuf[ line_below_begin ] == '\0' ){ + vg_ui.textbox.cursor_user = line_below_begin; + vg_ui.textbox.cursor_pos = line_below_begin; + return; + } + + if( vg_ui.textbuf[ line_below_begin ] == '\n' ){ + line_below_begin ++; + break; + } + + line_below_begin ++; + } + + int line_below_end = line_below_begin; + while(1){ + if( vg_ui.textbuf[ line_below_end ] == '\0' || + vg_ui.textbuf[ line_below_end ] == '\n' ){ + line_below_end ++; + break; + } + line_below_end ++; + } + + int offset = vg_ui.textbox.cursor_user - line_begin, + line_length_below = line_below_end - line_below_begin -1; + + offset = VG_MIN( line_length_below, offset ); + + vg_ui.textbox.cursor_user = line_below_begin+offset; + vg_ui.textbox.cursor_pos = line_below_begin+offset; + } + else{ + if( vg_ui.textbox.callbacks.down ){ + vg_ui.textbox.callbacks.down( vg_ui.textbuf, vg_ui.textbox.len ); + } + } +} + +void _ui_textbox_right_select(void) +{ + _ui_textbox_move_cursor( &vg_ui.textbox.cursor_user, NULL, 1, 0 ); +} + +void _ui_textbox_right(void) +{ + int cursor_diff = vg_ui.textbox.cursor_pos - vg_ui.textbox.cursor_user? 0: 1; + + _ui_textbox_move_cursor( &vg_ui.textbox.cursor_user, + &vg_ui.textbox.cursor_pos, +cursor_diff, 1 ); +} + +void _ui_textbox_backspace(void) +{ + if( vg_ui.focused_control_type == k_ui_control_textbox ){ + vg_ui.textbox.cursor_user = _ui_textbox_delete_char( -1 ); + vg_ui.textbox.cursor_pos = vg_ui.textbox.cursor_user; + _ui_textbox_change_callback(); + } +} + +void _ui_textbox_delete(void) +{ + if( vg_ui.focused_control_type == k_ui_control_textbox ){ + vg_ui.textbox.cursor_user = _ui_textbox_delete_char( 1 ); + vg_ui.textbox.cursor_pos = vg_ui.textbox.cursor_user; + _ui_textbox_change_callback(); + } +} + +void _ui_textbox_home_select(void) +{ + i32 start = vg_ui.textbox.cursor_user; + + if( vg_ui.textbox.flags & UI_TEXTBOX_MULTILINE ){ + while( start ){ + if( vg_ui.textbuf[start-1] == '\n' ) + break; + else + start --; + } + } + else + start = 0; + + vg_ui.textbox.cursor_user = start; +} + +void _ui_textbox_home(void) +{ + _ui_textbox_home_select(); + vg_ui.textbox.cursor_pos = vg_ui.textbox.cursor_user; +} + +void _ui_textbox_end_select(void) +{ + i32 end = vg_ui.textbox.cursor_user; + + if( vg_ui.textbox.flags & UI_TEXTBOX_MULTILINE ){ + while( vg_ui.textbuf[end] ){ + if( vg_ui.textbuf[end] == '\n' ) + break; + else + end ++; + } + } + else + end = VG_MIN( vg_ui.textbox.len-1, strlen(vg_ui.textbuf) ); + + vg_ui.textbox.cursor_user = end; +} + +void _ui_textbox_end(void) +{ + _ui_textbox_end_select(); + vg_ui.textbox.cursor_pos = vg_ui.textbox.cursor_user; +} + +void _ui_textbox_select_all(void) +{ + _ui_textbox_move_cursor( &vg_ui.textbox.cursor_user, NULL, 10000, 0); + _ui_textbox_move_cursor( &vg_ui.textbox.cursor_pos, NULL, -10000, 0); +} + +void _ui_textbox_cut(void) +{ + _ui_textbox_to_clipboard(); + vg_ui.textbox.cursor_user = _ui_textbox_delete_char(0); + vg_ui.textbox.cursor_pos = vg_ui.textbox.cursor_user; + _ui_textbox_change_callback(); +} + +void _ui_textbox_enter(void) +{ + if( vg_ui.focused_control_type == k_ui_control_textbox ){ + vg_ui.ignore_input_frames = 2; + + if( vg_ui.textbox.callbacks.enter ) + vg_ui.textbox.callbacks.enter( vg_ui.textbuf, vg_ui.textbox.len ); + + if( vg_ui.focused_control_type != k_ui_control_textbox ) return; + + if( vg_ui.textbox.flags & UI_TEXTBOX_MULTILINE ){ + _ui_textbox_put_char( '\n' ); + _ui_textbox_change_callback(); + } + else{ + if( !(vg_ui.textbox.flags & UI_TEXTBOX_AUTOFOCUS ) ) + ui_defocus_all(); + } + } +} + +/* + * based on a visual character coordinate relative to the anchor of the textbox, + * this works out the linear place in the buffer that coordinate maps to + * + * input coordinates go in co[0], co[1], and the result index is in co[2] + */ +static void _ui_textbox_calc_index_from_grid( int co[3], int wrap_length ){ + int i[3] = {0,0,0}; + + char c; + while( (c = vg_ui.textbuf[i[2]]) ){ + if( i[1]==co[1] && i[0]>=co[0] ) break; + + if( i[0] >= wrap_length ){ + i[1] ++; + i[0] = 0; + } + + if( c >= 32 && c <= 126 ){ + i[0] ++; + i[2] ++; + } + else if( c == '\n' ){ + i[1] ++; + + if( i[1] > co[1] ) break; + + i[2] ++; + i[0] = 0; + } + else i[2] ++; + } + + co[0] = i[0]; + co[1] = i[1]; + co[2] = i[2]; +} + +/* + * based on the index specied in co[2], work out the visual character + * coordinates and store them in co[0], co[1] + */ +static void _ui_textbox_index_calc_coords( int co[3], int wrap_length ){ + co[0] = 0; + co[1] = 0; + + char c; + int i=0; + + while( (c = vg_ui.textbuf[i ++]) ){ + if( i > co[2] ) break; + if( co[0] >= wrap_length ){ + co[1] ++; + co[0] = 0; + } + if( c >= 32 && c <= 126 ) co[0] ++; + else if( c == '\n' ){ + co[1] ++; + co[0] = 0; + } + } +} + +/* + * calculate the number of characters remaining until either: + * - the wrap_length limit is hit + * - end of the line/string + * + * index must be fully populated with visual X/Y, and linear index + */ +static int _ui_textbox_run_remaining( int index[3], int wrap_length ){ + int i=0, printed_chars=0; + char c; + while( (c = vg_ui.textbuf[index[2] + (i ++)]) ){ + if( index[0]+i >= wrap_length ) break; + if( c >= 32 && c <= 126 ) printed_chars ++; + else if( c == '\n' ) break; + } + + return printed_chars+1; +} + +int ui_textbox( ui_rect inout_panel, const char *label, + char *buf, u32 len, u32 lines, u32 flags, + struct ui_textbox_callbacks *callbacks ) +{ + if( lines > 1 ) flags |= UI_TEXTBOX_MULTILINE; + + ui_rect rect; + ui_standard_widget( inout_panel, rect, lines ); + + if( label ) + ui_label( rect, label, 1, 0, rect ); + + int clickup= ui_click_up(UI_MOUSE_LEFT), + clickdown = ui_click_down(UI_MOUSE_LEFT), + click = ui_clicking(UI_MOUSE_LEFT) | clickup, + target = ui_inside_rect( rect, vg_ui.mouse_click ) && click, + hover = ui_inside_rect( rect, vg_ui.mouse ); + + /* allow instant transitions from textbox->textbox */ + if( (vg_ui.focused_control_type != k_ui_control_none) && + (vg_ui.focused_control_type != k_ui_control_textbox) ){ + clickup = 0; + clickdown = 0; + click = 0; + target = 0; + hover = 0; + flags &= ~UI_TEXTBOX_AUTOFOCUS; + } + + u32 col_base = ui_colour( k_ui_bg ), + col_highlight = ui_colour( k_ui_fg ), + col_cursor = (0x00ffffff & ui_colour(k_ui_fg))|0x7f000000; + + ui_px border = -1; + + ui_rect text_rect; + rect_copy( rect, text_rect ); + + if( flags & UI_TEXTBOX_MULTILINE ) text_rect[3] = rect[3]-16; + else text_rect[3] = vg_ui.font->line_height; + + text_rect[2] -= 16; + ui_rect_center( rect, text_rect ); + + ui_px wrap_length = 1024; + + if( flags & UI_TEXTBOX_WRAP ) + wrap_length = text_rect[2] / vg_ui.font->spacing; + + if( hover ){ + vg_ui.cursor = k_ui_cursor_ibeam; + } + + if( vg_ui.focused_control_id == buf ){ + ui_fill( rect, col_base ); + ui_ntext( text_rect, buf, wrap_length, 1, k_ui_align_left, 0 ); + + if( !(flags & UI_TEXTBOX_AUTOFOCUS) && ((clickup||clickdown) && !target)){ + ui_defocus_all(); + } + else{ + vg_ui.focused_control_hit = 1; + if( click && target ){ + int p0[3] ={ + (vg_ui.mouse_click[0] - text_rect[0]) / vg_ui.font->spacing, + (vg_ui.mouse_click[1] - text_rect[1]) / vg_ui.font->line_height, + -1 + }, + p1[3] = { + (vg_ui.mouse[0] - text_rect[0]) / vg_ui.font->spacing, + (vg_ui.mouse[1] - text_rect[1]) / vg_ui.font->line_height, + -1 + }; + + if( flags & UI_TEXTBOX_MULTILINE ){ + _ui_textbox_calc_index_from_grid( p0, wrap_length ); + _ui_textbox_calc_index_from_grid( p1, wrap_length ); + + vg_ui.textbox.cursor_pos = p0[2]; + vg_ui.textbox.cursor_user = p1[2]; + } + else{ + int max = strlen( buf ); + vg_ui.textbox.cursor_pos = VG_MAX( 0, VG_MIN( max, p0[0] )), + vg_ui.textbox.cursor_user = VG_MAX( 0, VG_MIN( max, p1[0] )); + } + } + + ui_outline( rect, -2, vg_ui.scheme[ k_ui_orange ], 0 ); + + ui_rect cursor; + + int c0 = vg_ui.textbox.cursor_pos, + c1 = vg_ui.textbox.cursor_user, + start = VG_MIN( c0, c1 ), + end = VG_MAX( c0, c1 ), + chars = end-start; + + if( flags & (UI_TEXTBOX_WRAP|UI_TEXTBOX_MULTILINE) ){ + int pos[3], remaining = chars; + + pos[2] = start; + _ui_textbox_index_calc_coords( pos, wrap_length ); + + if( start==end ){ + cursor[0] = text_rect[0] + pos[0]*vg_ui.font->spacing-1; + cursor[1] = text_rect[1] + pos[1]*14; + cursor[2] = 2; + cursor[3] = 13; + ui_fill( cursor, col_cursor ); + rect_copy( cursor, vg_ui.click_fader_end ); + } + else{ + while( remaining ){ + int run = _ui_textbox_run_remaining( pos, wrap_length ); + run = VG_MIN( run, remaining ); + + cursor[0] = text_rect[0] + pos[0]*vg_ui.font->spacing-1; + cursor[1] = text_rect[1] + pos[1]*14; + cursor[2] = (float)(run)*(float)vg_ui.font->spacing; + cursor[3] = 13; + + ui_fill( cursor, col_cursor ); + + remaining -= run; + pos[0] = 0; + pos[1] ++; + pos[2] += run; + } + rect_copy( cursor, vg_ui.click_fader_end ); + } + } + else{ + cursor[0] = text_rect[0] + start*vg_ui.font->spacing-1; + cursor[1] = text_rect[1]; + cursor[3] = 13; + + if( start==end ){ + cursor[2] = 2; + } + else{ + cursor[2] = (float)(chars)*(float)vg_ui.font->spacing; + } + + if( (vg_ui.click_fade_opacity<=0.0f) && + ui_clip( rect, cursor, cursor ) ){ + ui_fill( cursor, col_cursor ); + } + + rect_copy( cursor, vg_ui.click_fader_end ); + } + } + + return 0; + } + + if( click || (flags & UI_TEXTBOX_AUTOFOCUS) ){ + if( (target && hover) || (flags & UI_TEXTBOX_AUTOFOCUS) ){ + ui_defocus_all(); + + ui_fill( rect, col_highlight ); + vg_ui.ignore_input_frames = 2; + rect_copy( rect, vg_ui.click_fader ); + rect_copy( rect, vg_ui.click_fader_end ); + + vg_ui.click_fade_opacity = 1.0f; + vg_ui.textbuf = buf; + vg_ui.focused_control_hit = 1; + vg_ui.focused_control_type = k_ui_control_textbox; + vg_ui.textbox.len = len; + vg_ui.textbox.flags = flags; + vg_ui.textbox.cursor_pos = 0; + vg_ui.textbox.cursor_user = 0; + + if( callbacks ){ + vg_ui.textbox.callbacks = *callbacks; + } + else{ + vg_ui.textbox.callbacks.change = NULL; + vg_ui.textbox.callbacks.down = NULL; + vg_ui.textbox.callbacks.up = NULL; + vg_ui.textbox.callbacks.enter = NULL; + } + + SDL_StartTextInput(); + } + } + + ui_fill( rect, col_base ); + + if( hover ){ + ui_outline( rect, -1, col_highlight, 0 ); + } + + ui_ntext( text_rect, buf, wrap_length, 1, k_ui_align_left, 0 ); + return 0; +} + +/* + * Tabs + * ----------------------------------------------------------------------------- + */ + +void ui_tabs( ui_rect inout_panel, ui_rect out_content_panel, + const char **titles, u32 count, i32 *page ) +{ + ui_rect bar; + ui_standard_widget( inout_panel, bar, 1 ); + + i32 cur_page = *page; + + f32 width = (f32)inout_panel[2] / (f32)count; + + ui_px h = (inout_panel[1] + inout_panel[3]) - (bar[1]+bar[3]); + inout_panel[1] = bar[1]+bar[3]; + inout_panel[3] = h; + + ui_fill( inout_panel, ui_colour( k_ui_bg+2 ) ); + ui_outline( inout_panel, 1, ui_colour( k_ui_bg+5 ), 0 ); + + rect_copy( inout_panel, out_content_panel ); + ui_rect_pad( out_content_panel, (ui_px[2]){ k_ui_padding, k_ui_padding } ); + + /* place buttons */ + for( i32 i=0; ikey == ev.sym ){ + if( mapping->mod == 0 ){ + if( mod == 0 ){ + mapping->handler(); + return; + } + } + else if( (mod & mapping->mod) == mapping->mod ){ + mapping->handler(); + return; + } + } + } +} + +/* + * Callback for text entry mode + */ +void ui_proc_utf8( const char *text ) +{ + if( vg_ui.focused_control_type == k_ui_control_textbox ){ + const char *ptr = text; + + while( *ptr ){ + if( *ptr != '`' ) _ui_textbox_put_char( *ptr ); + ptr ++; + } + + _ui_textbox_change_callback(); + } +} + +/* + * Development utils + * ----------------------------------------------------------------------------- + */ + +void ui_dev_colourview(void) +{ + ui_rect window = {vg.window_x-256,0,256,vg.window_y}, swatch; + + const char *names[vg_list_size(vg_ui.scheme)] = { + [k_ui_bg] = "k_ui_bg", "k_ui_bg+1", "k_ui_bg+2", "k_ui_bg+3", + "k_ui_bg+4", "k_ui_bg+5", "k_ui_bg+6", "k_ui_bg+7", + + [k_ui_fg] = "k_ui_fg", "k_ui_fg+1", "k_ui_fg+2", "k_ui_fg+3", + "k_ui_fg+4", "k_ui_fg+5", "k_ui_fg+6", "k_ui_fg+7", + + [k_ui_red] = "k_ui_red", "k_ui_orange", "k_ui_yellow", "k_ui_green", + "k_ui_aqua", "k_ui_blue", "k_ui_purple", "k_ui_gray", + "k_ui_red+8","k_ui_orange+8","k_ui_yellow+8","k_ui_green+8", + "k_ui_aqua+8","k_ui_blue+8","k_ui_purple+8","k_ui_gray+8" }; + + ui_rect col[2]; + ui_split_ratio( window, k_ui_axis_v, 0.5f, 0, col[0], col[1] ); + + for( int i=0; i