Key Classes: SelectionManager (355 lines), InteractionHelper, HistoryManager — Key File: DrawingManager.Interaction.cs

SelectionManager

SelectionManager (355 lines) handles all selection-state logic for components, connection lines, and connection points. It works in tandem with DrawingManager to provide a cohesive selection experience:

Selection Operations

OperationMethodBehavior
Single SelectSelect(IDrawableComponent)Clears current selection, selects the given component, fires SelectionChanged.
Add to SelectionAddToSelection(IDrawableComponent)Adds the component to the existing selection without clearing. Triggered by Ctrl+click.
Toggle SelectionToggleSelection(IDrawableComponent)If selected, deselects; if not selected, selects. Triggered by Ctrl+click on an already-selected item.
Rubber-Band SelectSelectInRect(SKRect rect)Selects all components whose bounds intersect the marquee rectangle. Drawn as a dashed rectangle during drag.
Select AllSelectAll()Selects every component on the canvas. Does not select connection lines.
Clear SelectionClearSelection()Deselects everything. Clicking on empty canvas space triggers this.
Select ConnectionSelect(ConnectionLine)Selects a connection line. Only one connection can be selected at a time. Clicking near a line's path triggers this.

Clipboard Operations

OperationMethodBehavior
CopyCopySelectedComponents()Serializes selected components (and their internal connections) to a DiagramDto JSON string held by the manager.
CutCopySelectedComponents() + DeleteSelectedComponents()Copies the selection, then removes it from the canvas. Both steps push onto the undo stack.
PastePasteComponents(SKPoint position)Deserializes the clipboard payload, instantiates new components with an offset, adds them to the canvas, and pushes a PasteComponentsAction onto the undo stack.
DeleteDeleteSelectedComponents()Removes the selection and its attached lines as a single undoable action.

Selection Usage

drawingManager.SelectionManager.SelectComponent(node1);
drawingManager.SelectionManager.AddToSelection(node2);

foreach (var comp in drawingManager.SelectionManager.SelectedComponents)
{
    comp.NodeProperties["StrokeColor"].ParameterCurrentValue = SKColors.DodgerBlue;
    comp.InvalidateVisual();
}

drawingManager.SelectionManager.SelectComponentsInRect(
    new SKRect(100, 100, 500, 400),
    drawingManager.GetComponents());

drawingManager.CopySelectedComponents();
drawingManager.DeleteSelectedComponents();
drawingManager.PasteComponents(new SKPoint(20, 20));

drawingManager.SelectionManager.ClearSelection();
drawingManager.RequestRedraw();

InteractionHelper

InteractionHelper processes raw mouse and keyboard events from the canvas surface and routes them to the appropriate handler based on the current interaction context. It manages a state machine with distinct drag modes:

Drag ModeEnum ValueTriggerBehavior
Component MoveDragMode.ComponentMoveMouse down on a component body, then drag.Repositions the component (and all selected components if part of a multi-select). Snaps to grid if enabled. Fires ComponentMoved on mouse up.
Port ConnectDragMode.PortConnectMouse down on a connection point (port), then drag.Draws a temporary rubber-band line from the port to the cursor. On mouse up over a compatible port, creates a ConnectionLine. Otherwise, cancels.
Marquee SelectDragMode.MarqueeSelectMouse down on empty canvas, then drag.Draws a dashed selection rectangle. On mouse up, selects all components that intersect the rectangle.
Canvas PanDragMode.CanvasPanMouse down with middle button, or Space+drag with left button, then drag.Scrolls the viewport by adjusting PanOffset. The cursor changes to a hand icon.
ResizeDragMode.ResizeMouse down on a resize handle, then drag.Adjusts component Width/Height. Eight handles: N, NE, E, SE, S, SW, W, NW. Snaps to grid if enabled.
NoneDragMode.NoneDefault idle state.Cursor updates based on what is under the mouse (pointer, move, crosshair).

Mouse Event Routing

