From 5c4e9c8b140b14ba9671b8efcc47d71c6c4f2217 Mon Sep 17 00:00:00 2001 From: kasull Date: Tue, 3 Mar 2026 12:22:40 -0500 Subject: ui header/menu and add undo/redo for creation actions full-width toolbar/menubar layer move save into file menu and add edit menu actions for undo/redo menubar dropdown interaction modal update header/view-type labels and menu layout behavior creation-history tracking for walls and polygons undo (ctrl+z) and redo (ctrl+r) in 2d editor improve list/button visual states --- src/editor/editor.cpp | 343 ++++++++++++++++++++++++++++++++++++++++++++++++ src/editor/editor.h | 24 +++- src/editor/gui.cpp | 352 +++++++++++++++++++++++++++++++++++++++++++++++--- src/editor/view2d.cpp | 29 +++++ src/gui/button.cpp | 5 +- src/gui/list.cpp | 32 +++-- 6 files changed, 756 insertions(+), 29 deletions(-) (limited to 'src') diff --git a/src/editor/editor.cpp b/src/editor/editor.cpp index 3c67ecd..63360d1 100644 --- a/src/editor/editor.cpp +++ b/src/editor/editor.cpp @@ -4,6 +4,347 @@ GAME_EDITOR* editor = 0; +static void editor_push_undo_action( GAME_EDITOR* e, const GAME_EDITOR::EDITOR_UNDO_ACTION& action ) { + if( !e ) + return; + + e->redo_actions.clear(); + e->undo_actions.push( action ); +} + +static void editor_refresh_after_map_change( GAME_EDITOR* e ) { + if( !e || !e->map ) + return; + + map_check_bounds( e->map ); + if( e->gui.props && e->propgrid ) + editor_update_properties_column( e ); + if( e->gui.tool ) + gui_editor_toolview_update( e->gui.tool ); +} + +static U8 editor_clr_eq( const CLR& a, const CLR& b ) { + return a.r == b.r && a.g == b.g && a.b == b.b && a.a == b.a; +} + +static U8 editor_vertex_eq( const MAP_VERTEX& a, const MAP_VERTEX& b ) { + return a.pos == b.pos + && a.normal == b.normal + && a.uv == b.uv + && editor_clr_eq( a.clr, b.clr ); +} + +static U8 editor_wall_eq( const MAP_WALL& a, const MAP_WALL& b ) { + return a.start == b.start + && a.end == b.end + && a.uvstart == b.uvstart + && a.uvend == b.uvend + && a.propid == b.propid; +} + +static U8 editor_poly_eq( const MAP_POLYGON& a, const MAP_POLYGON& b ) { + if( a.type != b.type || a.propid != b.propid ) + return 0; + if( !( a.mins == b.mins ) || !( a.maxs == b.maxs ) ) + return 0; + if( a.vertices.size != b.vertices.size ) + return 0; + + for( U32 i = 0; i < a.vertices.size; ++i ) { + if( !editor_vertex_eq( a.vertices.data[i], b.vertices.data[i] ) ) + return 0; + } + return 1; +} + +static I32 editor_clamp_valid_idx( I32 idx, I32 size ) { + if( size <= 0 ) + return -1; + if( idx < 0 ) + return 0; + if( idx >= size ) + return size - 1; + return idx; +} + +static I32 editor_idx_distance( I32 a, I32 b ) { + return a > b ? a - b : b - a; +} + +template +static I32 editor_find_nearest_idx( I32 size, I32 expected_idx, MATCH_FN match ) { + if( size <= 0 ) + return -1; + + expected_idx = editor_clamp_valid_idx( expected_idx, size ); + if( match( expected_idx ) ) + return expected_idx; + + I32 best = -1; + I32 best_dist = 0x7fffffff; + for( I32 i = 0; i < size; ++i ) { + if( !match( i ) ) + continue; + + I32 dist = editor_idx_distance( i, expected_idx ); + if( best == -1 || dist < best_dist ) { + best = i; + best_dist = dist; + } + } + + return best; +} + +static I32 editor_find_wall_idx( WORLD_MAP* map, const MAP_WALL& want, I32 expected_idx ) { + if( !map ) + return -1; + + return editor_find_nearest_idx( (I32)map->walls.size, expected_idx, fn( I32 i ) { + return editor_wall_eq( map->walls[i], want ); + } ); +} + +static I32 editor_find_poly_idx( WORLD_MAP* map, const MAP_POLYGON& want, I32 expected_idx ) { + if( !map ) + return -1; + + return editor_find_nearest_idx( (I32)map->polygons.size, expected_idx, fn( I32 i ) { + return editor_poly_eq( map->polygons[i], want ); + } ); +} + +static void editor_mark_clear_wall_refs( GAME_EDITOR* e, MAP_WALL* w, U8* clear_props_select, U8* clear_view_select, U8* clear_view_drag ) { + if( e->gui.props ) { + if( e->gui.props->seltype == EDITOR_SELECT_WALL && e->gui.props->curselect == w ) + *clear_props_select = 1; + if( e->gui.props->seltype == EDITOR_SELECT_WVERTEX + && ( e->gui.props->curselect == &w->start || e->gui.props->curselect == &w->end ) ) + *clear_props_select = 1; + } + + if( e->gui.v2d ) { + if( e->gui.v2d->seltype == EDITOR_SELECT_WALL && e->gui.v2d->curselect == w ) + *clear_view_select = 1; + if( e->gui.v2d->seltype == EDITOR_SELECT_WVERTEX + && ( e->gui.v2d->curselect == &w->start || e->gui.v2d->curselect == &w->end ) ) + *clear_view_select = 1; + + if( e->gui.v2d->dragtype == EDITOR_SELECT_WALL && e->gui.v2d->curdrag == w ) + *clear_view_drag = 1; + if( e->gui.v2d->dragtype == EDITOR_SELECT_WVERTEX + && ( e->gui.v2d->curdrag == &w->start || e->gui.v2d->curdrag == &w->end ) ) + *clear_view_drag = 1; + } +} + +static void editor_mark_clear_poly_refs( GAME_EDITOR* e, MAP_POLYGON* p, U8* clear_props_select, U8* clear_view_select, U8* clear_view_drag ) { + if( e->gui.props ) { + if( e->gui.props->seltype == EDITOR_SELECT_POLY && e->gui.props->curselect == p ) + *clear_props_select = 1; + if( e->gui.props->seltype == EDITOR_SELECT_PVERTEX ) { + p->vertices.each( fn( MAP_VERTEX* v ) { + if( e->gui.props->curselect == v ) + *clear_props_select = 1; + } ); + } + } + + if( e->gui.v2d ) { + if( e->gui.v2d->seltype == EDITOR_SELECT_POLY && e->gui.v2d->curselect == p ) + *clear_view_select = 1; + if( e->gui.v2d->seltype == EDITOR_SELECT_PVERTEX ) { + p->vertices.each( fn( MAP_VERTEX* v ) { + if( e->gui.v2d->curselect == v ) + *clear_view_select = 1; + } ); + } + + if( e->gui.v2d->dragtype == EDITOR_SELECT_POLY && e->gui.v2d->curdrag == p ) + *clear_view_drag = 1; + if( e->gui.v2d->dragtype == EDITOR_SELECT_PVERTEX ) { + p->vertices.each( fn( MAP_VERTEX* v ) { + if( e->gui.v2d->curdrag == v ) + *clear_view_drag = 1; + } ); + } + } +} + +void editor_undo_clear( GAME_EDITOR* e ) { + if( !e ) + return; + + e->undo_actions.clear(); + e->redo_actions.clear(); +} + +void editor_undo_record_create_walls( GAME_EDITOR* e, I32 start_idx, I32 count ) { + if( !e || !e->map || count <= 0 ) + return; + + WORLD_MAP* map = e->map; + if( start_idx < 0 || start_idx >= (I32)map->walls.size ) + return; + + I32 available = (I32)map->walls.size - start_idx; + if( count > available ) + count = available; + if( count <= 0 ) + return; + + GAME_EDITOR::EDITOR_UNDO_ACTION action{}; + action.type = EDITOR_UNDO_CREATE_WALLS; + action.start_idx = start_idx; + for( I32 i = 0; i < count; ++i ) + action.walls.push( map->walls[start_idx + i] ); + + editor_push_undo_action( e, action ); +} + +void editor_undo_record_create_poly( GAME_EDITOR* e, I32 start_idx ) { + if( !e || !e->map ) + return; + + WORLD_MAP* map = e->map; + if( start_idx < 0 || start_idx >= (I32)map->polygons.size ) + return; + + GAME_EDITOR::EDITOR_UNDO_ACTION action{}; + action.type = EDITOR_UNDO_CREATE_POLY; + action.start_idx = start_idx; + action.polys.push( map->polygons[start_idx] ); + + editor_push_undo_action( e, action ); +} + +static U8 editor_apply_undo_action_reverse( GAME_EDITOR* e, GAME_EDITOR::EDITOR_UNDO_ACTION& action ) { + if( !e || !e->map ) + return 0; + + WORLD_MAP* map = e->map; + U8 clear_props_select = 0; + U8 clear_view_select = 0; + U8 clear_view_drag = 0; + + switch( action.type ) { + case EDITOR_UNDO_CREATE_WALLS: { + I32 count = (I32)action.walls.size; + for( I32 i = count - 1; i >= 0; --i ) { + if( !map->walls.size ) + break; + + I32 expected = action.start_idx + i; + I32 idx = editor_find_wall_idx( map, action.walls[i], expected ); + if( idx < 0 ) + idx = editor_clamp_valid_idx( expected, (I32)map->walls.size ); + + MAP_WALL* w = &map->walls[idx]; + editor_mark_clear_wall_refs( e, w, &clear_props_select, &clear_view_select, &clear_view_drag ); + + map->walls.erase( idx ); + } + } break; + case EDITOR_UNDO_CREATE_POLY: { + I32 count = (I32)action.polys.size; + if( count <= 0 ) + return 0; + for( I32 i = count - 1; i >= 0; --i ) { + if( !map->polygons.size ) + break; + + I32 expected = action.start_idx + i; + I32 idx = i < (I32)action.polys.size ? editor_find_poly_idx( map, action.polys[i], expected ) : -1; + if( idx < 0 ) + idx = editor_clamp_valid_idx( expected, (I32)map->polygons.size ); + + MAP_POLYGON* p = &map->polygons[idx]; + editor_mark_clear_poly_refs( e, p, &clear_props_select, &clear_view_select, &clear_view_drag ); + + map->polygons.erase( idx ); + } + } break; + default: + return 0; + } + + if( e->gui.props && clear_props_select ) + gui_editor_propview_select( e->gui.props, 0, EDITOR_SELECT_NONE ); + if( e->gui.v2d ) { + if( clear_view_select ) { + e->gui.v2d->curselect = 0; + e->gui.v2d->seltype = EDITOR_SELECT_NONE; + } + if( clear_view_drag ) { + e->gui.v2d->curdrag = 0; + e->gui.v2d->dragtype = EDITOR_SELECT_NONE; + } + } + + editor_refresh_after_map_change( e ); + + return 1; +} + +static U8 editor_apply_undo_action_forward( GAME_EDITOR* e, GAME_EDITOR::EDITOR_UNDO_ACTION& action ) { + if( !e || !e->map ) + return 0; + + WORLD_MAP* map = e->map; + switch( action.type ) { + case EDITOR_UNDO_CREATE_WALLS: { + if( !action.walls.size ) + return 0; + + action.walls.each( fn( MAP_WALL* w ) { + map->walls.push( *w ); + } ); + } break; + case EDITOR_UNDO_CREATE_POLY: { + if( !action.polys.size ) + return 0; + + action.polys.each( fn( MAP_POLYGON* p ) { + map->polygons.push( *p ); + } ); + } break; + default: + return 0; + } + + editor_refresh_after_map_change( e ); + + return 1; +} + +U8 editor_undo( GAME_EDITOR* e ) { + if( !e || !e->map || !e->undo_actions.size ) + return 0; + + GAME_EDITOR::EDITOR_UNDO_ACTION action = e->undo_actions.pop(); + if( !editor_apply_undo_action_reverse( e, action ) ) { + e->undo_actions.push( action ); + return 0; + } + + e->redo_actions.push( action ); + return 1; +} + +U8 editor_redo( GAME_EDITOR* e ) { + if( !e || !e->map || !e->redo_actions.size ) + return 0; + + GAME_EDITOR::EDITOR_UNDO_ACTION action = e->redo_actions.pop(); + if( !editor_apply_undo_action_forward( e, action ) ) { + e->redo_actions.push( action ); + return 0; + } + + e->undo_actions.push( action ); + return 1; +} + void editor_register_grid_dependency( GAME_EDITOR* e, void* tag, EDITOR_GRID_DEP_CALLBACK cb ) { if( !e || !tag || !cb ) return; @@ -60,6 +401,7 @@ GAME_EDITOR* editor_create( GAME_DATA* game ) { } STAT editor_close( GAME_EDITOR* e ) { + editor_undo_clear( e ); game_unload_map( e->game ); e->map = 0; @@ -87,6 +429,7 @@ STAT editor_load_map( GAME_EDITOR* e, const char* mapname ) { return STAT_ERR; e->game->state.map = e->map = m; + editor_undo_clear( e ); gui_push_callback( e, pfn( void* d ) { editor_create_map_view( (GAME_EDITOR*)d ); } ); diff --git a/src/editor/editor.h b/src/editor/editor.h index 0fee667..2067df5 100644 --- a/src/editor/editor.h +++ b/src/editor/editor.h @@ -35,6 +35,12 @@ enum EditorSelectType_t { EDITOR_SELECT_SURFPROPS }; +enum EditorUndoType_t { + EDITOR_UNDO_NONE = 0, + EDITOR_UNDO_CREATE_WALLS = 1, + EDITOR_UNDO_CREATE_POLY = 2 +}; + struct GAME_EDITOR; typedef void( *EDITOR_GRID_DEP_CALLBACK )( GAME_EDITOR* e ); @@ -81,9 +87,9 @@ struct GAME_EDITOR { I32 view_mode{}; I32 view2d_type{}; + GUI_BASE* header_toolbar{}; GUI_LABEL* header_viewtype_label{}; GUI_BUTTON* header_back{}; - GUI_BUTTON* header_save{}; GUI_BUTTON* header_mode_2d{}; GUI_BUTTON* header_mode_3d{}; GUI_BUTTON* header_mode_sim{}; @@ -105,6 +111,15 @@ struct GAME_EDITOR { LIST grid_dependencies{}; LIST map_list{}; + + struct EDITOR_UNDO_ACTION { + U8 type{}; + I32 start_idx{}; + LIST walls{}; + LIST polys{}; + }; + LIST undo_actions{}; + LIST redo_actions{}; }; extern GAME_EDITOR* editor_create( struct GAME_DATA* game ); @@ -124,6 +139,11 @@ extern void editor_new_map_cb( void* ); extern void editor_create_map_view( GAME_EDITOR* e ); extern void editor_update_properties_column( GAME_EDITOR* e ); +extern void editor_undo_clear( GAME_EDITOR* e ); +extern void editor_undo_record_create_walls( GAME_EDITOR* e, I32 start_idx, I32 count ); +extern void editor_undo_record_create_poly( GAME_EDITOR* e, I32 start_idx ); +extern U8 editor_undo( GAME_EDITOR* e ); +extern U8 editor_redo( GAME_EDITOR* e ); struct GUI_EDITORWINDOW : GUI_WINDOW {}; struct GUI_EDITOR_3DVIEW : GUI_VIEW { @@ -153,6 +173,8 @@ struct GUI_EDITOR_2DVIEW : GUI_VIEW { U8 poly_drag; VEC2 poly_start; VEC2 poly_end; + + I32 pending_wall_undo_idx; }; struct GUI_EDITOR_PROPVIEW : GUI_VIEW { diff --git a/src/editor/gui.cpp b/src/editor/gui.cpp index 92f3f7c..04e69f1 100644 --- a/src/editor/gui.cpp +++ b/src/editor/gui.cpp @@ -6,8 +6,10 @@ const I32 EDITOR_LAYOUT_MARGIN = 10; const I32 EDITOR_LAYOUT_TITLE_OFFSET = 15; -const I32 EDITOR_LAYOUT_NAV_Y = 10; -const I32 EDITOR_LAYOUT_CONTENT_Y = 38; +const I32 EDITOR_LAYOUT_MENU_Y = 1; +const I32 EDITOR_LAYOUT_MENU_H = 22; +const I32 EDITOR_LAYOUT_NAV_Y = EDITOR_LAYOUT_MENU_Y + EDITOR_LAYOUT_MENU_H + 4; +const I32 EDITOR_LAYOUT_CONTENT_Y = EDITOR_LAYOUT_NAV_Y + 28; const I32 EDITOR_LAYOUT_COLUMN_GAP = 10; const I32 EDITOR_LAYOUT_VIEW_TOOL_GAP = 17; const I32 EDITOR_LAYOUT_TOOL_BTN_TOP_GAP = 3; @@ -40,6 +42,13 @@ struct GUI_EDITOR_VIEWTYPE_SEGMENT : GUI_BASE { I32 held_seg{-1}; }; +struct GUI_EDITOR_TOOLBAR : GUI_BASE { + U8 held{}; + I32 held_item{-1}; + U8 file_open{}; + U8 edit_open{}; +}; + enum EditorViewMode_t { EDITOR_VIEWMODE_2D = 0, EDITOR_VIEWMODE_3D = 1, @@ -53,6 +62,27 @@ enum Editor2DViewType_t { }; static void editor_layout_map_view( GAME_EDITOR* e ); +static U8 editor_menu_hover_mask_active = 0; +static I32 editor_menu_hover_real_x = 0; +static I32 editor_menu_hover_real_y = 0; + +enum EditorToolbarHit_t { + EDITOR_TOOLBAR_HIT_NONE = -1, + EDITOR_TOOLBAR_HIT_FILE = 0, + EDITOR_TOOLBAR_HIT_EDIT = 1, + EDITOR_TOOLBAR_HIT_VIEW = 2, + EDITOR_TOOLBAR_HIT_TOOLS = 3, + EDITOR_TOOLBAR_HIT_SAVE = 4, + EDITOR_TOOLBAR_HIT_UNDO = 5, + EDITOR_TOOLBAR_HIT_REDO = 6 +}; + +static void editor_toolbar_set_open( GUI_EDITOR_TOOLBAR* bar, U8 file_open, U8 edit_open ); + +static void editor_toolbar_close_menu() { + GUI_EDITOR_TOOLBAR* bar = (GUI_EDITOR_TOOLBAR*)editor->gui.header_toolbar; + editor_toolbar_set_open( bar, 0, 0 ); +} static void editor_apply_view_mode( GAME_EDITOR* e ) { if( !e || !e->gui.v2d || !e->gui.v3d ) @@ -70,14 +100,12 @@ static void editor_apply_view_mode( GAME_EDITOR* e ) { } static void editor_header_back_cb( void* ) { + editor_toolbar_close_menu(); editor_close( editor ); } -static void editor_header_save_cb( void* ) { - editor_save_map( editor ); -} - static void editor_set_view_mode( I32 mode ) { + editor_toolbar_close_menu(); editor->gui.view_mode = mode; editor_apply_view_mode( editor ); editor_layout_map_view( editor ); @@ -138,7 +166,7 @@ static void gui_editor_viewtype_segment_draw_fn( void* ptr ) { editor_header_viewtype_segment_widths( inner_w, &w0, &w1, &w2 ); I32 segx[3] = { inner_x, inner_x + w0, inner_x + w0 + w1 }; I32 segw[3] = { w0, w1, w2 }; - const char* txts[3] = { "top down", "side", "front" }; + const char* txts[3] = { "x/y", "x/z", "y/z" }; for( I32 i = 0; i < 3; ++i ) { U8 selected = i == active_seg; @@ -213,6 +241,250 @@ static GUI_EDITOR_VIEWTYPE_SEGMENT* gui_editor_viewtype_segment( I32 x, I32 y, I return seg; } +const I32 EDITOR_TOOLBAR_DROPDOWN_W = 74; + +static U8 editor_toolbar_pt_in_rect( I32 mx, I32 my, I32 x, I32 y, I32 w, I32 h ) { + return mx >= x && mx < x + w && my >= y && my < y + h; +} + +static void editor_toolbar_set_open( GUI_EDITOR_TOOLBAR* bar, U8 file_open, U8 edit_open ) { + if( !bar ) + return; + + bar->file_open = file_open; + bar->edit_open = edit_open; +} + +static void editor_toolbar_item_rect( GUI_EDITOR_TOOLBAR* bar, I32 idx, I32* rx, I32* ry, I32* rw, I32* rh ) { + static const I32 widths[4] = { 44, 44, 44, 52 }; + I32 x = gui_relx( bar ); + I32 y = gui_rely( bar ); + I32 cx = x + 8; + for( I32 i = 0; i < idx; ++i ) + cx += widths[i] + 4; + + *rx = cx; + *ry = y + 2; + *rw = widths[idx]; + *rh = max( 1, bar->h - 4 ); +} + +static void editor_toolbar_dropdown_row_rect( GUI_EDITOR_TOOLBAR* bar, I32 anchor_idx, I32 row, I32* rx, I32* ry, I32* rw, I32* rh ) { + I32 x = 0, y = 0, w = 0, h = 0; + editor_toolbar_item_rect( bar, anchor_idx, &x, &y, &w, &h ); + *rx = x; + *rh = max( 18, h ); + *ry = gui_rely( bar ) + bar->h + row * *rh; + *rw = EDITOR_TOOLBAR_DROPDOWN_W; +} + +static I32 editor_toolbar_hit_test( GUI_EDITOR_TOOLBAR* bar, I32 mx, I32 my ) { + for( I32 i = 0; i < 4; ++i ) { + I32 x = 0, y = 0, w = 0, h = 0; + editor_toolbar_item_rect( bar, i, &x, &y, &w, &h ); + if( editor_toolbar_pt_in_rect( mx, my, x, y, w, h ) ) + return i; + } + + if( bar->file_open ) { + I32 x = 0, y = 0, w = 0, h = 0; + editor_toolbar_dropdown_row_rect( bar, EDITOR_TOOLBAR_HIT_FILE, 0, &x, &y, &w, &h ); + if( editor_toolbar_pt_in_rect( mx, my, x, y, w, h ) ) + return EDITOR_TOOLBAR_HIT_SAVE; + } + + if( bar->edit_open ) { + for( I32 i = 0; i < 2; ++i ) { + I32 x = 0, y = 0, w = 0, h = 0; + editor_toolbar_dropdown_row_rect( bar, EDITOR_TOOLBAR_HIT_EDIT, i, &x, &y, &w, &h ); + if( editor_toolbar_pt_in_rect( mx, my, x, y, w, h ) ) + return i == 0 ? EDITOR_TOOLBAR_HIT_UNDO : EDITOR_TOOLBAR_HIT_REDO; + } + } + + return EDITOR_TOOLBAR_HIT_NONE; +} + +static void gui_editor_toolbar_draw_fn( void* ptr ) { + GUI_EDITOR_TOOLBAR* bar = (GUI_EDITOR_TOOLBAR*)ptr; + I32 x = gui_relx( bar ); + I32 y = gui_rely( bar ); + I32 mx, my; + if( editor_menu_hover_mask_active ) { + mx = editor_menu_hover_real_x; + my = editor_menu_hover_real_y; + } else { + gui_cursor_pos( &mx, &my ); + } + U8 m1 = gui_mbutton_down( GUI_MBTNLEFT ); + I32 hover = editor_toolbar_hit_test( bar, mx, my ); + CLR hover_fill = CLR::blend( ui_clr.border, ui_clr.bg, 0.70f ); + CLR active_fill = CLR::blend( ui_clr.bg_sec, ui_clr.border, 0.22f ); + + CLR border = gui_is_fg_window( bar ) ? ui_clr.border : ui_clr.border_inactive; + gui_draw_frect( x, y, bar->w, bar->h, ui_clr.bg_sec ); + + static const char* labels[4] = { "file", "edit", "view", "tools" }; + for( I32 i = 0; i < 4; ++i ) { + I32 rx = 0, ry = 0, rw = 0, rh = 0; + editor_toolbar_item_rect( bar, i, &rx, &ry, &rw, &rh ); + U8 is_hover = hover == i; + U8 is_active = bar->held && bar->held_item == i && m1; + U8 is_open = ( bar->file_open && i == EDITOR_TOOLBAR_HIT_FILE ) + || ( bar->edit_open && i == EDITOR_TOOLBAR_HIT_EDIT ); + + CLR fill = ui_clr.bg_sec; + if( is_open ) + fill = hover_fill; + else if( is_active ) + fill = active_fill; + else if( is_hover ) + fill = hover_fill; + + if( is_open || is_active || is_hover ) + gui_draw_frect( rx, ry, rw, rh, fill ); + gui_draw_str( rx + 6, y + bar->h / 2 - 8, ALIGN_L, FNT_JPN12, ui_clr.txt, labels[i] ); + } + + if( bar->file_open ) { + I32 rx = 0, ry = 0, rw = 0, rh = 0; + editor_toolbar_dropdown_row_rect( bar, EDITOR_TOOLBAR_HIT_FILE, 0, &rx, &ry, &rw, &rh ); + U8 is_hover = hover == EDITOR_TOOLBAR_HIT_SAVE; + U8 is_active = bar->held && bar->held_item == EDITOR_TOOLBAR_HIT_SAVE && m1; + + gui_draw_frect( rx, ry, rw, rh, border ); + CLR fill = ui_clr.bg_sec; + if( is_active ) + fill = active_fill; + else if( is_hover ) + fill = hover_fill; + + gui_draw_frect( rx + 1, ry + 1, max( 1, rw - 2 ), max( 1, rh - 2 ), fill ); + gui_draw_str( rx + 8, ry + rh / 2 - 8, ALIGN_L, FNT_JPN12, ui_clr.txt, "save" ); + } + + if( bar->edit_open ) { + const U8 enabled[2] = { editor->undo_actions.size > 0, editor->redo_actions.size > 0 }; + const char* labels[2] = { "undo", "redo" }; + const I32 hit_id[2] = { EDITOR_TOOLBAR_HIT_UNDO, EDITOR_TOOLBAR_HIT_REDO }; + + I32 panel_x = 0, panel_y = 0, panel_w = 0, panel_h = 0; + editor_toolbar_dropdown_row_rect( bar, EDITOR_TOOLBAR_HIT_EDIT, 0, &panel_x, &panel_y, &panel_w, &panel_h ); + I32 row_h = panel_h; + gui_draw_frect( panel_x, panel_y, panel_w, row_h * 2, border ); + gui_draw_frect( panel_x + 1, panel_y + 1, max( 1, panel_w - 2 ), max( 1, row_h * 2 - 2 ), ui_clr.bg_sec ); + + for( I32 i = 0; i < 2; ++i ) { + I32 rx = 0, ry = 0, rw = 0, rh = 0; + editor_toolbar_dropdown_row_rect( bar, EDITOR_TOOLBAR_HIT_EDIT, i, &rx, &ry, &rw, &rh ); + U8 is_hover = hover == hit_id[i]; + U8 is_active = bar->held && bar->held_item == hit_id[i] && m1; + + CLR fill = ui_clr.bg_sec; + if( enabled[i] && is_active ) + fill = active_fill; + else if( is_hover ) + fill = hover_fill; + gui_draw_frect( rx + 1, ry + 1, max( 1, rw - 2 ), max( 1, rh - 2 ), fill ); + + CLR txt_clr = enabled[i] ? ui_clr.txt : ui_clr.txt_inactive; + gui_draw_str( rx + 8, ry + rh / 2 - 8, ALIGN_L, FNT_JPN12, txt_clr, labels[i] ); + } + } +} + +static void gui_editor_toolbar_input_fn( void* ptr ) { + GUI_EDITOR_TOOLBAR* bar = (GUI_EDITOR_TOOLBAR*)ptr; + I32 mx, my; + gui_cursor_pos( &mx, &my ); + U8 m1 = gui_mbutton_down( GUI_MBTNLEFT ); + I32 hit = editor_toolbar_hit_test( bar, mx, my ); + + if( m1 ) { + if( !bar->held ) { + bar->held = 1; + bar->held_item = hit; + if( hit == EDITOR_TOOLBAR_HIT_NONE && ( bar->file_open || bar->edit_open ) ) + editor_toolbar_set_open( bar, 0, 0 ); + } + return; + } + + if( bar->held && bar->held_item == hit ) { + switch( hit ) { + case EDITOR_TOOLBAR_HIT_FILE: + editor_toolbar_set_open( bar, !bar->file_open, 0 ); + break; + case EDITOR_TOOLBAR_HIT_EDIT: + editor_toolbar_set_open( bar, 0, !bar->edit_open ); + break; + case EDITOR_TOOLBAR_HIT_SAVE: + editor_toolbar_set_open( bar, 0, 0 ); + editor_save_map( editor ); + break; + case EDITOR_TOOLBAR_HIT_UNDO: + editor_toolbar_set_open( bar, 0, 0 ); + editor_undo( editor ); + break; + case EDITOR_TOOLBAR_HIT_REDO: + editor_toolbar_set_open( bar, 0, 0 ); + editor_redo( editor ); + break; + case EDITOR_TOOLBAR_HIT_VIEW: + case EDITOR_TOOLBAR_HIT_TOOLS: + editor_toolbar_set_open( bar, 0, 0 ); + break; + default: + break; + } + } + + bar->held = 0; + bar->held_item = EDITOR_TOOLBAR_HIT_NONE; +} + +static GUI_EDITOR_TOOLBAR* gui_editor_toolbar( I32 x, I32 y, I32 w, I32 h, const char* name ) { + if( !gui_check_target() ) + return 0; + + GUI_EDITOR_TOOLBAR* bar = new GUI_EDITOR_TOOLBAR; + bar->x = x; + bar->y = y; + bar->w = w; + bar->h = h; + bar->xbound = w; + bar->ybound = h + 52; + bar->draw_fn = gui_editor_toolbar_draw_fn; + bar->input_fn = gui_editor_toolbar_input_fn; + bar->held = 0; + bar->held_item = EDITOR_TOOLBAR_HIT_NONE; + bar->file_open = 0; + bar->edit_open = 0; + strcpy( bar->name, name ); + + GUI_VIEW* parent = gui_get_view(); + bar->parent = parent; + parent->children.push( bar ); + return bar; +} + +static void editor_raise_header_toolbar( GAME_EDITOR* e ) { + if( !e || !e->gui.header_toolbar ) + return; + + GUI_BASE* bar = e->gui.header_toolbar; + GUI_BASE* parent = bar->parent; + if( !parent ) + return; + + I32 idx = parent->children.idx_of( bar ); + if( idx == -1 || idx == (I32)parent->children.size - 1 ) + return; + + parent->children.erase( idx ); + parent->children.push( bar ); +} + struct EDITOR_LAYOUT { I32 left_x; I32 left_w; @@ -260,8 +532,9 @@ static void editor_set_bounds( GUI_BASE* node, I32 x, I32 y, I32 w, I32 h ) { static void editor_create_header_controls( GAME_EDITOR* e ) { GAME_EDITOR::EDITOR_GUI* egui = &e->gui; + egui->header_toolbar = gui_editor_toolbar( 1, EDITOR_LAYOUT_MENU_Y, e->wnd->w - 3, EDITOR_LAYOUT_MENU_H, "editor toolbar" ); + egui->header_back = gui_button( 10, EDITOR_LAYOUT_NAV_Y, 92, 20, "[ back ]", editor_header_back_cb ); - egui->header_save = gui_button( 110, EDITOR_LAYOUT_NAV_Y, 92, 20, "[ save ]", editor_header_save_cb ); egui->header_viewtype_label = gui_label( 250, EDITOR_LAYOUT_NAV_Y + 4, "view type:" ); egui->header_viewtype = gui_editor_viewtype_segment( 320, EDITOR_LAYOUT_NAV_Y, 186, 20, "viewtype segment" ); @@ -333,7 +606,7 @@ const I32 EDITOR_ASSETS_ROW_H = 28; const I32 EDITOR_ASSETS_THUMB = 20; const I32 EDITOR_ASSETS_INNER_PAD_X = 8; const I32 EDITOR_ASSETS_INNER_PAD_Y = 6; -const I32 EDITOR_ASSETS_SCROLLBAR_W = 8; +const I32 EDITOR_ASSETS_SCROLLBAR_W = 7; const I32 EDITOR_ASSETS_SCROLLBAR_GAP = 4; const I32 EDITOR_ASSETS_SCROLL_STEP = 20; @@ -368,7 +641,7 @@ static EDITOR_ASSETS_LAYOUT editor_assets_layout( GUI_EDITOR_INFOBOX* box, I32 p if( l.show_scroll ) l.row_w -= EDITOR_ASSETS_SCROLLBAR_W + EDITOR_ASSETS_SCROLLBAR_GAP; l.row_w = max( 1, l.row_w ); - l.track_x = l.inner_x + l.inner_w - EDITOR_ASSETS_SCROLLBAR_W; + l.track_x = l.inner_x + l.inner_w - EDITOR_ASSETS_SCROLLBAR_W - 1 - ( EDITOR_ASSETS_SCROLLBAR_GAP / 2 ); l.track_y = l.inner_y; l.track_h = l.list_h - 2; return l; @@ -407,11 +680,9 @@ static void gui_editor_infobox_draw_assets( GUI_EDITOR_INFOBOX* box, I32 panel_x CLR rowbg = idx % 2 ? ui_clr.bg : ui_clr.bg_alt; if( selected ) - rowbg = CLR::blend( (CLR){ 0.f, 1.f, 0.f, 1.f }, ui_clr.bg, 0.85f ); + rowbg = CLR::blend( ui_clr.border, ui_clr.bg, 0.70f ); gui_draw_frect( l.row_x, row_y, l.row_w, EDITOR_ASSETS_ROW_H - 2, rowbg ); - if( selected ) - gui_draw_rect( l.row_x, row_y, l.row_w, EDITOR_ASSETS_ROW_H - 2, (CLR){ 0.f, 1.f, 0.f, 1.f } ); I32 tx = l.row_x + 3; I32 ty = row_y + 3; @@ -426,7 +697,7 @@ static void gui_editor_infobox_draw_assets( GUI_EDITOR_INFOBOX* box, I32 panel_x } I32 text_x = tx + EDITOR_ASSETS_THUMB + 8; - CLR txt = selected ? (CLR){ 0.8f, 1.f, 0.8f, 1.f } : ui_clr.txt; + CLR txt = ui_clr.txt; if( map_entry ) { gui_draw_str( text_x, row_y + 7, ALIGN_L, FNT_JPN12, txt, "[map] -> %s", map->name ); } else if( p->tex ) { @@ -445,7 +716,7 @@ static void gui_editor_infobox_draw_assets( GUI_EDITOR_INFOBOX* box, I32 panel_x thumb_h = min( thumb_h, l.track_h ); I32 travel = max( 1, l.track_h - thumb_h ); I32 thumb_y = l.track_y + ( travel * editor->gui.assets_scroll ) / max( 1, l.max_scroll ); - gui_draw_frect( l.track_x + 1, thumb_y + 1, EDITOR_ASSETS_SCROLLBAR_W - 2, max( 1, thumb_h - 2 ), ui_clr.txt ); + gui_draw_frect( l.track_x, thumb_y + 1, EDITOR_ASSETS_SCROLLBAR_W, max( 1, thumb_h - 1 ), ui_clr.txt ); } } @@ -683,16 +954,18 @@ static void editor_layout_map_view( GAME_EDITOR* e ) { return; EDITOR_LAYOUT l = editor_calc_layout( e ); + editor_raise_header_toolbar( e ); GAME_EDITOR::EDITOR_GUI* egui = &e->gui; + GUI_BASE* toolbar = egui->header_toolbar; GUI_BUTTON* back = egui->header_back; - GUI_BUTTON* save = egui->header_save; GUI_BUTTON* v2btn = egui->header_mode_2d; GUI_BUTTON* v3btn = egui->header_mode_3d; GUI_BUTTON* simbtn = egui->header_mode_sim; GUI_LABEL* view_type_lbl = egui->header_viewtype_label; GUI_BASE* view_type_seg = egui->header_viewtype; + const I32 menu_btn_y = EDITOR_LAYOUT_MENU_Y; const I32 top_btn_y = l.top_y; const I32 top_btn_h = 20; const I32 nav_left_x = 10; @@ -707,8 +980,9 @@ static void editor_layout_map_view( GAME_EDITOR* e ) { I32 max_type_w = mode_x - type_seg_x - 8; I32 type_seg_w = min( 174, max( 120, max_type_w ) ); + editor_set_bounds( toolbar, 1, menu_btn_y, e->wnd->w - 3, EDITOR_LAYOUT_MENU_H ); + editor_set_bounds( back, nav_left_x, top_btn_y, nav_btn_w, top_btn_h ); - editor_set_bounds( save, nav_left_x + nav_btn_w + nav_gap, top_btn_y, nav_btn_w, top_btn_h ); GUI_BUTTON* mode_btns[] = { v2btn, v3btn, simbtn }; const char* mode_names[] = { "[ 2d view ]", "[ 3d view ]", "[ simulation ]" }; @@ -796,6 +1070,21 @@ static void editor_layout_map_view( GAME_EDITOR* e ) { void gui_editorwindow_draw_fn( void* ptr ) { GUI_EDITORWINDOW* wnd = (GUI_EDITORWINDOW*)ptr; + editor_raise_header_toolbar( editor ); + GUI_BASE* toolbar = editor ? editor->gui.header_toolbar : 0; + GUI_EDITOR_TOOLBAR* tbar = (GUI_EDITOR_TOOLBAR*)toolbar; + U8 menu_open = tbar && ( tbar->file_open || tbar->edit_open ); + F32 saved_mx = input.mouse.pos.x; + F32 saved_my = input.mouse.pos.y; + if( menu_open ) { + editor_menu_hover_mask_active = 1; + editor_menu_hover_real_x = (I32)saved_mx; + editor_menu_hover_real_y = (I32)saved_my; + input.mouse.pos.x = -100000.f; + input.mouse.pos.y = -100000.f; + } else { + editor_menu_hover_mask_active = 0; + } CLR clr = gui_is_fg_window( wnd )? ui_clr.border : ui_clr.border_inactive; gui_draw_frect( wnd->x, wnd->y, wnd->w, wnd->h, clr ); @@ -808,6 +1097,32 @@ void gui_editorwindow_draw_fn( void* ptr ) { if( it->draw_fn ) it->draw_fn( it ); else dlog( "gui_editorwindow_draw_fn(): child %p has no draw_fn", it ); } ); + + input.mouse.pos.x = saved_mx; + input.mouse.pos.y = saved_my; + editor_menu_hover_mask_active = 0; +} + +static void gui_editorwindow_input_fn( void* ptr ) { + GUI_EDITORWINDOW* wnd = (GUI_EDITORWINDOW*)ptr; + editor_raise_header_toolbar( editor ); + + GUI_BASE* toolbar = editor ? editor->gui.header_toolbar : 0; + if( toolbar && toolbar->enabled && toolbar->input_fn ) + toolbar->input_fn( toolbar ); + + GUI_EDITOR_TOOLBAR* tbar = (GUI_EDITOR_TOOLBAR*)toolbar; + U8 menu_open = tbar && ( tbar->file_open || tbar->edit_open ); + if( menu_open ) + return; + + wnd->children.each( fn( GUI_BASE** childptr ) { + GUI_BASE* child = *childptr; + if( !child || child == toolbar || !child->enabled || !child->input_fn ) + return; + + child->input_fn( child ); + } ); } GUI_EDITORWINDOW* gui_editorwindow_create( I32 w, I32 h ) { @@ -818,6 +1133,7 @@ GUI_EDITORWINDOW* gui_editorwindow_create( I32 w, I32 h ) { wnd->ybound = wnd->h = h; wnd->locked = 1; wnd->draw_fn = gui_editorwindow_draw_fn; + wnd->input_fn = gui_editorwindow_input_fn; strcpy( wnd->name, "EDITORWINDOW" ); _gui.windows.push( wnd ); @@ -933,9 +1249,9 @@ void editor_create_map_view( GAME_EDITOR* e ) { e->gui.assets = 0; e->gui.status = 0; e->gui.assets_scroll = 0; + e->gui.header_toolbar = 0; e->gui.header_viewtype_label = 0; e->gui.header_back = 0; - e->gui.header_save = 0; e->gui.header_mode_2d = 0; e->gui.header_mode_3d = 0; e->gui.header_mode_sim = 0; diff --git a/src/editor/view2d.cpp b/src/editor/view2d.cpp index 6814fd2..b68846e 100644 --- a/src/editor/view2d.cpp +++ b/src/editor/view2d.cpp @@ -433,7 +433,9 @@ void gui_editor_2dview_create_poly_from_drag( GUI_EDITOR_2DVIEW* view ) { } map_polygon_calc_bounds( &newp ); + I32 poly_idx = editor->map->polygons.size; editor->map->polygons.push( newp ); + editor_undo_record_create_poly( editor, poly_idx ); gui_editor_2dview_check_bounds_preserve_view( view ); } @@ -443,6 +445,7 @@ void gui_editor_2dview_create_wallpoly_from_drag( GUI_EDITOR_2DVIEW* view ) { return; I32 propid = gui_editor_2dview_find_or_create_wallpoly_propid(); + I32 first_wall_idx = editor->map->walls.size; F32 depth = gui_editor_2dview_placement_z(); F32 wallheight = gui_editor_2dview_wall_height(); @@ -459,6 +462,8 @@ void gui_editor_2dview_create_wallpoly_from_drag( GUI_EDITOR_2DVIEW* view ) { wall.propid = propid; editor->map->walls.push( wall ); } + I32 new_wall_count = editor->map->walls.size - first_wall_idx; + editor_undo_record_create_walls( editor, first_wall_idx, new_wall_count ); gui_editor_2dview_check_bounds_preserve_view( view ); } @@ -1141,8 +1146,11 @@ void gui_editor_2dview_input_tool_wall( GUI_EDITOR_2DVIEW* view ) { U8 m1 = gui_mbutton_down( 0 ); if( !m1 ) { if( view->held ) { + if( view->pending_wall_undo_idx >= 0 ) + editor_undo_record_create_walls( editor, view->pending_wall_undo_idx, 1 ); gui_editor_2dview_check_bounds_preserve_view( view ); } + view->pending_wall_undo_idx = -1; view->held = 0; view->curdrag = 0; return; @@ -1168,6 +1176,7 @@ void gui_editor_2dview_input_tool_wall( GUI_EDITOR_2DVIEW* view ) { I32 idx = editor->map->walls.size; editor->map->walls.push( neww ); + view->pending_wall_undo_idx = idx; view->curdrag = &editor->map->walls[idx].end; view->held = 1; @@ -1338,12 +1347,31 @@ void gui_editor_view2d_delete_obj( GUI_EDITOR_2DVIEW* view ) { void gui_editor_2dview_key_input( GUI_EDITOR_2DVIEW* view ) { static U8 del_held = 0; + static U8 undo_held = 0; + static U8 redo_held = 0; if( kb_down( SDLK_DELETE ) && !input.mouselock ) { if( !del_held ) gui_editor_view2d_delete_obj( view ); del_held = 1; } else del_held = 0; + + U8 ctrl = kb_down( SDLK_LCTRL ) || kb_down( SDLK_RCTRL ); + U8 z = kb_down( SDLK_z ); + if( ctrl && z && !input.mouselock ) { + if( !undo_held ) + editor_undo( editor ); + + undo_held = 1; + } else undo_held = 0; + + U8 r = kb_down( SDLK_r ); + if( ctrl && r && !input.mouselock ) { + if( !redo_held ) + editor_redo( editor ); + + redo_held = 1; + } else redo_held = 0; } void gui_editor_2dview_input_fn( void* ptr ) { @@ -1469,6 +1497,7 @@ GUI_EDITOR_2DVIEW* gui_editor_2dview( I32 x, I32 y, I32 w, I32 h ) { view->poly_drag = 0; view->poly_start = { 0.f, 0.f }; view->poly_end = { 0.f, 0.f }; + view->pending_wall_undo_idx = -1; GUI_BASE* parent = gui_get_view(); if( !parent ) diff --git a/src/gui/button.cpp b/src/gui/button.cpp index 1548e0e..e66130b 100644 --- a/src/gui/button.cpp +++ b/src/gui/button.cpp @@ -12,11 +12,12 @@ void gui_button_draw_fn( void* ptr ) { U8 hover = inbounds && !active; CLR border = gui_is_fg_window( btn )? ui_clr.border : ui_clr.border_inactive; - if( active ) - border = { 0.f, 1.f, 0.f, 1.f }; CLR fill = ui_clr.bg_sec; CLR txt = ui_clr.txt; + if( active ) { + fill = CLR::blend( ui_clr.bg_sec, ui_clr.border, 0.22f ); + } if( hover ) { fill = ui_clr.border; txt = CLR::BLACK(); diff --git a/src/gui/list.cpp b/src/gui/list.cpp index 90f2f49..2feff47 100644 --- a/src/gui/list.cpp +++ b/src/gui/list.cpp @@ -10,18 +10,33 @@ void gui_list_draw_fn( void* ptr ) { I32 y = gui_rely( list ); gui_draw_str( x, y, ALIGN_L, FNT_JPN12, ui_clr.txt, list->name ); - y += LIST_TITLE_OFFSET; + I32 panel_y = y + LIST_TITLE_OFFSET; CLR col = gui_is_fg_window( list )? ui_clr.border : ui_clr.border_inactive; - gui_draw_frect( x, y, list->w, list->h, col ); - gui_draw_frect( x+1, y+1, list->w-2, list->h-2, ui_clr.bg_sec ); + gui_draw_frect( x, panel_y, list->w, list->h, col ); + gui_draw_frect( x + 1, panel_y + 1, list->w - 2, list->h - 2, ui_clr.bg_sec ); + + I32 txt_h = 12; + gui_draw_get_str_bounds( 0, &txt_h, FNT_JPN12, "ag" ); I32 yoff = 0; list->plist->each( fn( GUI_LIST_ENTRY* e ) { U8 selected = e->val == *list->pval; - CLR col = selected? ui_clr.txt : ui_clr.txt_inactive; + I32 row_x = x + 1; + I32 row_y = panel_y + 1 + yoff; + I32 row_w = list->w - 2; + if( row_w < 1 ) row_w = 1; + I32 row_h = LIST_ITEM_HEIGHT; + + if( selected ) { + CLR sel_bg = CLR::blend( ui_clr.border, ui_clr.bg, 0.70f ); + gui_draw_frect( row_x, row_y, row_w, row_h, sel_bg ); + } + + CLR col = selected ? ui_clr.txt : ui_clr.txt_inactive; + I32 txt_y = row_y + ( row_h - txt_h ) / 2; - gui_draw_str( x + 4, y + yoff + 6, ALIGN_L, FNT_JPN12, col, e->title ); + gui_draw_str( row_x + 3, txt_y, ALIGN_L, FNT_JPN12, col, e->title ); yoff += LIST_ITEM_HEIGHT; } ); } @@ -31,6 +46,7 @@ void gui_list_input_fn( void* ptr ) { I32 x = gui_relx( list ); I32 y = gui_rely( list ); + I32 panel_y = y + LIST_TITLE_OFFSET; I32 w = list->w; I32 h = list->h; @@ -38,12 +54,12 @@ void gui_list_input_fn( void* ptr ) { I32 mx, my; gui_cursor_pos( &mx, &my ); - U8 inbounds = mx >= x && mx <= x + w && my >= y && my <= y + h; + U8 inbounds = mx >= x && mx <= x + w && my >= panel_y && my <= panel_y + h; if( !inbounds ) return; - I32 diff = my - y; - I32 idx = diff / LIST_ITEM_HEIGHT - 1; + I32 diff = my - panel_y; + I32 idx = diff / LIST_ITEM_HEIGHT; if( idx >= list->plist->size ) idx = list->plist->size - 1; if( idx < 0 ) idx = 0; -- cgit v1.2.3