Skip to content

Mouse Router & Drag-and-Drop

Tako's Mouse Router brings the same declarative, zone-based philosophy from the Key Router to mouse and pointer events. Register hitboxes and handlers — Tako does the rest.

Core Concepts

Hitbox Registry

Because Tako is UI-agnostic, the framework does not know where your UI elements are rendered on screen. The solution is a Hitbox Registry: before each mouse event is dispatched, you tell the router the current position and dimensions of each zone:

go
// Call this every render cycle, inside your View() function
app.Mouse().UpdateHitbox("sidebar", x, y, width, height)

When a mouse event arrives, the router performs a hit-test across all registered hitboxes at the current stack level. The first zone whose bounds contain the event coordinates receives the event.

Stack-level Isolation

The Mouse Router mirrors the Key Router's strict isolation: only the topmost stack level's hitboxes are eligible when dispatching a mouse event. If a modal is open (level 1), clicking behind it at coordinates that land in a level-0 zone has no effect.

Stack: ["base", "modal"]  → only level 1 hitboxes receive events
Stack: ["base"]           → level 0 hitboxes receive events

Memory Management

When a layer is popped via app.Stack().Pop() or app.Overlay().Close(), all hitboxes and handlers for that stack level are automatically removed from the registry — no manual cleanup required.


API Reference

app.Mouse() / ctx.Mouse()

Returns a MouseBuilder (from the host app) or ContextMouseBuilder (from a plugin). Both expose the same fluent interface.

Registering Hitboxes

go
// Base level (level 0)
app.Mouse().UpdateHitbox("sidebar", x, y, width, height)

// Explicit stack level (e.g. an overlay at level 1)
app.Mouse().UpdateHitboxAt(1, "modal-btn", x, y, width, height)

Important: Call UpdateHitbox inside your View() function every render cycle. If the terminal is resized or the layout shifts, the hitbox must be refreshed.

Registering Handlers

All handler methods return the ZoneMouseBuilder so you can chain them:

go
app.Mouse().Zone("sidebar").
    OnClick(func(x, y int) {
        // left-click at terminal coordinates (x, y)
    }).
    OnRightClick(func(x, y int) {
        // right-click
    }).
    OnMiddleClick(func(x, y int) {
        // middle button click
    }).
    OnScrollUp(func(x, y int) {
        // mouse wheel up
    }).
    OnScrollDown(func(x, y int) {
        // mouse wheel down
    }).
    OnDragStart(func(x, y int) {
        // drag operation began in this zone
    }).
    OnDrop(func(fromZone string, x, y int) {
        // item dropped here; fromZone is where the drag started
    })

Explicit Stack Level

go
// For a zone inside a modal overlay at stack level 1
app.Mouse().Zone("close-btn", 1).OnClick(func(x, y int) {
    app.Overlay().Close()
})

Drag & Drop

Drag-and-drop is a two-zone interaction:

  1. The source zone registers OnDragStart.
  2. The target zone registers OnDrop.

The router tracks the active drag internally. When the mouse button is released over a target zone, OnDrop(fromZone, x, y) is called with the name of the zone where the drag started.

go
app.Mouse().Zone("file-list").
    OnDragStart(func(x, y int) {
        // remember which item is being dragged
    })

app.Mouse().Zone("trash-bin").
    OnDrop(func(fromZone string, x, y int) {
        if fromZone == "file-list" {
            deleteSelected()
        }
    })

You can check drag state at any time:

go
router := app.Router()
if router.Mouse().DragActive() {
    fmt.Println("dragging from:", router.Mouse().DragSource())
}

Full Example

The demo/layouts/mouse_demo.go file contains a working two-panel demo:

  • Left panel: click to select, right-click to cycle, scroll to navigate, drag to move items.
  • Right panel: accepts drops from the left panel.
go
// In main.go (or a plugin's OnInit)
app.Mouse().Zone("left-panel").
    OnClick(func(x, y int) {
        itemRow := y - headerOffset
        if itemRow >= 0 && itemRow < len(items) {
            selected = itemRow
        }
    }).
    OnScrollUp(func(x, y int) {
        if selected > 0 { selected-- }
    }).
    OnScrollDown(func(x, y int) {
        if selected < len(items)-1 { selected++ }
    }).
    OnDragStart(func(x, y int) {
        // drag tracking is automatic
    })

app.Mouse().Zone("right-panel").
    OnDrop(func(fromZone string, x, y int) {
        if fromZone == "left-panel" {
            move(selected, &leftItems, &rightItems)
        }
    })

// In your View() function — update hitboxes every render
func (l *MyLayout) View(ctx *tako.Context, r *router.Router) tea.View {
    // ... render panels ...
    ctx.Mouse().UpdateHitbox("left-panel", leftX, topY, panelW, panelH)
    ctx.Mouse().UpdateHitbox("right-panel", rightX, topY, panelW, panelH)
    // ...
}

In a Plugin (ctx.Mouse())

Plugins access the same API via the Context:

go
func (p *MyPlugin) OnInit(ctx *tako.Context) {
    ctx.Mouse().Zone("my-zone").OnClick(func(x, y int) {
        ctx.Emit("my-zone:clicked", map[string]int{"x": x, "y": y})
    })
}

// In the plugin's render hook, update the hitbox:
func (p *MyPlugin) render(ctx *tako.Context) string {
    ctx.Mouse().UpdateHitbox("my-zone", p.x, p.y, p.w, p.h)
    return p.content
}

As a MouseComponent

For self-contained components (registered via app.Overlay().ShowComponent(c)), implement the optional contracts.MouseComponent interface:

go
type FilePanel struct{ x, y, w, h int }

func (p *FilePanel) ID() string  { return "files" }
func (p *FilePanel) Render() any { /* ... */ }
func (p *FilePanel) RegisterKeys(km contracts.KeyManager) { /* ... */ }

// Optional: declare mouse handlers
func (p *FilePanel) RegisterMouse(mm contracts.MouseManager) {
    mm.Zone(p.ID()).OnClick(func(x, y int) {
        // select item
    })
    mm.Zone(p.ID()).OnDrop(func(fromZone string, x, y int) {
        // accept drop
    })
}

The overlay manager calls RegisterMouse automatically if the component implements MouseComponent.


Key Behaviours Summary

SituationBehaviour
Click outside all hitboxesDropped (no handler called)
Click on zone with no handlerDropped
OnDrop called without prior dragNot called (DragActive must be true)
Overlay closedAll hitboxes for that level are freed
Multiple hitboxes overlapFirst registered zone wins
ctrl+c while mouse is activeHandled by key router (unchanged)

Up next: 04 — Layer Management