OverlayManager
The OverlayManager is the single high-level facade for all layer management in Tako. Three categories of operations live here: basic overlays, component overlays, and dialog primitives (via the Dialog() sub-namespace).
The Problem It Solves
// ❌ Old way — 3 calls just to show an overlay
ctx.Stack().Push("search")
ctx.Router().Focus(1, "search")
ctx.Hooks().Set("app.overlay.search", func() any { return p.render() })
// And cleanup when closing
ctx.Stack().Pop()
ctx.Router().Focus(0, ctx.Stack().Top())Five coordinated calls, all of which you had to write and keep in sync.
The Solution
// ✅ One call to show
ctx.Overlay().Show("search", func() any { return p.render() })
// One call to close
ctx.Overlay().Close()
// Dismiss everything
ctx.Overlay().CloseAll()Accessing the OverlayManager
| Location | How to access |
|---|---|
Host app (main.go) | app.Overlay() |
Plugin (OnInit, etc.) | ctx.Overlay() |
| IoC container | app.Make(new(contracts.OverlayManager)) |
API Reference
Basic Overlays
Show(layerID string, render func() any)
Pushes layerID onto the focus stack, registers render as the hook provider under "tako.overlay.<layerID>", and shifts keyboard focus to the new layer.
- Idempotent: calling
ShowwhenlayerIDis already the top layer is a no-op. - Emits:
"overlay:shown"on the EventBus withlayerIDas data.
ctx.Overlay().Show("help", func() any {
return p.renderHelpView() // return any — string, struct, etc.
})Your layout reads the render output via the hook:
// In your Layout.View():
content := ctx.Hooks().Get("tako.overlay.help")Close()
Pops the topmost overlay and reverts focus to the layer below.
- Safe at base layer: no-op if only the base layer is active.
- Emits:
"overlay:closed"with the closed layer ID.
CloseAll()
Pops every overlay down to (but not including) the base layer. Emits "overlay:closed" for each.
Top() string
Returns the ID of the currently active overlay, or "" at the base layer.
IsActive() bool
Reports whether any overlay is currently shown. Sugar for Top() != "".
Component Overlays
Register(c Component)
Wires the component's render hook and keybindings without pushing to the stack. For pre-registration before a layer becomes active. Prefer ShowComponent for the common case.
ShowComponent(c Component)
Wires the component (hook + keybindings) and immediately pushes it onto the overlay stack.
ctx.Keys().Bind("ctrl+f", func() {
if !ctx.Overlay().IsActive() {
ctx.Overlay().ShowComponent(&SearchBox{})
}
})See 04.03 — Component Interface for how to define a Component.
Dialog Sub-Namespace
Dialog() DialogService
Returns the DialogService for this overlay manager — the sub-namespace for built-in interaction primitives.
Dialogs are overlays: they push onto the same stack and are dismissed the same way. Grouping them under .Dialog() makes that relationship explicit, while keeping OverlayManager's interface clean as new dialog types are added.
// Single call
app.Overlay().Dialog().Confirm("Delete?", func(yes bool) { ... })
// Cached for multiple calls
d := ctx.Overlay().Dialog()
d.Confirm("Step 1?", cb1)See 04.04 — DialogService for the full dialog API.
Hook Namespace
Render hooks are stored under:
"tako.overlay.<layerID>"Your layout should consume hooks under this namespace. The older "app.overlay.*" namespace from pre-DX code is unaffected — you can migrate incrementally:
// Layout supporting both namespaces during migration
top := r.Stack().Top()
content := ctx.Hooks().Get("tako.overlay." + top)
if content == nil {
content = ctx.Hooks().Get("app.overlay." + top) // legacy fallback
}Events Emitted
| Event | Data | When |
|---|---|---|
"overlay:shown" | layerID (string) | After a successful Show() or ShowComponent() |
"overlay:closed" | layerID (string) | After each successful Close() |
Complete Plugin Example
Before (manual coordination)
func (p *Plugin) OnInit(ctx *tako.Context) error {
ctx.Hooks().Set("app.overlay.fzf", func() any {
if !p.isActive { return nil }
return p.renderFZF()
})
ctx.Keys().Bind("ctrl+f", func() {
if p.isActive { return }
p.isActive = true
ctx.Stack().Push("fzf")
ctx.Router().Focus(1, "fzf")
ctx.Emit("fzf:opened", nil)
})
ctx.On("fzf:closed", func(e contracts.Event) {
p.isActive = false
})
return nil
}After (OverlayManager)
func (p *Plugin) OnInit(ctx *tako.Context) error {
ctx.Keys().Bind("ctrl+f", func() {
if ctx.Overlay().IsActive() { return }
ctx.Overlay().Show("fzf", p.renderFZF)
ctx.Emit("fzf:opened", nil)
})
return nil
}The isActive field is gone — the overlay manager's state is the single source of truth.
Overlay Guards Still Work
OverlayManager.Show() calls Router.PushLayer(), which calls Stack.Push(). All guards registered with BeforePush still fire.
Up next: 04.03 — Component Interface — bundle a view and its keybindings into a single reusable struct.
