From 413ef78504278d37080b9b59a652e4bbd5e2a0fc Mon Sep 17 00:00:00 2001 From: kasull Date: Sun, 1 Mar 2026 20:18:08 -0500 Subject: editor ui rework add responsive editor relayout for window resize rework header with back/save, view mode buttons, and 2d view type selector add clipped, scrollable assets and contextual tool panels simplify tool button wiring and repeated layout logic simplify 2d top-down editor internals and shared grid update hooks --- src/editor/editor.cpp | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) (limited to 'src/editor/editor.cpp') diff --git a/src/editor/editor.cpp b/src/editor/editor.cpp index ad23d1e..3c67ecd 100644 --- a/src/editor/editor.cpp +++ b/src/editor/editor.cpp @@ -4,6 +4,36 @@ GAME_EDITOR* editor = 0; +void editor_register_grid_dependency( GAME_EDITOR* e, void* tag, EDITOR_GRID_DEP_CALLBACK cb ) { + if( !e || !tag || !cb ) + return; + + I32 idx = e->grid_dependencies.idx_where( fn( GAME_EDITOR::GRID_DEPENDENCY* dep ) { + return dep->tag == tag; + } ); + + if( idx != -1 ) { + e->grid_dependencies[idx].cb = cb; + return; + } + + GAME_EDITOR::GRID_DEPENDENCY dep; + dep.tag = tag; + dep.cb = cb; + e->grid_dependencies.push( dep ); +} + +void editor_notify_grid_change( GAME_EDITOR* e ) { + if( !e ) + return; + + e->grid_dependencies.each( fn( GAME_EDITOR::GRID_DEPENDENCY* dep ) { + if( dep->cb ) { + dep->cb( e ); + } + } ); +} + GAME_EDITOR* editor_create( GAME_DATA* game ) { if( editor ) { dlog( "editor_create() : attempted to create editor when one already exists\n" ); -- cgit v1.2.3 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 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 343 insertions(+) (limited to 'src/editor/editor.cpp') 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 ); } ); -- cgit v1.2.3 From be91342733fd56d1e7bafe72e82a8ac4dc67b79d Mon Sep 17 00:00:00 2001 From: kasull Date: Tue, 3 Mar 2026 18:49:43 -0500 Subject: fix use after free --- src/editor/editor.cpp | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) (limited to 'src/editor/editor.cpp') diff --git a/src/editor/editor.cpp b/src/editor/editor.cpp index 63360d1..3873913 100644 --- a/src/editor/editor.cpp +++ b/src/editor/editor.cpp @@ -4,6 +4,31 @@ GAME_EDITOR* editor = 0; +void editor_clear_gui_state_refs( GAME_EDITOR* e ) { + if( !e ) + return; + + e->gui.new_map_popup = 0; + + e->gui.v2d = 0; + e->gui.v3d = 0; + e->gui.gridlabel = 0; + e->gui.props = 0; + e->gui.tool = 0; + + 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_mode_2d = 0; + e->gui.header_mode_3d = 0; + e->gui.header_mode_sim = 0; + e->gui.header_viewtype = 0; +} + static void editor_push_undo_action( GAME_EDITOR* e, const GAME_EDITOR::EDITOR_UNDO_ACTION& action ) { if( !e ) return; @@ -410,6 +435,7 @@ STAT editor_close( GAME_EDITOR* e ) { I32 w = e->wnd->w, h = e->wnd->h; gui_free( e->wnd ); + editor_clear_gui_state_refs( e ); e->wnd = gui_editorwindow( w, h ); } ); -- cgit v1.2.3