Building an Editor Panel

← Editor Overview | Home

A panel is a GG-UI function the editor's dock shows in a tab. This page walks through adding one, using the Engine Stats panel (EditorStats) as the model. Read GG-UI Foundations first.


The Shape

PartWhereDoes
Build(UI&)src/Editor/Private/<Panel>.cppThe panel's content
Refresh(...)SameOnce a frame on Encoder: reads what the panel shows and bumps a revision when it changed
A GGPanelEditorShell.cppThe panel's name and title string
dock.Panel(...)EditorShell::DeclareRegisters the content with the dock
StringsTrinyx/assets/strings/en.stringsIts title and text
ClassesTrinyx/assets/ui/trinyx.ggssIts looks

Panels are free functions in a namespace (EditorStats::Build) or a small class when they own state (EditorLog). Editor code may use virtuals and classes; the ECS rules are for components.

1. The Header

// src/Editor/Public/EditorMarkers.h
#pragma once
#if !defined(TNX_ENABLE_EDITOR)
#error "EditorMarkers.h requires TNX_ENABLE_EDITOR"
#endif

class EditorContext;

namespace GGUI
{
class UI;
}

// EditorMarkers: the editor's Markers panel. Encoder only.
namespace EditorMarkers
{
void Build(GGUI::UI& ui, EditorContext& editor);
void Refresh(EditorContext& editor);
} // namespace EditorMarkers

Declarations only: the bodies go in the .cpp, so changing the panel rebuilds one file (Code Structure).

2. Read on Encoder, Build From What Was Read

The panel builds inside the UI update, where the editor and the world aren't its to touch. It reads in Refresh, which the editor calls on Encoder at the top of its frame, and bumps a revision when something changed. Build watches the revision, so it re-runs only then.

// src/Editor/Private/EditorMarkers.cpp
namespace EditorMarkers
{
namespace
{
std::vector<Marker> Shown; // what Refresh read
GGRevision Changed;
} // namespace

void Refresh(EditorContext& editor)
{
    std::vector<Marker> now = ReadMarkers(editor);
    if (now == Shown) return;
    Shown = std::move(now);
    Changed.Bump();
}

void Build(UI& ui, EditorContext& editor)
{
    EditorContext* self = &editor;
    ui.Watch(Changed);
    if (auto page = ui.Column({ .Width = GGSize::Grow(), .Height = GGSize::Grow(), .Gap = 4, .Padding = GGEdges::All(8),
            .Overflow = Overflow::Scroll }))
    {
        if (Shown.empty()) ui.Label("Editor.Markers.None", { .Class = "stats-name" });
        for (const Marker& marker : Shown)
        {
            ui.Key(marker.ID);
            if (auto row = ui.Row({ .Width = GGSize::Grow(), .Gap = 8, .Align = GGAlign::Center }))
            {
                ui.Label(GGText::Verbatim(marker.Name));
                ui.Spacer();
                ui.Button("Editor.Markers.Go", [self, id = marker.ID]
                {
                    self->Do([id](EditorContext& editor) { editor.FrameMarker(id); });
                });
            }
        }
    }
}
} // namespace EditorMarkers

Call Refresh from EditorContext::BuildFrame, beside the other panels' (EditorStats::Refresh(State), ...).

  • Values that change often are bound, not rebuilt. Engine numbers go through EngineStats (GGBind(EngineStats::BindTarget, GGExpr::Field(stat))); entity fields through TnxBind or the property grid. A bound label updates every frame with no re-run.
  • Lists keep identity with keys (ui.Key(marker.ID)), so a row keeps its focus and state when the list changes.
  • Large lists use a data view (ui.List, ui.Tree, ui.Table over a GGDataSource), which builds only the rows that show. The Outliner and Content Browser are examples.
  • Editor state that Do changes is watched with editor.Changes().

3. Acting

Handlers run in the UI update too. Anything they do to the editor or a world goes through EditorContext::Do, which runs it on Encoder before the next frame and then moves Changes().

ToUse
Run an editor commandeditor->Do([](EditorContext& e) { e.SaveScene(); })
Edit an entity's fieldThe property grid (ui.Properties) or a field editor: edits arrive as undoable FieldEdits through the editor's edit sink
Make your own undoable changeInside Do: apply it, then editor.PushCommand(std::make_unique<YourCommand>(...), "Name")
Offer a shortcutAn input action in EditorActions.h, run with ui.Command(action, handler); menu items and tool buttons given the action show its chord

A widget's own state (open, scrolled, being edited) needs none of this: ui.State and the handler's captures are the UI's to change.

4. Register It

In EditorShell.cpp:

inline constexpr GGPanel MarkersPanel{ "Editor.Markers", "Editor.Markers.Title" };

void EditorShell::Declare()
{
    ...
    dock.Panel(MarkersPanel, [this](UI& ui)
    {
        EditorMarkers::Build(ui, *Editor);
    });
}
  • The name (Editor.Markers) identifies the panel in saved layouts. Keep it stable once shipped.
  • The title is a string ID. Every registered panel is listed in the View menu, which opens and closes it.
  • To show it in a workspace from the start, add it to a tab group in Workspace(index) (Tabs({ LogPanel, MarkersPanel })). Saved layouts name panels, so one that doesn't know the new panel shows it once it is opened from the View menu.
  • PanelProps can make it unclosable (bClosable = false) or keep its torn-off window above the main one (bAbove).

The panel's content is a page of its own: it doesn't follow the dock's watches, and moving it to another window rebuilds it, so keep anything that must survive outside ui.State (in the panel's namespace or class).

5. Text and Looks

# Trinyx/assets/strings/en.strings
Editor.Markers.Title = Markers
Editor.Markers.None = No markers in this scene
Editor.Markers.Go = Go
Editor.Markers.Go.Tip = Frame the marker in the viewport

Panel text is never written in code: titles, labels, and tooltips are string IDs. Reuse the theme's classes (stats-name, stats-value, stats-heading, meter, ...) and add new ones to Trinyx/assets/ui/trinyx.ggss. Both files hot reload, so wording and looks can be adjusted with the editor running.

6. Test It

A panel's content runs in a headless document like any GG-UI function: build it in a UIContext, then check nodes and click buttons (see Testbed/src/Tests/GGUI_*.cpp). Panels that need an EditorContext are tested through the editor build; keep the logic that decides what to show in functions that take plain data, so it can be tested on its own.

Checklist

  • Build and Refresh declared in a header, defined in a .cpp
  • Reads in Refresh on Encoder; Build watches a revision
  • Changing values bound, not rebuilt; list rows keyed
  • Registered in EditorShell::Declare, in a workspace if it belongs in one
  • Title and text in en.strings; looks in the theme sheet
  • python scripts/presubmit.py passes