public void OnMouseDown(SKPoint screenPoint, MouseButton button)
{
    var worldPoint = drawingManager.ScreenToWorld(screenPoint);

    if (button == MouseButton.Middle ||
        (button == MouseButton.Left && IsSpaceHeld))
    {
        _dragMode = DragMode.CanvasPan;
        _dragStart = screenPoint;
        _panStart = drawingManager.PanOffset;
        return;
    }

    var hitResult = HitTestChain(worldPoint);

    switch (hitResult.Type)
    {
        case HitTestType.ConnectionPoint:
            _dragMode = DragMode.PortConnect;
            _connectSource = hitResult.Point;
            break;

        case HitTestType.ResizeHandle:
            _dragMode = DragMode.Resize;
            _resizeHandle = hitResult.Handle;
            _dragStart = worldPoint;
            break;

        case HitTestType.Component:
            _dragMode = DragMode.ComponentMove;
            _dragStart = worldPoint;
            if (!IsCtrlHeld)
                SelectionManager.ClearSelection();
            SelectionManager.Select(hitResult.Component);
            break;

        case HitTestType.ConnectionLine:
            SelectionManager.Select(hitResult.Connection);
            break;

        case HitTestType.None:
            _dragMode = DragMode.MarqueeSelect;
            _marqueeStart = worldPoint;
            SelectionManager.ClearSelection();
            break;
    }
}

Hit-Test Priority Chain

When the user clicks on the canvas, the hit-test chain determines which entity receives the event. The chain is evaluated in strict priority order — the first match wins:

1
Connection Points (Ports)

Highest priority. Each port has a small hit-test radius (typically 8 pixels in screen-space). This ensures users can reliably grab connection ports even on small components.

2
Resize Handles

If the hit misses ports but falls within a resize handle's 6-pixel radius, the resize operation starts. Only active on selected components.

3
Component Bodies

Tests components in reverse Z-order (topmost first). Uses Bounds.Contains(point) for axis-aligned hit testing. Components can override HitTest(SKPoint) for precise path-based hit testing.

4
Connection Lines

Tests against the computed path geometry of each connection line. Uses distance-to-path calculation with a configurable tolerance (default: 6 pixels).

5
Canvas Background

Lowest priority. If nothing else is hit, the canvas background receives the event. Triggers ClearSelection() and starts marquee selection if dragging.

Hit-Test Chain Implementation

private HitTestResult HitTestChain(SKPoint worldPoint)
{
    var portHit = FindConnectionPointAt(worldPoint, PortHitRadius);
    if (portHit != null)
        return HitTestResult.ConnectionPoint(portHit);

    foreach (var comp in GetSelectedComponents())
    {
        var handle = comp.GetResizeHandleAt(worldPoint, ResizeHandleRadius);
        if (handle != ResizeHandle.None)
            return HitTestResult.ResizeHandle(comp, handle);
    }

    for (int i = Components.Count - 1; i >= 0; i--)
    {
        var comp = Components[i];
        if (comp.HitTest(worldPoint))
            return HitTestResult.Component(comp);
    }

    foreach (var conn in Connections)
    {
        if (conn.HitTest(worldPoint, ConnectionHitTolerance))
            return HitTestResult.ConnectionLine(conn);
    }

    return HitTestResult.None;
}

Drag & Drop

Beep.Skia supports two distinct drag-and-drop workflows:

1. Component Palette Drag-Drop

The palette raises ComponentDragEnded with the dragged ComponentItem and the pointer position. The host creates the instance through the registry and converts the screen point to world coordinates using the manager's viewport (PanOffset and Zoom):

Palette-to-Canvas Drag-Drop

palette.ComponentDragEnded += (s, args) =>
{
    if (args.State != ComponentDragState.Ended) return;

    var component = SkiaComponentRegistry.CreateComponentInstance(args.Component.ClassDefinition);
    if (component == null) return;

    // Screen -> world: subtract the pan offset, then divide by zoom
    float zoom = drawingManager.Zoom == 0 ? 1f : drawingManager.Zoom;
    component.X = (args.MousePosition.X - drawingManager.PanOffset.X) / zoom;
    component.Y = (args.MousePosition.Y - drawingManager.PanOffset.Y) / zoom;

    drawingManager.AddComponent(component);   // pushed onto the undo stack
    drawingManager.RequestRedraw();
};

2. Within-Canvas Repositioning

Dragging existing components is handled by InteractionHelper. While a component drag is in progress, IsDragging and DraggingComponent report the state; when the drag ends, the manager raises ComponentDropped with the final canvas and screen positions:

Observing Component Drops

drawingManager.ComponentDropped += (s, e) =>
{
    // e.Component     - the dropped component
    // e.CanvasPosition - final position in world space (e.Location is an alias)
    // e.ScreenPosition - final position in screen space
    // e.Bounds         - final bounds in world space
    Console.WriteLine($"Dropped {e.Component.Name} at {e.CanvasPosition}");
};

// Programmatic move of the current selection (undoable)
drawingManager.MoveSelectedComponents(new SKPoint(dx: 20, dy: 0));

