FlowPanels

Overlay UI panels for canvas controls. Add custom buttons, zoom controls, or any UI elements that float above the canvas.

Overview

FlowPanels provides a container for overlay UI elements. Place it inside the <Panels> section of FlowCanvas to add floating controls.

Usage

<FlowCanvas Graph="graph" Height="100vh" Width="100vw">
    <BackgroundContent>
        <FlowBackground class="grid-bg"/>
    </BackgroundContent>
    <Panels>
        <FlowPanels>
            <!-- Your custom UI here -->
            <button @onclick="ZoomIn">Zoom In</button>
            <button @onclick="ZoomOut">Zoom Out</button>
        </FlowPanels>
    </Panels>
</FlowCanvas>

Zoom Controls Example

A complete zoom control panel from the GraphViewportUnity example:

@using FlowState.Components
@using FlowState.Models

<FlowCanvas @ref="canvas" 
            Graph="graph" 
            Height="100vh" 
            Width="100vw">
    <BackgroundContent>
        <FlowBackground class="grid-bg"/>
    </BackgroundContent>
    <Panels>
        <FlowPanels>
            <div class="panel-group">
                <!-- Zoom controls -->
                <button class="panel-btn" title="Zoom In" @onclick="ZoomIn">
                    <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
                        <path d="M8 3V13M3 8H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
                    </svg>
                </button>
                <button class="panel-btn" title="Zoom Out" @onclick="ZoomOut">
                    <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
                        <path d="M3 8H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
                    </svg>
                </button>
                <button class="panel-btn" title="Reset View" @onclick="ResetView">
                    <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
                        <path d="M14 8C14 8 12 4 8 4C4 4 2 8 2 8M8 4V1M8 4L5 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
                    </svg>
                </button>
                <div class="zoom-level">@($"{currentZoom:P0}")</div>
            </div>
        </FlowPanels>
    </Panels>
</FlowCanvas>

<style>
/* Panel Controls */
.panels {
    position: absolute;
    top: 20px;
    right: 20px;
    z-index: 1000;
    pointer-events: none;
}

.panel-group {
    display: flex;
    flex-direction: column;
    gap: 8px;
    background: rgba(30, 30, 30, 0.95);
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 8px;
    padding: 8px;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
    backdrop-filter: blur(10px);
    pointer-events: auto;
}

.panel-btn {
    width: 40px;
    height: 40px;
    background: rgba(60, 60, 60, 0.8);
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 6px;
    color: #ffffff;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-center;
    transition: all 0.2s ease;
    padding: 0;
}

.panel-btn:hover {
    background: rgba(80, 80, 80, 0.9);
    border-color: rgba(255, 255, 255, 0.2);
    transform: scale(1.05);
}

.panel-btn:active {
    transform: scale(0.95);
}

.panel-btn svg {
    pointer-events: none;
}

.zoom-level {
    text-align: center;
    font-size: 12px;
    font-weight: 600;
    color: #ffffff;
    padding: 8px 4px;
    background: rgba(40, 40, 40, 0.8);
    border-radius: 6px;
    border: 1px solid rgba(255, 255, 255, 0.05);
    min-width: 40px;
}
</style>

@code {
    FlowCanvas? canvas;
    FlowGraph graph = new();
    double currentZoom = 1.0;

    async Task ZoomIn()
    {
        if (canvas == null) return;
        
        var props = await canvas.GetViewportPropertiesAsync();
        var newZoom = Math.Min(props.MaxZoom, props.Zoom + 0.1);
        await canvas.SetZoomAsync(newZoom);
        currentZoom = newZoom;
    }
    
    async Task ZoomOut()
    {
        if (canvas == null) return;
        
        var props = await canvas.GetViewportPropertiesAsync();
        var newZoom = Math.Max(props.MinZoom, props.Zoom - 0.1);
        await canvas.SetZoomAsync(newZoom);
        currentZoom = newZoom;
    }
    
    async Task ResetView()
    {
        if (canvas == null) return;
        
        await canvas.SetViewportPropertiesAsync(new FlowState.Models.Serializable.CanvasProperties 
        { 
            Zoom = 1.0, 
            OffsetX = 0, 
            OffsetY = 0,
            MinZoom = canvas.MinZoom,
            MaxZoom = canvas.MaxZoom
        });
        currentZoom = 1.0;
    }
}

Custom Toolbar Example

Create a custom toolbar with various actions:

