Tiling Panes (PanelTree)
PanelTree tiles widgets into a tree of non-overlapping, resizable panes. It is the tiling counterpart to the WindowManager: instead of floating windows that overlap, a PanelTree always partitions the full area, so every pane gets a dedicated slot. Panes can be split, focused, closed, dragged to a new position, docked against another pane's edge, and resized by their dividers — all while still forwarding keyboard and mouse input to their content widgets.
When to use this
Use PanelTree when your UI is a set of panels that should tile rather than float:
IDE-style layouts: an editor next to a file tree and a terminal
Dashboards where each panel owns a fixed, non-overlapping region
Split views the user can rearrange, resize, and grow/shrink at runtime
If you want overlapping, desktop-style windows, use WindowManager instead. If you want a fixed, non-interactive arrangement, prefer the container-based layouts in layout.md.
Basic pieces
A PanelTree owns a tree of panes:
A
PaneLeafholds one piece ofcontentand an optionaltitle.A
PaneSplitarranges child panes along an axis with draggable dividers.
content is anything that renders — a widget with render(w, rect, buf) (and optionally handle_key! / handle_mouse!), or a Model (rendered via view, driven via update!). Construct a tree from a single starting pane:
pt = Tachikoma.PanelTree(my_widget; title="main", chrome=:bars)chrome=:barsdraws a title bar on each pane; the focused pane is marked with a focus ring. Passchrome=:nonefor borderless panes.alivelets the tree prune panes whose content has exited (seeprune!).
Tiling demo
Splitting the focused pane right (s) and down (v), cycling focus (f), then dragging the editor pane's title bar across to dock it on another pane's edge — the drop preview follows the cursor and the layout re-tiles on release:
import Tachikoma
using Match
# Each pane just holds a scrollable list so the splits are easy to tell apart.
_pane(name) = Tachikoma.ScrollPane(["$name · line $i" for i in 1:12]; following=false)
@kwdef mutable struct _PTAnim <: Tachikoma.Model
pt::Tachikoma.PanelTree = Tachikoma.PanelTree(_pane("editor"); title="editor")
n::Int = 1
end
# Keys split / focus / close the panes; anything else falls through to the
# focused pane's own widget.
function Tachikoma.update!(m::_PTAnim, evt::Tachikoma.KeyEvent)
@match (evt.key, evt.char) begin
(:char, 's') => (m.n += 1; Tachikoma.split_pane!(m.pt, _pane("pane $(m.n)"); horizontal=true, title="pane $(m.n)"))
(:char, 'v') => (m.n += 1; Tachikoma.split_pane!(m.pt, _pane("pane $(m.n)"); horizontal=false, title="pane $(m.n)"))
(:char, 'f') => Tachikoma.focus_next!(m.pt)
(:char, 'w') => Tachikoma.close_pane!(m.pt)
_ => Tachikoma.handle_key!(m.pt, evt)
end
end
# Mouse drives move / dock / resize directly.
Tachikoma.update!(m::_PTAnim, evt::Tachikoma.MouseEvent) = Tachikoma.handle_mouse!(m.pt, evt)
Tachikoma.view(m::_PTAnim, f::Tachikoma.Frame) = Tachikoma.render(m.pt, f.area, f.buffer)
Tachikoma.app(_PTAnim())
Commands
PanelTree deliberately has no built-in key bindings — handle_key! forwards straight to the focused pane's content, so the application owns every command. Wire these into your update!:
split_pane!(pt, content; horizontal=true) # split focused pane; true = side-by-side, false = stacked
focus_next!(pt); focus_prev!(pt) # cycle focus between panes
close_pane!(pt) # close the focused pane (never the last one)
focused_pane(pt); focused_content(pt) # the focused leaf / its content
panes(pt); pane_count(pt) # all leaves (left→right, top→bottom) / their countsplit_pane! inserts the new pane beside the focused one and focuses it, then returns the new PaneLeaf. close_pane! collapses the surrounding split so the remaining panes reclaim the space, and refuses to close the final pane.
Mouse: move, dock, and resize
handle_mouse! gives you direct manipulation with no extra wiring:
Move — drag a pane's title bar; a drop preview follows the cursor.
Dock — release over another pane's edge (left/right/top/bottom) to re-split and dock there, or over its center to swap the two panes' contents.
Resize — drag a divider between panes to grow one and shrink its neighbour.
Focus — click any pane to focus it.
is_moving(pt) reports whether a drag is in progress, and cancel_move!(pt) aborts it (e.g. on Escape).
Forwarding input
The integration is the same shape as any container widget — forward events in update! and render in view:
function Tachikoma.update!(m::MyModel, evt::Tachikoma.Event)
evt isa Tachikoma.KeyEvent && Tachikoma.handle_key!(m.pt, evt)
evt isa Tachikoma.MouseEvent && Tachikoma.handle_mouse!(m.pt, evt)
end
function Tachikoma.view(m::MyModel, f::Tachikoma.Frame)
Tachikoma.render(m.pt, f.area, f.buffer)
endContent widgets keep their own key and mouse bindings — PanelTree forwards events to the focused pane transparently.
API summary
PanelTree(content; title="", chrome=:bars, alive=nothing)split_pane!(pt, content; horizontal=true, title="", after=true, at=focused_pane(pt))→PaneLeafclose_pane!(pt, leaf=focused_pane(pt))→Boolfocus_next!(pt),focus_prev!(pt)focused_pane(pt),focused_content(pt)panes(pt),pane_count(pt)prune!(pt)— drop panes whose content has exited (peralive); returnsfalsewhen all are deadis_moving(pt),cancel_move!(pt)handle_key!(pt, evt),handle_mouse!(pt, evt),render(pt, area, buf)