Mukund Pareek / mukund pareek
  • Projects
  • Blog
  • Notes
  • Resume

MP Mukund Pareek

Unreal Engine C++ Developer

  • Projects
  • Blog
  • Notes
  • Resume

© 2026 Mukund Pareek. Built with Next.js and Tailwind CSS.

Blog
unreal
editor-mode
plugin
slate
c++

March 5, 2026

Building Editor Modes: GeoTracker Deep Dive

The Unreal Editor Mode API is powerful but poorly documented. This post walks through the core patterns using the GeoTracker distance measurement plugin as a reference. You'll understand tool registration, viewport input interception, and custom toolkit panels after reading this.

What Is an Editor Mode?

An Editor Mode (FEdMode) is a modal context the editor enters that changes how viewport input, rendering, and toolbars behave. The standard modes (Place, Select, Landscape, Foliage) are all editor modes. You can register custom ones from a plugin's StartupModule.

The key classes:

  • FEdMode - the mode itself; handles input, rendering overlay, and activation lifecycle
  • FModeToolkit - provides the left-side panel in the editor (same panel that shows landscape layers, foliage types, etc.)
  • FEditorModeRegistry - global registry where you register/unregister your mode ID

Registration Pattern

Always register in StartupModule and unregister in ShutdownModule:

void FGeoTrackerEditorModule::StartupModule()
{
    FEditorModeRegistry::Get().RegisterMode<FGeoTrackerEdMode>(
        FGeoTrackerEdMode::EM_GeoTracker,
        LOCTEXT("GeoTrackerMode", "GeoTracker"),
        FSlateIcon(), // TODO: add toolbar icon
        true /* bVisible */);
}

void FGeoTrackerEditorModule::ShutdownModule()
{
    FEditorModeRegistry::Get().UnregisterMode(FGeoTrackerEdMode::EM_GeoTracker);
}

EM_GeoTracker is a static FEditorModeID (just an FName). Keep it globally unique - prefix with your plugin name.

Capturing Viewport Clicks

Override HandleClick in your FEdMode subclass. The tricky part: the click event competes with the default selection mode. If your mode should suppress selection while active, return true from HandleClick to consume the event.

bool FGeoTrackerEdMode::HandleClick(
    FEditorViewportClient* ViewportClient,
    HHitProxy* HitProxy,
    const FViewportClick& Click)
{
    FVector WorldOrigin, WorldDir;
    ViewportClient->DeprojectFVector2D(
        Click.GetClickPos(), WorldOrigin, WorldDir);

    FHitResult Hit;
    const bool bHit = GetWorld()->LineTraceSingleByChannel(
        Hit, WorldOrigin, WorldOrigin + WorldDir * 100000.f, ECC_Visibility);

    if (bHit)
    {
        StoreMeasurementPoint(Hit.ImpactPoint);
    }
    return true; // Consumed - suppress default selection
}

DeprojectFVector2D converts the 2D screen click position into a world-space ray. The ViewportClient pointer comes from Enter() - cache it there.

Click-to-measurement pipeline in GeoTracker.

Drawing the Overlay

Override Render() to draw lines and labels over the viewport using FPrimitiveDrawInterface:

void FGeoTrackerEdMode::Render(
    const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI)
{
    for (const FMeasurement& M : Measurements)
    {
        PDI->DrawLine(M.Start, M.End, FLinearColor::Green, SDPG_Foreground, 2.f);
    }
}

For text labels, use FCanvas in DrawHUD() instead - PDI doesn't support text directly.

Building the Toolkit Panel

The toolkit provides the panel that appears on the left when your mode is active:

TSharedRef<SWidget> FGeoTrackerToolkit::GetInlineContent() const
{
    return SNew(SVerticalBox)
        + SVerticalBox::Slot().AutoHeight().Padding(8)
        [
            SNew(STextBlock)
                .Text(LOCTEXT("Title", "GeoTracker Measurements"))
                .Font(FCoreStyle::GetDefaultFontStyle("Bold", 12))
        ]
        + SVerticalBox::Slot().FillHeight(1.f)
        [
            MeasurementListWidget.ToSharedRef()
        ];
}

What I Learned

  • Editor modes must be registered from an editor-only module - adding mode registration to a runtime module causes the editor to refuse loading.
  • HandleClick returning true only suppresses the click for your mode's handlers. The default FEdMode base class still processes it unless you also override InputKey for non-click events.
  • FEditorViewportClient pointers are valid during mode lifetime but not after Exit() is called. Null-check before use in any deferred callback.
  • Slate panels in toolkits are rebuilt every time the mode activates. Cache expensive widget construction behind a lazy factory pattern.