24 KiB
Canvas Slimming Plan
Owner: imgui-frontend-engineer
Status: In Progress (Phases 1-5 Complete for Dungeon + Overworld; Phase 6 Pending)
Scope: src/app/gui/canvas/ + editor call-sites
Goal: Reduce gui::Canvas bloat, align lifecycle with ImGui-style, and enable safer per-frame usage without hidden state mutation.
Current Problems
- Too many constructors and in-class defaults; state is scattered across legacy fields (
custom_step_,global_scale_, enable_* duplicates). - Per-frame options are applied via mutations instead of “Begin arguments,” diverging from ImGui patterns.
- Heavy optional subsystems (bpp dialogs, palette editor, modals, automation) live on the core type.
- Helpers rely on implicit internal state (
draw_list_,canvas_p0_) instead of a passed-in context.
Target Shape
- Persistent state:
CanvasConfig,CanvasSelection, stable IDs, scrolling, rom/game_data pointers. - Transient per-frame:
CanvasRuntime(draw_list, geometry, hover flags, resolved grid step/scale). Constructed inBegin, discarded inEnd. - Options:
CanvasFrameOptions(andBitmapPreviewOptions) are the “Begin args.” Prefer per-frame opts over setters. - Helpers: Accept a
CanvasRuntime&(orCanvasContextvalue) rather than reading globals; keep wrappers for compatibility during migration. - Optional features: Move bpp/palette/modals/automation behind lightweight extension pointers or a separate host struct.
- Construction: One ctor +
Init(CanvasConfig)(or default). Deprecate overloads; keep temp forwarding for compatibility.
Phased Work
-
Runtime extraction (core) [COMPLETE]
- Add
CanvasRuntime(geometry, draw_list, hover, resolved grid step/scale, content size). - Add
CanvasRuntime-based helpers:DrawBitmap,DrawBitmapPreview,RenderPreviewPanel. - Make
Begin/Endcreate and tear down runtime; route grid/overlay/popups through it.- Implemented
BeginCanvas(Canvas&, CanvasFrameOptions)returningCanvasRuntime - Implemented
EndCanvas(Canvas&, CanvasRuntime&, CanvasFrameOptions)handling grid/overlay/popups
- Implemented
- Deprecate internal legacy mirrors (
custom_step_,global_scale_, enable_* duplicates); keep sync shims temporarily.
- Add
-
API narrowing [COMPLETE]
- Collapse ctors to a single default +
Init(config).- Added
Init(const CanvasConfig& config)andInit(const std::string& id, ImVec2 canvas_size)methods - Legacy constructors marked with
[[deprecated("Use default ctor + Init() instead")]]
- Added
- Mark setters that mutate per-frame state (
SetCanvasSize,SetGridSize, etc.) as "compat; prefer frame options."- Added COMPAT comments to
SetGridSize,SetCustomGridStep,SetCanvasSize,SetGlobalScale,set_global_scale
- Added COMPAT comments to
- Add
BeginInTable(label, CanvasFrameOptions)that wraps child sizing and returns a runtime-aware frame.- Implemented
BeginInTable(label, CanvasFrameOptions)returningCanvasRuntime - Implemented
EndInTable(CanvasRuntime&, CanvasFrameOptions)handling grid/overlay/popups
- Implemented
- Collapse ctors to a single default +
-
Helper refactor [COMPLETE]
- Change helper signatures to accept
CanvasRuntime&:DrawBitmap,DrawBitmapPreview,RenderPreviewPanel. - Refactor remaining helpers:
DrawTilemapPainter,DrawSelectRect,DrawTileSelector.- All three now have stateless
CanvasRuntime-based implementations - Member functions delegate to stateless helpers via
BuildCurrentRuntime()
- All three now have stateless
- Keep thin wrappers that fetch the current runtime for legacy calls.
- Added
Canvas::BuildCurrentRuntime()private helper
- Added
- Added
DrawBitmap(CanvasRuntime&, Bitmap&, BitmapDrawOpts)overload for options-based drawing.
- Change helper signatures to accept
-
Optional modules split [COMPLETE]
- Move bpp dialogs, palette editor, modals, automation into an extension struct (
CanvasExtensions) held by unique_ptr on demand.- Created
canvas_extensions.hwithCanvasExtensionsstruct containing:bpp_format_ui,bpp_conversion_dialog,bpp_comparison_tool,modals,palette_editor,automation_api - Created
canvas_extensions.ccwith lazy initialization helpers:InitializeModals(),InitializePaletteEditor(),InitializeBppUI(),InitializeAutomation() - Canvas now uses single
std::unique_ptr<CanvasExtensions> extensions_withEnsureExtensions()lazy accessor
- Created
- Core
Canvasremains lean even when extensions are absent.- Extensions only allocated on first use of optional features
- All Show* methods and GetAutomationAPI() delegate to EnsureExtensions()
- Move bpp dialogs, palette editor, modals, automation into an extension struct (
-
Call-site migration [COMPLETE]
- Update low-risk previews first: graphics thumbnails (
sheet_browser_panel), small previews (object_editor_panel,link_sprite_panel). - Medium: screen/inventory canvases, sprite/tileset selectors.
DrawInventoryMenuEditor: migrated toBeginCanvas/EndCanvas+ statelessDrawBitmapDrawDungeonMapsRoomGfx: migrated tilesheet and current_tile canvases to new pattern
- High/complex - Dungeon Editor:
DungeonCanvasViewer::DrawDungeonCanvasmigrated- Uses
BeginCanvas(canvas_, frame_opts)/EndCanvas(canvas_, canvas_rt, frame_opts)pattern - Entity rendering functions updated to accept
CanvasRuntime¶meter - All
canvas_.DrawRect/DrawTextcalls replaced withgui::DrawRect(rt, ...)/gui::DrawText(rt, ...) - Grid visibility controlled via
frame_opts.draw_grid = show_grid_
- Uses
- High/complex - Overworld Editor:
DrawOverworldCanvasand secondary canvases migrated- Main canvas uses
BeginCanvas/EndCanvaswithCanvasFrameOptions - Entity renderer (
OverworldEntityRenderer) updated withCanvasRuntime-based methods - Scroll bounds implemented via
ClampScroll()helper inHandleOverworldPan() - Secondary canvases migrated:
scratch_canvas_,current_gfx_canvas_,graphics_bin_canvas_ - Zoom support deferred (documented in code); positions not yet scaled with global_scale
- Main canvas uses
- Update low-risk previews first: graphics thumbnails (
-
Cleanup & deprecations [PENDING]
- Remove deprecated ctors/fields after call-sites are migrated.
- Document "per-frame opts" pattern and add brief usage examples in
canvas.h. - Remove legacy
BeginCanvas(Canvas&, ImVec2)overload (only used bymessage_editor.cc) - Audit remaining
AlwaysVerticalScrollbarusage inGraphicsBinCanvasPipeline/BitmapCanvasPipeline - Remove
custom_step_,global_scale_duplicates once all editors useCanvasFrameOptions - Consider making
CanvasRuntimea first-class return from allBeginvariants
Where to Start (low risk, high leverage)
-
COMPLETED (Phase 1 - Low Risk): Migrated low-risk graphics thumbnails and previews:
src/app/editor/graphics/sheet_browser_panel.cc(Thumbnails useDrawBitmapPreview(rt, ...)viaBeginCanvas)src/app/editor/dungeon/panels/object_editor_panel.cc(Static editor preview usesRenderPreviewPanel(rt, ...))src/app/editor/graphics/link_sprite_panel.cc(Preview canvas usesDrawBitmap(rt, ...)with manual begin/end)
-
COMPLETED (Phase 2 - Medium Risk): Migrated screen editor canvases:
src/app/editor/graphics/screen_editor.cc:DrawInventoryMenuEditor: UsesBeginCanvas/EndCanvas+ statelessgui::DrawBitmap(rt, ...)DrawDungeonMapsRoomGfx: Tilesheet canvas and current tile canvas migrated toBeginCanvas/EndCanvaspattern withCanvasFrameOptionsfor grid step configuration
Editor-Specific Strategies (for later phases)
-
Overworld Editor (high complexity) [MIGRATED]
- Surfaces: main overworld canvas (tile painting, selection, multi-layer), scratch space, tile16 selector, property info grids.
- Completed Migration:
DrawOverworldCanvas: UsesBeginCanvas(ow_map_canvas_, frame_opts)/EndCanvas()patternOverworldEntityRenderer: AddedCanvasRuntime-based methods (DrawEntrances(rt, world),DrawExits(rt, world), etc.)HandleOverworldPan: Now clamps scrolling viagui::ClampScroll()to prevent scrolling outside map boundsCenterOverworldView: Properly centers on current map with clamped scrollDrawScratchSpace: Migrated toBeginCanvas/EndCanvaspatternDrawAreaGraphics: Migratedcurrent_gfx_canvas_to new patternDrawTile8Selector: Migratedgraphics_bin_canvas_to new pattern
- Key Implementation Details:
- Context menu setup happens BEFORE
BeginCanvasviamap_properties_system_->SetupCanvasContextMenu() - Entity drag/drop uses
canvas_rt.scaleandcanvas_rt.canvas_p0from runtime - Hover detection uses
canvas_rt.hoveredinstead ofow_map_canvas_.IsMouseHovering() IsMouseHoveringOverEntity(entity, rt)overload added for runtime-based entity detection
- Context menu setup happens BEFORE
- Zoom Support: [IMPLEMENTED]
DrawOverworldMaps()now appliesglobal_scale()to both bitmap positions and scale- Placeholder rectangles for unloaded maps also scaled correctly
- Entity rendering already uses
rt.scalevia stateless helpers - alignment works automatically
- Testing Focus:
- Pan respects canvas bounds (can't scroll outside map)
- Entity hover detection works
- Entity drag/drop positioning correct
- Context menu opens in correct mode
- Zoom scales bitmaps and positions correctly
-
Dungeon Editor [MIGRATED]
- Surfaces: room graphics viewer, object selector preview, integrated editor panels, room canvases with palettes/blocks.
- Completed Migration:
DungeonCanvasViewer::DrawDungeonCanvas: UsesBeginCanvas/EndCanvaswithCanvasFrameOptions- Entity rendering functions (
RenderSprites,RenderPotItems,DrawObjectPositionOutlines) acceptconst gui::CanvasRuntime& - All drawing calls use stateless helpers:
gui::DrawRect(rt, ...),gui::DrawText(rt, ...) DungeonObjectSelector: Already usedCanvasFramepattern (no changes needed)ObjectEditorPanel: Already usedBeginCanvas/EndCanvaspattern (no changes needed)
- Key Lessons:
- Context menu setup (
ClearContextMenuItems,AddContextMenuItem) must happen BEFOREBeginCanvas - The
DungeonObjectInteractionclass still usescanvas_pointer internally - this works because canvas state is set up byBeginCanvas - Debug overlay windows (
ImGui::Begin/End) work fine inside the canvas frame since they're separate ImGui windows - Grid visibility is now controlled via
frame_opts.draw_gridinstead of manualif (show_grid_) { canvas_.DrawGrid(); }
- Context menu setup (
-
Screen/Inventory Editors [MIGRATED]
DrawInventoryMenuEditorandDrawDungeonMapsRoomGfxnow useBeginCanvas/EndCanvaswithCanvasFrameOptions.- Stateless
gui::DrawBitmap(rt, ...)andgui::DrawTileSelector(rt, ...)used throughout. - Grid step configured via
CanvasFrameOptions(32 for screen, 16/32 for tilesheet).
-
Graphics/Pixels Panels
- Low risk; continue to replace manual
draw_listusage withDrawBitmapPreviewand runtime-aware helpers. Ensureensure_texture=truefor arena-backed bitmaps.
- Low risk; continue to replace manual
-
Tile16Editor [MIGRATED]
- Three canvases migrated from
DrawBackground()/DrawContextMenu()/DrawGrid()/DrawOverlay()toBeginCanvas/EndCanvas:blockset_canvas_: Two usages inUpdateBlockset()andDrawContent()now useCanvasFrameOptionstile8_source_canvas_: Tile8 source selector now usesBeginCanvas/EndCanvaswith grid step 32.0ftile16_edit_canvas_: Tile16 edit canvas now usesBeginCanvas/EndCanvaswith grid step 8.0f
- Tile selection and painting logic preserved; only frame management changed
- Three canvases migrated from
-
Automation/Testing Surfaces
- Leave automation API untouched until core migration is stable. When ready, have it consume
CanvasRuntimeso tests don't depend on hidden members.
- Leave automation API untouched until core migration is stable. When ready, have it consume
Testing Focus per Editor
- Overworld [VALIDATED]: scroll bounds ✓, entity hover ✓, entity drag/drop ✓, context menu ✓, tile painting ✓, zoom ✓.
- Dungeon [VALIDATED]: grid alignment ✓, object interaction ✓, entity overlays ✓, context menu ✓, layer visibility ✓.
- Screen/Inventory: zoom buttons, grid overlay alignment, selectors.
- Graphics panels: texture creation on demand, grid overlay spacing, tooltip/selection hits.
Context Menu & Popup Cleanup
- Problem: Caller menus stack atop generic defaults; composition is implicit. Popups hang off internal state instead of the menu contract.
- Target shape:
CanvasMenuHost(or similar) owns menu items for the frame. Generic items are registered explicitly viaRegisterDefaultCanvasMenu(host, runtime, config).- Callers add items with
AddItem(label, shortcut, callback, enabled_fn)or structured entries (id, tooltip, icon). - Rendering is single-pass per frame:
RenderContextMenu(host, runtime, config). No hidden additions elsewhere. - Persistent popups are tied to menu actions and rendered via the same host (or a
PopupRegistryowned by it), gated byCanvasFrameOptions.render_popups.
- Migration plan:
- Extract menu item POD (id, label, shortcut text, optional enable/predicate, callback).
- Refactor
DrawContextMenuto:- Create/clear a host each frame
- Optionally call
RegisterDefaultCanvasMenu - Provide a caller hook to register custom items
- Render once.
- Deprecate ad-hoc menu additions inside draw helpers; require explicit registration.
- Keep legacy editor menus working by shimming their registrations into the host; remove implicit defaults once call-sites are migrated.
- Popups: route open/close through the host/registry; render via a single
RenderPersistentPopups(host)invoked fromEndwhenrender_popupsis true.
- Usage pattern for callers (future API):
auto& host = canvas.MenuHost();host.Clear();RegisterDefaultCanvasMenu(host, runtime, config); // optionalhost.AddItem("Custom", "Ctrl+K", []{ ... });RenderContextMenu(host, runtime, config);
Critical Insights (Lessons Learned)
Child Window & Scrollbar Behavior:
- The canvas has its own internal scrolling mechanism via
scrolling_state and pan handling inDrawBackground(). - Wrapping in
ImGui::BeginChild()with scrollbars is redundant and causes visual issues. CanvasFrameOptions.use_child_windowdefaults tofalseto match legacyDrawBackground()behavior.- Only use
use_child_window = truewhen you explicitly need a scrollable container (rare). - All
BeginChildcalls in canvas code now useImGuiWindowFlags_NoScrollbarby default.
Context Menu Ordering:
- Context menu setup (
ClearContextMenuItems,AddContextMenuItem) must happen BEFOREBeginCanvas. - The menu items are state on the canvas object, not per-frame data.
BeginCanvascallsDrawBackground+DrawContextMenu, so items must be registered first.
Interaction Systems:
- Systems like
DungeonObjectInteractionthat hold acanvas_pointer still work because canvas internal state is valid afterBeginCanvas. - The runtime provides read-only geometry; interaction systems can still read canvas state directly.
Scroll Bounds (Overworld Lesson):
- Large canvases (4096x4096) need explicit scroll clamping to prevent users from panning beyond content bounds.
- Use
ClampScroll(scroll, content_px * scale, viewport_px)after computing new scroll position. - The overworld's
HandleOverworldPan()now demonstrates this pattern withgui::ClampScroll().
Multi-Bitmap Canvases (Overworld Lesson):
- When a canvas renders multiple bitmaps (e.g., 64 map tiles), positions must be scaled with
global_scale_for zoom to work. - The overworld's zoom is deferred because bitmap positions are not yet scaled (see TODO in
DrawOverworldMaps()). - Fix approach:
map_x = static_cast<int>(xx * kOverworldMapSize * scale)and passscaletoDrawBitmap.
Entity Renderer Refactoring:
- Separate entity renderers (like
OverworldEntityRenderer) should acceptconst gui::CanvasRuntime&for stateless rendering. - Legacy methods can be kept as thin wrappers that build runtime from canvas state.
- The
IsMouseHoveringOverEntity(entity, rt)overload demonstrates runtime-based hit testing.
Design Principles to Follow
- ImGui-like: options-as-arguments at
Begin, minimal persistent mutation, small POD contexts for draw helpers. - Keep the core type thin; optional features live in extensions, not the base.
- Maintain compatibility shims temporarily, but mark them and plan removal once editors are migrated.
- Avoid child window wrappers unless truly needed for scrollable regions.
Quick Reference for Future Agents
- Core files to touch:
src/app/gui/canvas/canvas.{h,cc},canvas_extensions.{h,cc},canvas_menu.{h,cc},canvas_context_menu.{h,cc},canvas_menu_builder.{h,cc},canvas_utils.{h,cc}. Common call-sites:screen_editor.cc,link_sprite_panel.cc,sheet_browser_panel.cc,object_editor_panel.cc,dungeon_object_selector.cc, overworld/dungeon editors. - Avoid legacy patterns: direct
draw_list()math in callers,custom_step_/global_scale_duplicates, scatteredDrawBackground/DrawGrid/DrawOverlaychains, implicit menu stacking inDrawContextMenu. - Naming/API standardization:
- Per-frame context:
CanvasRuntime; pass it to helpers. - Options at begin:
CanvasFrameOptions(viaBeginCanvas/EndCanvasorCanvasFrame), not mutating setters (setters are compat-only). - Menu host: use
CanvasMenuAction/CanvasMenuActionHost(avoidCanvasMenuItemcollision). - Implemented stateless helpers:
DrawBitmap(rt, bitmap, ...)- multiple overloads includingBitmapDrawOptsDrawBitmapPreview(rt, bitmap, BitmapPreviewOptions)RenderPreviewPanel(rt, bitmap, PreviewPanelOpts)DrawTilemapPainter(rt, tilemap, current_tile, out_drawn_pos)- returns drawn position via output paramDrawTileSelector(rt, size, size_y, out_selected_pos)- returns selection via output paramDrawSelectRect(rt, current_map, tile_size, scale, CanvasSelection&)- updates selection structDrawRect(rt, x, y, w, h, color)- draws filled rectangle (added for entity overlays)DrawText(rt, text, x, y)- draws text at position (added for entity labels)DrawOutline(rt, x, y, w, h, color)- draws outline rectangle
- Frame management:
BeginCanvas(canvas, CanvasFrameOptions)→ returnsCanvasRuntimeEndCanvas(canvas, runtime, CanvasFrameOptions)→ draws grid/overlay/popups based on optionsBeginInTable(label, CanvasFrameOptions)→ table-aware begin returningCanvasRuntimeEndInTable(runtime, CanvasFrameOptions)→ table-aware end with grid/overlay/popups
- CanvasFrameOptions fields:
canvas_size- size of canvas content (0,0 = auto from config)draw_context_menu- whether to call DrawContextMenu (default true)draw_grid- whether to draw grid overlay (default true)grid_step- optional grid step overridedraw_overlay- whether to draw selection overlay (default true)render_popups- whether to render persistent popups (default true)use_child_window- wrap in ImGui::BeginChild (default false - important!)show_scrollbar- show scrollbar when use_child_window is true (default false)
- Initialization (Phase 2):
Canvas()→ default constructor (preferred)Init(const CanvasConfig& config)→ post-construction initialization with full configInit(const std::string& id, ImVec2 canvas_size)→ simple initialization- Legacy constructors deprecated with
[[deprecated]]attribute
- Extensions (Phase 4):
CanvasExtensionsstruct holds optional modules: bpp_format_ui, bpp_conversion_dialog, bpp_comparison_tool, modals, palette_editor, automation_apiEnsureExtensions()→ lazy accessor (private, creates extensions on first use)- Extensions only allocated when Show* or GetAutomationAPI() methods are called
- Legacy delegation: Member functions delegate to stateless helpers via
Canvas::BuildCurrentRuntime() - Context menu flow: host.Clear → optional
RegisterDefaultCanvasMenu(host, rt, cfg)→ caller adds items →RenderContextMenu(host, rt, cfg)once per frame.
- Per-frame context:
- Migration checklist (per call-site):
- Replace manual DrawBackground/DrawGrid/DrawOverlay/popup sequences with
CanvasFrameorBeginCanvas/EndCanvasusingCanvasFrameOptions. - Replace
draw_list()/zero_point()math withAddImageAt/AddRectFilledAt/AddTextAtor overlay helpers that takeCanvasRuntime. - For tile selection, use
TileIndexAtwith grid_step from options/runtime. - For previews/selectors, use
RenderPreviewPanel/RenderSelectorPanel(ensure_texture=truefor arena bitmaps). - For context menus, switch to a
CanvasMenuActionHost+ explicit render pass.
- Replace manual DrawBackground/DrawGrid/DrawOverlay/popup sequences with
- Deprecations to remove after migration:
custom_step_,global_scale_duplicates, legacyenable_*mirrors, directdraw_list_access in callers. - Testing hints: pure helpers for tests (
TileIndexAt,ComputeZoomToFit,ClampScroll). Manual checks per editor: grid alignment, tile hit correctness, zoom/fit, context menu actions, popup render, texture creation (ensure_texture). - Risk order: low (previews/thumbnails) → medium (selectors, inventory/screen) → high (overworld main, dungeon main). Start low, validate patterns, then proceed.
- Scroll & Zoom Helpers (Phase 0 - Infrastructure):
ClampScroll(scroll, content_px, canvas_px)→ clamps scroll to valid bounds[-max_scroll, 0]ComputeZoomToFit(content_px, canvas_px, padding_px)→ returnsZoomToFitResult{scale, scroll}to fit contentIsMouseHoveringOverEntity(entity, rt)→ runtime-based entity hover detection for overworld
Future Feature Ideas (for follow-on work)
Interaction & UX
- Smooth zoom/pan (scroll + modifiers), double-click zoom-to-fit, snap-to-grid toggle.
- Rulers/guides aligned to
grid_step, draggable guides, and a measure tool that reports delta in px and tiles. - Hover/tooltips: configurable hover info (tile id, palette, coordinates) and/or a status strip.
- Keyboard navigation: arrow-key tile navigation, PgUp/PgDn for layer changes, shortcut-able grid presets (8/16/32/64).
Drawing & overlays
- Layer-aware overlays: separate channels for selection/hover/warnings with theme-driven colors.
- Mask/visibility controls: per-layer visibility toggles and opacity sliders for multi-layer editing.
- Batch highlight API: accept a span of tile ids/positions and render combined overlays to reduce draw calls.
Selection & tools
- Lasso/rect modes with additive/subtractive (Shift/Ctrl) semantics and a visible mode indicator.
- Clamp-to-region selection for maps; expose a “clamp to region” flag in options/runtime.
- Quick selection actions: context submenu for fill/eyedropper/replace palette/duplicate to scratch.
Panels & presets
- Preset grid/scale sets (“Pixel”, “Tile16”, “Map”) that set grid_step + default zoom together.
- Per-canvas profiles to store/recall
CanvasConfig+ view (scroll/scale) per editor.
Performance & rendering
- Virtualized rendering: auto skip off-screen tiles/bitmaps; opt-in culling flag for large maps.
- Texture readiness indicator: badge/text when a bitmap lacks a texture; one-click “ensure texture.”
- Draw-call diagnostics: small overlay showing batch count and last-frame time.
Automation & testing
- Deterministic snapshot API: export runtime (hover tile, selection rect, grid step, scale, scroll) for tests/automation.
- Scriptable macro hooks: tiny API to run canvas actions (zoom, pan, select, draw tile) for recorded scripts.
Accessibility & discoverability
- Shortcut cheatsheet popover for canvas actions (zoom, grid toggle, fit, selection modes).
- High-contrast overlays and grids; configurable outline thickness.