Building an Editor Panel
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
| Part | Where | Does |
|---|---|---|
Build(UI&) | src/Editor/Private/<Panel>.cpp | The panel's content |
Refresh(...) | Same | Once a frame on Encoder: reads what the panel shows and bumps a revision when it changed |
A GGPanel | EditorShell.cpp | The panel's name and title string |
dock.Panel(...) | EditorShell::Declare | Registers the content with the dock |
| Strings | Trinyx/assets/strings/en.strings | Its title and text |
| Classes | Trinyx/assets/ui/trinyx.ggss | Its 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::, 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 throughTnxBindor 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.Tableover aGGDataSource), which builds only the rows that show. The Outliner and Content Browser are examples. - Editor state that
Dochanges is watched witheditor.Changes().
3. Acting
Handlers run in the UI update too. Anything they do to the editor or a world goes through EditorContext::, which runs it on Encoder before the next frame and then moves Changes().
| To | Use |
|---|---|
| Run an editor command | editor->Do([](EditorContext& e) { e.SaveScene(); }) |
| Edit an entity's field | The property grid (ui.Properties) or a field editor: edits arrive as undoable FieldEdits through the editor's edit sink |
| Make your own undoable change | Inside Do: apply it, then editor.PushCommand(std::make_unique<YourCommand>(...), "Name") |
| Offer a shortcut | An 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. PanelPropscan 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
BuildandRefreshdeclared in a header, defined in a.cpp
- Reads in
Refreshon Encoder;Buildwatches a revision
- Changing values bound, not rebuilt; list rows keyed
- Actions through
EditorContext::; world edits undoableDo
- 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.pypasses