| name | godot-editor-theme-layout |
| description | Improve, debug, and repair NeoCade Godot editor theme layout, sizing, spacing, chrome, icons, colors, borders, and Control state styling. Use when working on the NeoCade Theme project in the Godot editor, comparing Pulse/Bubble/Burst/etc. against the default editor theme or Godot Minimal Theme, or when a user provides editor screenshots and asks to make the theme fit, align, or behave correctly. |
Godot Editor Theme Layout
Purpose
Use this skill to iterate on NeoCade editor-theme problems with source-backed diagnosis, measured evidence, and log-only verification when requested. Treat editor screenshots as clues, not proof of which theme item is wrong.
Default local paths:
- NeoCade project:
C:\Programming_Files\Shilocity\Godot\NeoCade-Theme
- Godot source:
C:\Programming_Files\Godot\godot-master
- Godot Minimal Theme:
C:\Programming_Files\Godot\godot-minimal-theme-main
- Canonical theme script:
addons/neocade_theme/scripts/neocade_theme.gd
- Canonical theme resource:
addons/neocade_theme/neocade_theme.tres
- Existing QA probes:
.planning/qa/theme-rescue/
Read references/editor-theme-debugging.md when the task involves editor docks, inspector/filesystem/scene panels, popups, tabs, scrollbars, or any screenshot-to-source investigation.
Workflow
- Identify the visible editor area from the user's screenshot or description.
- Trace the Godot source that creates that area before editing the theme.
- Map rendered controls to actual classes, variations, and theme slots.
- Compare with Godot Minimal Theme and/or Godot built-in editor theme source.
- Patch NeoCade narrowly, usually in
BINDING_TABLE, TYPE_VARIATIONS, verifier scripts, or generated icons.
- Verify with headless scripts and logs. Do not rely on screenshots unless the user explicitly asks or has not forbidden them.
- Report the exact source-class finding, changed theme hooks, and verification results.
Source-First Rules
- Use
rg against Godot source to find the actual editor class and node construction. Examples: InspectorDock, FileSystemDock, SceneTreeDock, DockTabContainer, EditorInspector.
- Read the relevant C++ around the node creation. Do not infer that a visible toolbar is a
TabContainer, PanelContainer, or HBoxContainer until source confirms it.
- Follow class inheritance. Example:
InspectorDock -> EditorDock -> MarginContainer; this means EditorDock.margin_* can affect dock content even when DockTabContainer.panel appears relevant.
- Check type variations. Godot editor often uses strings such as
FlatMenuButton, NoBorderHorizontalBottom, EditorInspectorContainer, EditorPropertyContainer, BottomPanel, and TabContainerInner.
- Theme internal editor class names when needed. It is valid to add rows such as
EditorDock, DockTabContainer, or NoBorderHorizontalBottom when source shows those classes/variations own the layout.
- Treat generic base classes as high blast radius. Do not patch base
TabContainer, MarginContainer, HBoxContainer, VBoxContainer, PanelContainer, or Button behavior for an editor-only symptom until source proves the same hook owns the visible editor case and the runtime case.
Invisible Fix Recovery
If a theme change verifies in logs but the user reports no visible change, stop tuning that same hook. Re-trace the editor node stack from source and find the wrapper that actually owns the visible spacing, background, or chrome.
Recovery order:
- Confirm the changed theme item resolves at runtime.
- Check whether the visible control is wrapped by editor-only classes or theme variations.
- Trace parent and child containers in Godot source.
- Add a targeted probe for each suspected layer before accepting the fix.
- Prefer editor-specific hooks over generic Control hooks unless the issue is truly global.
Session pattern to remember: DockTabContainer.panel did not visibly fix Inspector toolbar spacing; EditorDock.margin_* affected the dock content; NoBorderHorizontalBottom.margin_top controlled the remaining bottom gap.
Regression Avoidance
- Do not turn overlay/focus styleboxes into normal filled panels.
EditorStyles.FocusViewport is drawn directly over the 2D/3D viewport; it must be transparent center / outline-only (draw_center=false in Godot source terms, or NeoCade's focus_ring recipe). A filled background here hides the entire viewport on hover/focus.
- Do not fix one plain dialog by broadening base
Tree.panel without checking every source path that also uses it. EditorResourcePicker draws its inspector fields from Tree.panel, and TreeSecondary / TreeTable inherit from Tree; a base Tree surface change can regress Scene, Signals, Groups, resource pickers, dependency dialogs, and settings tables.
- When a Signals/Groups/class subsection row should reveal the parent list edge on the left/right, keep the fix scoped to
Editor.prop_subsection and Editor.prop_subsection_stylebox. Critical trap: these rows call TreeItem.set_custom_bg_color() with Editor.prop_subsection, and Tree draws that color edge-to-edge before drawing the stylebox. Keep Editor.prop_subsection transparent and put the visible inset fill in Editor.prop_subsection_stylebox; do not change TreeSecondary/TreeTable title buttons unless source inspection proves the target is a real Tree title row.
Verification Standard
Use scripts and logs as the main proof. Existing useful commands:
& 'C:\Programming_Files\Godot\Godot_v4.6.2-stable_win64.exe\Godot_v4.6.2-stable_win64.exe' --headless --path . --import
& 'C:\Programming_Files\Godot\Godot_v4.6.2-stable_win64.exe\Godot_v4.6.2-stable_win64.exe' --headless --path . --script .planning/qa/theme-rescue/theme_rescue_verify.gd
& 'C:\Programming_Files\Godot\Godot_v4.6.2-stable_win64.exe\Godot_v4.6.2-stable_win64.exe' --headless --path . --script .planning/qa/theme-rescue/theme_default_compare.gd
& 'C:\Programming_Files\Godot\Godot_v4.6.2-stable_win64.exe\Godot_v4.6.2-stable_win64.exe' --headless --editor --path . --script .planning/qa/theme-rescue/theme_editor_dock_probe.gd
& 'C:\Programming_Files\Godot\Godot_v4.6.2-stable_win64.exe\Godot_v4.6.2-stable_win64.exe' --headless --path . --script .planning/qa/theme-rescue/theme_scene_structure_probe.gd
& 'C:\Programming_Files\Godot\Godot_v4.6.2-stable_win64.exe\Godot_v4.6.2-stable_win64.exe' --headless --path . --script .planning/qa/theme-rescue/theme_tab_state_probe.gd
& 'C:\Programming_Files\Godot\Godot_v4.6.2-stable_win64.exe\Godot_v4.6.2-stable_win64.exe' --headless --path . --script .planning/qa/theme-rescue/theme_popup_scrollbar_probe.gd
& 'C:\Programming_Files\Godot\Godot_v4.6.2-stable_win64.exe\Godot_v4.6.2-stable_win64.exe' --headless --path . --script .planning/qa/theme-rescue/theme_mobile_tap_target_probe.gd
& 'C:\Programming_Files\Godot\Godot_v4.6.2-stable_win64.exe\Godot_v4.6.2-stable_win64.exe' --headless --path . --script .planning/qa/theme-rescue/theme_create_dialog_probe.gd
& 'C:\Programming_Files\Godot\Godot_v4.6.2-stable_win64.exe\Godot_v4.6.2-stable_win64.exe' --headless --path . --script .planning/qa/theme-rescue/theme_editor_regression_probe.gd
& 'C:\Programming_Files\Godot\Godot_v4.6.2-stable_win64.exe\Godot_v4.6.2-stable_win64.exe' --headless --path . --script .planning/qa/theme-rescue/theme_editor_spacing_probe.gd
& 'C:\Programming_Files\Godot\Godot_v4.6.2-stable_win64.exe\Godot_v4.6.2-stable_win64.exe' --headless --editor --path . --script .planning/qa/theme-rescue/theme_editor_sentinel_leak_probe.gd
& 'C:\Programming_Files\Godot\Godot_v4.6.2-stable_win64.exe\Godot_v4.6.2-stable_win64.exe' --headless --editor --path . --script .planning/qa/theme-rescue/theme_editor_merge_leak_probe.gd
& 'C:\Programming_Files\Godot\Godot_v4.6.2-stable_win64.exe\Godot_v4.6.2-stable_win64_console.exe' --headless --editor --path . --script .planning/qa/theme-rescue/theme_option_button_editor_passive_probe.gd
& 'C:\Programming_Files\Godot\Godot_v4.6.2-stable_win64.exe\Godot_v4.6.2-stable_win64_console.exe' --headless --path . --script .planning/qa/theme-rescue/theme_showcase_runtime_theme_probe.gd
& 'C:\Programming_Files\Godot\Godot_v4.6.2-stable_win64.exe\Godot_v4.6.2-stable_win64_console.exe' --headless --path . --script .planning/qa/theme-rescue/theme_performance_probe.gd
git diff --check
Prefer adding a small targeted GDScript probe when a bug depends on an editor class, theme variation, or inherited theme lookup. Keep probes in .planning/qa/theme-rescue/.
For spacing, padding, clipping, icon-size, or alignment complaints based on a user-provided screenshot, measure pixels before changing constants. Do not guess spacing by eye. User-provided screenshots may be measured even when new screenshots are forbidden. Report measured values and what theme hook controls them.
When investigating editor-setting color leaks, ask the user to temporarily set obvious sentinel colors in the Godot editor first; do not assume the editor is already using sentinel values. Best diagnostic pair: set interface/theme/base_color to #ff00ff (magenta) and interface/theme/accent_color to #00ffff (cyan), then reproduce/reload the editor theme and run theme_editor_sentinel_leak_probe.gd. Magenta marks generated editor base/surface leaks; cyan marks generated editor accent leaks. These colors are temporary diagnostic aids only and should not be treated as the normal theme configuration. Godot builds a generated editor theme from interface/theme/* and then merges the custom theme over it, so unowned editor slots can leak sentinel colors even when runtime controls are fully styled. Treat probe hits as missing theme ownership for real editor type/slot names, not as a reason to globally copy editor settings. theme_editor_sentinel_leak_probe.gd scans both the merged EditorTheme type/slot table and the live editor Control tree for resolved sentinel colors/styleboxes.
What Helps Behind The Scenes
- Comparing runtime theme values against
ThemeDB.get_default_theme().
- Logging stylebox colors, borders, content margins, expand margins, corner radii, icon sizes, and constants.
- Testing flat, raised, desktop, and mobile variants across all selectable styles when a change affects shared bindings.
- Checking for
UNINTENTIONAL_STYLE, positive shadows, wrong label chrome, excessive icon sizes, missing type variations, and fallback/default-only drift.
- Checking
StyleBoxEmpty vs StyleBoxFlat when a Godot default gap should inherit its parent surface instead of painting its own color.
- Fully authoring state slots for editor secondary variations when probes show Godot is falling back to defaults instead of resolving inherited NeoCade slots.
- Tracing flat editor inspector controls back to their parent paint source. Many editor property
OptionButtons and EditorSpinSliders set flat = true, so their normal face is not drawn by the control itself; EditorProperty.child_bg is the reusable value-cell surface.
- Using Godot Minimal Theme as a practical reference for compact editor spacing and editor-specific variations, while avoiding code-for-code copying.
- Regenerating comparison logs after changing intentional overrides so future sessions can see what changed and why.
Reusable Editor Patterns
- Treat editor screenshots as compositions of nested generic controls plus editor-specific variations. Trace the owning source class first, then decide whether the fix belongs to the base control, an editor variation, or a wrapper.
- Splitter gaps should usually be contextual. Godot's default theme sets
SplitContainer.split_bar_background, HSplitContainer.split_bar_background, and VSplitContainer.split_bar_background to StyleBoxEmpty; the modern editor theme and Godot Minimal Theme mostly set splitter constants/icons, not a painted background. This lets docks, dialogs, and panels inherit the parent surface instead of forcing one global stripe color.
- Do not globally paint split-bar backgrounds to the darkest surface unless the user explicitly wants a visible stripe everywhere. The safer editor-layout default is an empty split-bar background with explicit grabber icons/colors.
- SplitContainer grabbers may be correctly themed but hidden. Godot only draws the non-touch grabber while dragging, hovering, forced visible, or when
autohide is disabled. Log split_bar_background, separation, minimum_grab_thickness, autohide, and grabber icon sizes from the merged editor theme before calling it a styling regression.
- Secondary editor variations can be inheritance traps. Variations such as
TreeSecondary, ItemListSecondary, ScrollContainerSecondary, and editor-only container variants may not resolve every dynamic NeoCade slot the way the base type does. If probes show fallback colors or sizes, explicitly author the variation slots.
- For list/tree views with no separator/border/outline, check all relevant theme paths: panel border width, guide/relationship colors, guide/relationship constants,
outline_size, and font_outline_color. Use Button hover/pressed styleboxes as the reference for item hover/selected backgrounds when the user asks for state consistency.
TreeTable is a compact editor table variation, not just "Tree with table colors." Godot's ActionMapEditor uses TreeTable for the Input Map action rows and packs Revert/Add/Remove as TreeItem buttons, not normal Button nodes. Tree lays these buttons right-to-left. Godot's modern theme keeps TreeTable.button_margin=0, but its button_pressed and button_hover styleboxes reserve transparent left/right content margins; without those margins the icons visually collapse. Fix the TreeTable button stylebox minimum size before changing icon artwork, unrelated toolbar spacing, or generic Tree spacing. Verify with a get_item_area_rect() probe.
- For Tree nesting paths, do not confuse row guides with relationship lines.
draw_guides/guide_color control guide/separator style lines; draw_relationship_lines, relationship_line_width, parent_hl_line_width, children_hl_line_width, and the related colors control the parent-child path lines.
- Respect Godot's editor relationship-line setting semantics.
interface/theme/draw_relationship_lines maps None=0, Selected Only=1, All=2; Selected Only still sets draw_relationship_lines=1, but keeps relationship_line_width=0 while parent_hl_line_width/children_hl_line_width draw only the selected branch path. Gate editor-setting reads behind Engine.is_editor_hint() before touching EditorInterface.
- For editor settings/project settings property inputs, source usually routes enum and numeric fields through
EditorProperty children. Theme the value surface with EditorProperty.child_bg, keep EditorProperty.bg transparent, and use EditorSpinSlider.label_bg plus EditorInspectorButton sizing/color slots for consistency.
- Resource picker fields such as Theme, Material, and Script are
EditorResourcePicker controls. Godot currently draws their background from Tree.panel, so making base Tree.panel empty lets EditorProperty.child_bg show through; keep visible tree/list surfaces on contextual variations such as TreeSecondary instead.
ScrollContainer.panel is empty in Godot's default runtime theme and in Godot Minimal Theme. Keep the base ScrollContainer visually empty unless a source-traced editor variation proves it needs a panel; a bordered runtime ScrollContainer reads as unwanted chrome.
- If a plain editor dialog
Tree needs a visible list surface, remember this competes with EditorResourcePicker, which also draws Tree.panel. This is a high-blast-radius tradeoff: keep the base Tree panel borderless/subtle, and re-run resource picker/editor property probes after changing it.
- Inspector resource fields (
Theme, Material, Script) are EditorPropertyResource containing EditorResourcePicker; the picker paints Tree.panel directly. If these fields do not match inspector value cells, compare Tree.panel against EditorProperty.child_bg in a probe.
- Inspector/category/property text may use
EditorFonts.main / EditorFonts.bold instead of ordinary Control font slots. When font families look mixed in editor-only UI, explicitly author EditorFonts aliases before chasing Label/Button font slots.
- Checkbox and CheckButton active icon tint is not controlled by ordinary
Button.icon_*_color. Godot draws CheckBox.checkbox_checked_color / checkbox_unchecked_color and CheckButton.button_checked_color / button_unchecked_color; author those slots when checked state should use accent.
- CheckBox and CheckButton state styleboxes must keep stable content margins across
normal, hover, pressed, hover_pressed, mirrored states, and disabled states. Godot's default and Godot Minimal Theme use empty/stable state boxes here; mismatched padding causes horizontal label jumps, especially in NeoCade's flat path when raised=false.
- Bottom panel tabs and counters are editor-specific. The
Output, Debugger, etc. labels are TabContainer tabs under the BottomPanel variation, utility buttons use BottomPanelButton, and Output counters use EditorLogFilterButton.
- Do not assume the bottom status ellipsis is
EditorToaster or a disabled FlatMenuButton. The bottom bar has multiple adjacent controls: bottom TabContainer popup/menu button, separators, EditorToaster, version label, pin/expand buttons. If changing FlatMenuButton.disabled, BottomPanel.panel, or RichTextLabel.normal produces no visible change, revert it and trace the live node stack or TabContainer internals before another patch.
- The bottom
Output / Audio / Shader Editor content shell is EditorBottomPanel (DockTabContainer) using EditorStyles.BottomPanel as its panel style override. The visible child dock position can be checked with a live editor probe: if an active bottom dock reports pos=(6,5) or similar, the shell stylebox is still adding content margin. Fix that shell before touching RichTextLabel, disabled buttons, or per-pane controls.
- The bottom bar 3-dot menu next to the status/version controls is the internal
TabContainer popup button. It uses TabContainer.menu / menu_highlight, sourced from EditorIcons.GuiTabMenu* in Godot's editor theme. Do not resize/reposition live editor nodes to fix it. If an icon-canvas experiment only moves the visible dots or fails to change the button allocation, revert it and leave the issue open until the actual TabContainer popup-button sizing path is proven.
- The 2D viewport toolbar's main row is
CanvasItemEditor -> MainToolBarMargin -> HFlowContainer -> HBoxContainer with plain containers that do not paint styleboxes. EditorStyles.ContextualToolbar only paints the secondary contextual PanelContainer row. EditorStyles.Content paints the whole EditorMainScreen panel, so changing it to fix only the toolbar will affect the entire viewport/content shell and is usually a failed fix.
- Top-right editor run controls are
EditorRunBar buttons using RunBarButton, not generic toolbar buttons. In Godot's modern theme RunBarButton inherits FlatMenuButton; active/toggled run buttons use transparent pressed chrome and tint the icon via icon_pressed_color. If Play/Pause/Stop/Run Scene buttons look like large filled squares, fix RunBarButton pressed/hover_pressed styleboxes and type variation inheritance before touching base FlatButton.
- Crowded toolbar icons are often generic
FlatButton / FlatMenuButton, not a broken icon asset. Godot modern uses base_empty_wide_style for these buttons, and Godot Minimal Theme uses base_empty_wide_sb: transparent normal chrome with wider left/right content margins. In NeoCade, shared FlatButton / FlatMenuButton spacing should stay consistent between runtime and editor; add a separate explicit compact variation only when compact icon buttons are a deliberate product feature.
- Do not fix custom popup spacing through the wrong popup class. Normal menu lists are
PopupMenu and should stay dense when the user rejects menu-list padding. Dock Position is DockContextPopup : PopupPanel (editor/docks/editor_dock_manager.cpp), while Control anchor/alignment popovers use ControlEditorPopupButton with a child PopupPanel (editor/scene/gui/control_editor_plugin.cpp). Put edge breathing room on PopupPanel.panel for those custom popups, not on PopupMenu.panel.
- Missing secondary contextual-toolbar contrast maps to
EditorStyles.ContextualToolbar. Godot modern and Minimal Theme give the 2D/3D contextual toolbar a higher surface than the main content; theme that slot directly only for the contextual PanelContainer row, not the whole main toolbar band.
- SplitContainer hover affordance length and thickness are separate. If the grabber feels too small, increase the generated grabber icon length toward the built-in/editor scale while keeping
minimum_grab_thickness, separation, and actual icon thickness stable so the grab area does not grow.
- Missing editor dividers are usually separator styleboxes, not container spacing. Godot modern and Minimal Theme use
StyleBoxLine for HSeparator, VSeparator, and PopupMenu separator slots; a flat stylebox with tiny margins can render as a filled block or visually disappear. Verify line orientation (VSeparator.vertical=true) and alpha before changing toolbar layout.
- The renderer selector text (
Forward+, Mobile, Compatibility) is a flat OptionButton with TopBarOptionButton. Its text color comes from Editor.forward_plus_color, Editor.mobile_color, or Editor.gl_compatibility_color via EditorNode::_update_renderer_color(), not from accent_color; changing the editor accent intentionally does not recolor "Compatibility" in the built-in theme.
- Godot Minimal Theme and Godot's built-in modern/classic themes explicitly author
Editor.forward_plus_color, Editor.mobile_color, and Editor.gl_compatibility_color. These are legitimate editor semantic colors, but they should be generated only under Engine.is_editor_hint(); do not pay runtime regeneration cost for renderer selector slots.
- Keep editor-only theme ownership behind an editor-hint gate. Runtime generation should skip editor-only type variations and binding rows such as
Editor, EditorIcons, EditorStyles, EditorProperty, RunBarButton, TopBarOptionButton, TreeSecondary, TreeTable, BottomPanel, and dock/editor panel wrappers. Add or run a runtime gate probe when touching this area so the normal game theme still authors core controls while editor-only slots stay absent outside the editor.
- Be careful with editor probes from the CLI: a normal
--script SceneTree run is not an editor-hint context even if the script is named "editor regression." If the probe needs Engine.is_editor_hint() / EditorInterface, make it skip clearly outside editor context and use source/static checks plus runtime gate probes for command-line verification.
- Editor Settings top tabs use
TabContainerOdd, not the base TabBar. Signals/class header rows use Editor.prop_subsection and Editor.prop_subsection_stylebox, not Tree column title buttons.
- File dialog thumbnail blur usually means a tiny fallback icon is being scaled up. Check
FileDialog.file_thumbnail/folder_thumbnail and filesystem EditorIcons.FileBigThumb/FolderBigThumb/medium variants; imported SVG texture size should be close to the requested thumbnail size.
- The Output log body is a plain
RichTextLabel in EditorLog. Godot editor themes give it a background by styling global RichTextLabel.normal; NeoCade intentionally keeps base RichTextLabel text-only, so use BottomPanel / EditorStyles.BottomPanel for theme-only Output-safe background work unless a source/plugin hook adds an Output-specific variation.
- Plain editor
RichTextLabel surfaces appear in logs, help, export result dialogs, warnings, and other editor panes. If runtime RichTextLabel must stay text-only, apply the opaque editor RichTextLabel panel from a guarded editor-only runtime hook (Engine.is_editor_hint() / EditorInterface) instead of changing the base runtime binding.
- Generic editor dialogs may use a plain
Tree with no type variation. If a dialog list blends into the dialog background, trace whether the list is Tree, ItemList, or a secondary variation before assuming the shell panel is wrong. A visible, borderless base Tree.panel can be necessary for plain editor Tree dialogs.
- Inspector layout hints are
ControlPositioningWarning and get their panel from EditorProperty.bg_group_note. If the icon/text hugs the edge, fix the stylebox content margins before changing Label or TextureRect behavior.
- Signals/Groups/class subsection rows use
Editor.prop_subsection_stylebox. If the header should reveal the parent view edge, reserve only the needed transparent side space there and avoid full outlines that turn subsection rows into boxes. If changing the stylebox does nothing visually, inspect the paired Editor.prop_subsection color first; an opaque color masks the stylebox inset in Tree.
- Checkbox/toggle active fill should stay on Godot's built-in modulation path when possible. Use white mask SVGs with black internal marks/knobs, then set
CheckBox.checkbox_checked_color / CheckButton.button_checked_color to accent and the unchecked colors to a lighter base-derived inactive fill. This keeps base_color / accent_color dynamic without baking per-color icons.
- CheckBox radio buttons use the CheckBox icon slots (
radio_checked, radio_unchecked) and the same checkbox_checked_color / checkbox_unchecked_color modulation path. Keep radio, checkbox, and checkbutton active/inactive fills visually related unless the user asks for a deliberate distinction. PopupMenu radio/check items are different: PopupMenu draws those icons with item icon_modulate, so CheckBox color slots do not recolor popup menu radios; use tiny runtime-generated PopupMenu icons from the same accent/inactive roles when menu check/radio items must visually match the rest of the theme. NeoCade gates this with the Advanced export use_runtime_popup_selection_icons so overhead can be disabled deliberately.
- Static SVG checkbox/checkbutton/radio icons cannot automatically follow the exported corner radius. Dynamic radius support requires generated icons or a small family of per-radius assets; static SVGs are acceptable when the radius mismatch is minor and the visual contract values consistency over exact per-control radius.
- Godot's editor theme settings (
interface/theme/base_color, accent_color, etc.) can leak through unstyled editor slots as obvious sentinel fallback colors when the user changes those settings. If NeoCade is not intentionally adopting editor settings as its source of truth, author the specific missing editor theme slots found by theme_editor_merge_leak_probe.gd so internal editor widgets stop falling back to Godot's generated editor theme.
- Editor icon polarity is a separate generate-before-merge caveat, not a normal missing-slot leak. When NeoCade is loaded as
interface/theme/custom_theme, Godot first generates EditorIcons from interface/theme/*; with interface/theme/icon_and_font_color = Auto, the icon/font polarity follows interface/theme/base_color. If the user sets a bright editor base color such as #ffffff, Godot bakes dark editor icons before NeoCade's dark surfaces are merged. The supported workaround is to set interface/theme/icon_and_font_color to Light. Do not refactor NeoCade to own every built-in EditorIcons texture just to bypass this; that is a large Godot-version maintenance surface. Do not mutate global EditorSettings from NeoCadeTheme; only an explicit opt-in editor plugin should change editor settings, and any such code must be editor-only guarded.
- Sentinel leaks in editor screenshots often map to
EditorStyles and editor-only variations, not ordinary runtime controls. EditorStyles.Content paints the main editor content panel, EditorStyles.DebuggerPanel paints the debugger bottom panel, EditorStyles.PanelForeground / PanelForeground.panel / EditorInspectorForeground.panel paint foreground editor panels, and TabContainerInner / TabBarInner own inner editor tab strips. Source-trace these first before changing base Panel, TabContainer, TabBar, or MarginContainer.
- A sentinel leak run is not finished until both sections in
theme-editor-sentinel-leaks.log have zero LEAK lines: merged EditorTheme slots and live editor Control tree resolved values. Re-run the probe after each fix; do not claim success from source edits alone.
- Common editor sentinel leak families:
_mirrored Button/CheckBox/CheckButton styleboxes, FlatButton/FlatMenuButton mirrored hover/pressed states, SpinBox pressed arrow colors plus up/down pressed background styleboxes, PopupProgressBar.background/fill during editor loading/import, TreeTable row-state styleboxes, TreeLineEdit.normal, and editor-only panels such as GamePanel, PanelContainerTabbarInner, ScrollContainerSecondary, EditorAudioBus, EditorDebuggerInspector, and EditorValidationPanel.
- After changing source formulas or reverting a failed visual experiment, regenerate comparison logs before committing. Stale
theme-default-vs-neocade.log entries can preserve old margins/colors even when neocade_theme.gd has been corrected; treat log/source disagreement as a commit blocker.
- When a dynamic
Theme regenerates while assigned to a live editor Control.theme, every unbatched Theme.set_*() mutation can emit changed and cause repeated Control::set_theme() propagation / NOTIFICATION_THEME_CHANGED work across the whole Control subtree. NeoCade should block signals during _regenerate_theme(), then emit one final changed signal. Verify with a changed-signal count probe and a headed editor apply benchmark before blaming individual styleboxes.
- CLI
SceneTree timing is useful for raw regeneration/load costs, but it cannot prove a headed inspector freeze. Use .planning/qa/theme-rescue/theme_headed_editor_apply_benchmark.gd from the Godot Script workspace when measuring the actual editor assignment path; it times resource loading, regeneration, root Control.theme assignment, and follow-up editor frames, then restores the original theme.
- If switching back to
showcase/showcase.tscn freezes the headed editor while switching to a new/empty scene does not, check for scene/editor theme coupling before assuming recursive regeneration. The confirmed 2026-05 freeze happened when NeoCade was installed as the Godot editor custom theme and the edited Showcase root also serialized theme = ExtResource("res://addons/neocade_theme/neocade_theme.tres"). Diagnostic logs showed one regeneration per theme instance, not recursion; raw generation measured around 25ms uncached and around 4ms with persistent texture cache warmed, so a multi-minute stall points at editor assignment/inspection of the live dynamic theme resource.
- Showcase/sample scenes that are edited while NeoCade is also the editor custom theme should not serialize the canonical theme onto the root
Control.theme. Keep the .tscn root theme unset and apply at runtime with NeoCadeTheme.apply_to_control(self) from _ready(). The helper must no-op in editor mode and when the target already has a theme.
NeoCadeThemeOptionButton must be passive in editor mode for unthemed showcase roots. Do not mirror a null root theme to the None entry just because the edited scene has no serialized theme; use select(-1) in editor mode and let runtime mirror the already-applied target theme before honoring serialized picker state. Verify with theme_option_button_editor_passive_probe.gd and theme_showcase_runtime_theme_probe.gd.
- Temporary editor-freeze diagnostics must not ship. After adding synchronous log writes or probe-only instrumentation, remove them from both the working tree and the staged index, then run
rg for diagnostic marker names and git diff --cached --check.
- Keep
neocade_theme.tres lean: script reference plus exported source-of-truth values only. Generated Theme entries, subresources, and icon ext_resources are rebuilt from neocade_theme.gd; serializing them bloats load/apply time, preserves stale generated slots, and diverges from Godot Minimal Theme's script-driven resource pattern.
- Godot editor testing can reserialize
neocade_theme.tres with thousands of generated Theme entries, especially after changing exports such as platform, style, or raised in the Inspector. Treat this as editor-produced dirty state, not as a normal code fix. Do not commit the bloated resource unless the user explicitly wants a serialized generated snapshot; prefer restoring the lean resource before release/commit work.
- If a consumer sample intentionally serializes a scene-local NeoCade theme, keep it as an external reference to
res://addons/neocade_theme/neocade_theme.tres rather than embedding generated Theme entries. If a scene save embeds generated entries, the text scene can balloon by hundreds of KiB and trigger Godot's large text resource warning; run the embedded-theme cleanup probe and verify the scene still opens.
- Cache static SVG loads and deterministic generated textures within every regeneration pass. Rebuilding hue ramps, split grabbers, slider knobs, PopupMenu check/radio icons, and empty textures repeatedly inside one setter call can dominate regeneration time even when the stylebox table itself is modest. NeoCade also has an Advanced
texture_cache export: keep it disabled by default to release temporary caches after each regeneration, and enable it only when live editor/theme tweaking benefits from retaining those textures across regenerations.
- Keep optional texture caches instance-local. A static persistent cache makes one theme resource retain textures process-wide and lets another instance clear/warm it unexpectedly. If a cache toggle is public, add a multi-instance verifier that proves toggling cache on one
NeoCadeTheme does not mutate another instance's retained cache.
- Public export-surface changes must update the project contract (
AGENTS.md, .planning/PROJECT.md, .planning/REQUIREMENTS.md, and roadmap/state references) and add a verifier for NeoCadeTheme.new() defaults. The script defaults should match the canonical starter style exports so code-created themes do not silently start as Pulse-looking-but-Custom-matching hybrids.
- Some editor classes are registered built-in classes, not type variations. If
set_type_variation() logs "A type associated with a built-in class cannot be marked as a variation", remove that entry from TYPE_VARIATIONS and keep explicit BINDING_TABLE slots for the type instead.
- Label and RichTextLabel must remain visually text-only. If editor-generated fallback leaks through
Label.normal, use StyleBoxEmpty or fully transparent zero-border chrome and update probes to treat that as intentional blocking of fallback, not as visible label chrome.
- EditorSpinSlider hover uses
HSlider.grabber_highlight; generic sliders use grabber_highlight and grabber_area_highlight. If a hover knob changes shape, inspect the HSlider/VSlider icons rather than EditorSpinSlider.label_bg.
SpinBox.updown is a legacy composite icon slot. If it is non-empty, Godot skips drawing separate SpinBox.up and SpinBox.down icons, so arrow centering can look wrong inside the half-buttons. Keep runtime SpinBox.updown empty when separate centered arrows are desired; keep EditorSpinSlider.updown and Tree.updown independent because those editor controls intentionally use a composite icon.
- ColorPicker alpha/checker artifacts usually come from tiled icon slots, not text. Check
ColorPicker.sample_bg, ColorPickerButton.bg, and ColorPresetButton.preset_bg; they should be simple checker textures, not pictorial glyphs that repeat across swatches.
ColorPicker.color_hue is named like an icon but must behave like Godot's generated hue ramp: a long horizontal gradient texture that ColorPickerShapeRectangle stretches and rotates for the vertical hue bar. Do not use a decorative SVG glyph here. Also keep HSlider.slider / VSlider.slider cross-axis minimum size nonzero; the ColorPicker intensity slider has no custom draw callback and will show only a grabber if the base slider track stylebox has zero height/width.
ColorPickerButton cannot be made into a true 48px mobile tap target from Theme alone without damaging its swatch: Godot exposes only the normal stylebox and bg icon, and its draw code subtracts the stylebox minimum size from the swatch rect. Use scene/custom minimum size for standalone mobile swatches instead of inflating stylebox margins until the swatch disappears.
- For mobile tap targets, verify actual
get_combined_minimum_size() in addition to proxy audits. Stylebox density scaling can still leave button variations, icon/flat buttons, CheckBox, CheckButton, OptionButton, LineEdit, SpinBox, sliders, tabs, and PopupMenu rows below 48px; use mobile-only padding/constant overrides, keep scrollbars compact, and treat ColorPickerButton as source-limited. NeoCade mobile sizes are 1920x1080 design-space units that scale to devices, not physical device pixels, so scrollbars should stay readable as compact indicators rather than trying to satisfy button-like 48px hit targets.
- Mobile editor inspector rows are not fixed by the base
OptionButton alone. Godot editor property rows use Editor.inspector_property_height, and many inspector enum/resource fields use EditorInspectorButton / EditorInspectorFlatButton variations. When an Inspector OptionButton looks desktop-height in mobile mode, log Editor.inspector_property_height, EditorProperty.child_bg, EditorSpinSlider.label_bg, and live OptionButton sizes before changing generic OptionButton.
- Mobile editor audits should log every live interactive control, but distinguish theme-owned minimum-size failures from source-constrained editor internals.
EditorResourcePicker icon buttons, EditorPropertyColor swatches (ColorPickerButton), some Asset Library/SpriteFrames/TileSet plugin buttons, and DynamicFont import micro fields can be explicitly source-limited; do not broaden base controls or move live editor nodes just to make these pass a 48px heuristic.
- Icon-only mobile buttons can fail even when text buttons pass. A 12px icon plus 16px top/bottom padding yields only 44px, so flat/icon button variations may need slightly larger mobile-only vertical padding than text-heavy buttons. Verify both text and icon-only probes after changing
Button, FlatButton, FlatMenuButton, RunBarButton, BottomPanelButton, or EditorLogFilterButton.
- Mobile hit area fixes do not automatically make small glyphs mobile-readable. CheckBox/radio/checkbutton icons, LineEdit clear icons, and TabBar overflow arrows should be verified as icon textures too. Prefer mobile-only SVG raster scaling through the theme loader (
mobile_svg_scale) over editing/recomposing source SVG artwork; keep ColorPickerButton chrome margins small so a scene-level 48x48 minimum still leaves a usable swatch.
- Slider mobile comfort needs both the logical hit target and visible affordance checked.
Slider::get_minimum_size() uses the larger of slider stylebox minimum and grabber icon size, so a 48px transparent icon can pass a tap-target probe while the visible knob still looks desktop-small. Probe the icon alpha bounding box and track stylebox cross-axis size before calling HSlider/VSlider mobile-ready.
FoldableContainer title height is source-driven from title stylebox margins plus the arrow/text height; it has no separate title-height constant. Mobile fixes belong on title_panel / hover / collapsed stylebox mobile_padding, arrow mobile_svg_scale, and h_separation, with desktop values left unchanged.
- Embedded
Window close button placement is source-driven: Godot draws Window.close at x = width - close_h_offset, y = -close_v_offset, while title text is centered within title_height. For mobile, scale the close icon and update title_height, close_h_offset, and close_v_offset together; verify the offset formula instead of nudging scene nodes.
- Mobile list comfort depends on row metrics, not just fonts.
ItemList.v_separation, Tree.v_separation, PopupMenu.v_separation, and PopupMenu.icon_max_width may need mobile-only values so rows and menu check/radio icons do not stay desktop-sized while surrounding buttons grow.
- Runtime-generated PopupMenu check/radio icons should include SVG scale in their generated texture cache key. Otherwise desktop and mobile generated icons can collide when
texture_cache is enabled or when multiple generated sizes are produced during one regeneration. If the generated-icon Advanced toggle is disabled, the disk-icon fallback must receive the same mobile SVG scale so the opt-out path does not shrink back to desktop glyphs.
- PopupMenu disabled check/radio slots are distinct from enabled slots. When generating icons from theme colors, include disabled state in the recipe, fill color, and cache key; otherwise disabled checked menu items can look active/accented.
- Godot 4.6 PopupMenu includes searchable-menu chrome (
search icon and search_bar_separation). Author those slots alongside regular row spacing so searchable popups do not fall back to default/editor visuals.
- Raised depth must append layout height, not steal face/content height. In NeoCade this means every stylebox with a raised bottom depth should reserve the extra bottom border in
content_margin_bottom; do not rely on the thicker bottom border alone. Run theme_raised_depth_probe.gd after touching raised_intensity, raised_face_edge, reserve_raised_depth, or raised border generation.
- Compare editor-only type-variation base classes against Godot's editor theme source before adding custom styling. Example:
EditorAudioBusEffectsTree is a Tree variation in Godot's modern theme, not a PanelContainer; a wrong base type skips Tree-specific row slots and can make editor panes behave strangely.
- Code editor readability is split between Theme and EditorSettings.
CodeEdit.normal, gutter colors, caret, selection, completion, and line colors are theme slots; syntax token colors and several highlighting colors come from text_editor/theme/highlighting/* EditorSettings.
- Keep concrete source maps as examples, not as the only target. Example:
CreateDialog / "Create New Node" is built in editor/gui/create_dialog.cpp with nested split containers, TreeSecondary, ItemListSecondary, and HeaderSmall; the same investigation pattern applies to other editor dialogs and docks.
Collaboration Style
The user will often iterate from screenshots and corrections. Treat corrections as new evidence, not as failure noise. Keep working autonomously, but explain the source mapping and verification briefly. If screenshots are forbidden for a session, do not take them; use scripts, logs, source reads, and pixel measurement of user-provided images instead.