<Panels>
    <FlowPanels>
        <div class="custom-toolbar">
            <button @onclick="SaveGraph">💾 Save</button>
            <button @onclick="LoadGraph">📂 Load</button>
            <button @onclick="ExecuteGraph">▶️ Run</button>
            <button @onclick="ToggleReadOnly">🔒 Lock</button>
            <button @onclick="UndoAction">↶ Undo</button>
            <button @onclick="RedoAction">↷ Redo</button>
        </div>
    </FlowPanels>
</Panels>

<style>
.custom-toolbar {
    position: absolute;
    top: 10px;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    gap: 8px;
    background: rgba(30, 30, 30, 0.95);
    padding: 8px;
    border-radius: 8px;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
    pointer-events: auto;
}

.custom-toolbar button {
    padding: 8px 16px;
    background: rgba(60, 60, 60, 0.8);
    border: 1px solid rgba(255, 255, 255, 0.1);
    color: white;
    border-radius: 4px;
    cursor: pointer;
    transition: all 0.2s;
}

.custom-toolbar button:hover {
    background: rgba(80, 80, 80, 0.9);
    transform: translateY(-1px);
}
</style>

Minimap Example

Create a minimap panel:

<Panels>
    <FlowPanels>
        <div class="minimap-panel">
            <div class="minimap-title">Minimap</div>
            <div class="minimap-content">
                <!-- Simplified view of your graph -->
                <div class="minimap-viewport"></div>
            </div>
        </div>
    </FlowPanels>
</Panels>

<style>
.minimap-panel {
    position: absolute;
    bottom: 20px;
    right: 20px;
    width: 200px;
    height: 150px;
    background: rgba(30, 30, 30, 0.95);
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 8px;
    overflow: hidden;
    pointer-events: auto;
}

.minimap-title {
    padding: 8px;
    font-size: 12px;
    font-weight: 600;
    color: white;
    background: rgba(20, 20, 20, 0.8);
    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}

.minimap-content {
    position: relative;
    width: 100%;
    height: calc(100% - 32px);
}

.minimap-viewport {
    position: absolute;
    border: 2px solid #7c3aed;
    background: rgba(124, 58, 237, 0.1);
}
</style>

Status Bar Example

Add a status bar at the bottom:

<Panels>
    <FlowPanels>
        <div class="status-bar">
            <div class="status-item">Nodes: @graph.Nodes.Count</div>
            <div class="status-item">Edges: @graph.Edges.Count</div>
            <div class="status-item">Zoom: @($"{currentZoom:P0}")</div>
            <div class="status-item">@(isReadOnly ? "🔒 Read-Only" : "✏️ Editing")</div>
        </div>
    </FlowPanels>
</Panels>

<style>
.status-bar {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    display: flex;
    gap: 16px;
    padding: 8px 16px;
    background: rgba(20, 20, 20, 0.95);
    border-top: 1px solid rgba(255, 255, 255, 0.1);
    font-size: 12px;
    color: #cbd5e1;
    pointer-events: auto;
}

.status-item {
    padding: 4px 8px;
    background: rgba(60, 60, 60, 0.5);
    border-radius: 4px;
}
</style>

Positioning

Panels can be positioned anywhere on the canvas using CSS:

/* Top Right (default for zoom controls) */
.panel-top-right {
    position: absolute;
    top: 20px;
    right: 20px;
}

/* Top Left */
.panel-top-left {
    position: absolute;
    top: 20px;
    left: 20px;
}

/* Bottom Left */
.panel-bottom-left {
    position: absolute;
    bottom: 20px;
    left: 20px;
}

/* Centered Top */
.panel-centered-top {
    position: absolute;
    top: 20px;
    left: 50%;
    transform: translateX(-50%);
}

Important CSS Properties

Always set pointer-events: auto on your panel content so buttons are clickable. The panel container has pointer-events: none by default to allow canvas interaction.

.my-panel {
    pointer-events: auto; /* Make clickable */
    z-index: 1000; /* Above canvas content */
}

Complete Multi-Panel Example

Combine multiple panels:

<Panels>
    <FlowPanels>
        <!-- Zoom Controls (Top Right) -->
        <div class="zoom-panel">
            <button @onclick="ZoomIn">+</button>
            <button @onclick="ZoomOut">-</button>
            <div class="zoom-display">@($"{currentZoom:P0}")</div>
        </div>
        
        <!-- Toolbar (Top Center) -->
        <div class="toolbar">
            <button @onclick="SaveGraph">Save</button>
            <button @onclick="LoadGraph">Load</button>
            <button @onclick="ExecuteGraph">Run</button>
        </div>
        
        <!-- Status (Bottom) -->
        <div class="status">
            Nodes: @graph.Nodes.Count | Edges: @graph.Edges.Count
        </div>
    </FlowPanels>
</Panels>

See Also