Multiple terminals per tab, rearrangeable by drag or keyboard

A tab is now a view holding an ordered list of panes rather than a single
terminal. New panes open side by side; moving one against a top or bottom
edge restacks the view, and against a side edge returns it to a row.

Layout is a flat list with one orientation per view rather than a split
tree. That covers a shell beside an agent without the structure a tree
needs, and it can be replaced once mixed layouts actually matter.

Panes live in a GtkBox and are reordered in place, so rearranging never
unparents a terminal and running processes and scrollback survive the move.

Dragging a pane uses its header as the handle so it never competes with the
terminal's own mouse handling, and drops go through the same moveRelative
path as the Ctrl+Shift+arrow shortcuts.

Also fixes an ordering bug this surfaced: View.create used to add its first
pane immediately, firing the title callback into a Tab that had not been
initialized yet. The view is now created empty and the caller adds the pane
once it has finished wiring itself up.
This commit is contained in:
Greyson Parrelli
2026-08-11 10:59:21 -04:00
parent 74bc9e8fd3
commit 5b00c98e03
6 changed files with 768 additions and 63 deletions
+58 -1
View File
@@ -60,7 +60,64 @@
}
.vtabs-content {
background-color: #0f0d14;
}
/* A view is the container for one tab's terminals. */
.vtabs-view {
padding: 6px 6px 6px 0;
}
/* Each terminal sits in its own rounded frame so multiple panes in a view
read as distinct surfaces. */
.vtabs-pane {
background-color: #16141c;
border-radius: 10px;
margin: 6px 6px 6px 0;
border: 1px solid #2a2536;
}
.vtabs-pane.active {
border-color: #5b4d80;
}
.vtabs-pane-header {
padding: 2px 4px 2px 10px;
background-color: #1c1926;
border-bottom: 1px solid #262133;
}
.vtabs-pane.active .vtabs-pane-header {
background-color: #241f33;
}
.vtabs-pane-title {
font-size: 0.82em;
color: #8f87a3;
}
.vtabs-pane.active .vtabs-pane-title {
color: #ded7ef;
}
.vtabs-pane-button {
min-width: 22px;
min-height: 22px;
padding: 0;
opacity: 0;
}
.vtabs-pane:hover .vtabs-pane-button,
.vtabs-pane.active .vtabs-pane-button {
opacity: 0.55;
}
.vtabs-pane-button:hover {
opacity: 1;
}
/* Drop indicator: an inset bar on the edge the pane would land against, so
the preview also tells you which way the view is about to be arranged. */
.vtabs-pane.drop-left { box-shadow: inset 4px 0 0 0 #b29df5; }
.vtabs-pane.drop-right { box-shadow: inset -4px 0 0 0 #b29df5; }
.vtabs-pane.drop-top { box-shadow: inset 0 4px 0 0 #b29df5; }
.vtabs-pane.drop-bottom { box-shadow: inset 0 -4px 0 0 #b29df5; }