// Undo/redo is exposed directly on the manager
if (drawingManager.CanUndo)
    drawingManager.Undo();

HistoryManager

HistoryManager implements a command-pattern undo/redo system that records every mutation to the diagram state. Each operation is represented as a DrawingAction subclass with Execute() and Undo() methods:

Action ClassDescriptionUndo Behavior
AddComponentActionRecords a component addition.Removes the component from the canvas and disconnects all its connections.
RemoveComponentActionRecords a component removal with all its connections.Re-adds the component and restores all its connections using saved GUIDs.
MoveComponentActionRecords a component position change.Restores the component to its previous X, Y position. Multiple moves during a single drag are coalesced.
ConnectActionRecords a connection creation between two ports.Disconnects the line and unbinds the ports.
DisconnectActionRecords a connection removal.Re-establishes the connection using the saved port GUIDs.
PropertyChangeActionRecords a change to one or more NodeProperties values.Restores the previous property values and invalidates the component.
ResizeComponentActionRecords a change to Width/Height via resize handles.Restores previous Width and Height values.
CompositeActionGroups multiple actions into a single undoable unit.Undoes all child actions in reverse order. Used for multi-select operations.

Undo/Redo Architecture

The history manager maintains two stacks: an undo stack (past actions) and a redo stack (undone actions that can be re-applied). The redo stack is cleared whenever a new action is pushed, following the standard undo/redo model:

HistoryManager Core Logic

public class HistoryManager
{
    private Stack<DrawingAction> _undoStack = new();
    private Stack<DrawingAction> _redoStack = new();
    private const int MaxHistory = 256;

    public bool CanUndo => _undoStack.Count > 0;
    public bool CanRedo => _redoStack.Count > 0;

    public void Push(DrawingAction action)
    {
        _undoStack.Push(action);
        _redoStack.Clear();

        if (_undoStack.Count > MaxHistory)
        {
            var oldest = new Stack<DrawingAction>(
                _undoStack.Take(MaxHistory));
            _undoStack = oldest;
        }
    }

    public void Undo()
    {
        if (!CanUndo) return;
        var action = _undoStack.Pop();
        action.Undo(drawingManager);
        _redoStack.Push(action);
        drawingManager.Invalidate();
    }

    public void Redo()
    {
        if (!CanRedo) return;
        var action = _redoStack.Pop();
        action.Execute(drawingManager);
        _undoStack.Push(action);
        drawingManager.Invalidate();
    }
}

Tip

The CompositeAction is essential for multi-select operations. When a user drags 5 selected components, all 5 positions are recorded in a single composite action — so one Ctrl+Z moves all 5 back, not just one.

Keyboard Shortcuts

The interaction system defines a standard set of keyboard shortcuts that are processed by InteractionHelper before any canvas-level key handling:

ShortcutActionScopeDescription
Ctrl + ZUndoGlobalReverts the most recent diagram mutation. Repeated presses continue undoing through the history stack.
Ctrl + YRedoGlobalRe-applies the most recently undone action. Disabled after any new action is performed.
Ctrl + ASelect AllGlobalSelects every component on the canvas. Connection lines are not selected by Select All.
Delete / BackspaceDelete SelectedSelectionRemoves all selected components and connections. Pushes CompositeAction onto the undo stack.
Ctrl + CCopySelectionCopies selected components and their internal connections to the clipboard as JSON.
Ctrl + XCutSelectionCopies selected components to clipboard, then removes them from the canvas.
Ctrl + VPasteGlobalPastes clipboard contents onto the canvas with a cascading offset from the original positions.
Ctrl + DDuplicateSelectionCopy + paste in one operation. Convenience shortcut for rapidly cloning components.
EscapeCancel / DeselectGlobalCancels the current drag operation (port connect, resize, move) or clears selection if idle.
Ctrl + GToggle GridGlobalToggles ShowGrid on the drawing manager.
Ctrl + + / =Zoom InGlobalIncreases ZoomFactor by one step. Centers zoom on the viewport center.
Ctrl + -Zoom OutGlobalDecreases ZoomFactor by one step. Centers zoom on the viewport center.
Ctrl + 0Reset ZoomGlobalResets ZoomFactor to 1.0 and centers the diagram via ResetView().
Space + DragCanvas PanGlobalHolding Space temporarily switches to canvas pan mode. Left-click drag scrolls the viewport.
Arrow KeysNudge SelectionSelectionMoves selected components by 1 grid unit in the arrow direction. Hold Shift for 10x nudge distance.