Better operations to manipulate layout #759
nzeh
started this conversation in
feature-ideas
Replies: 1 comment
-
I definitely like the I typically work with the root window using horizontal tile layout, and I have a vertical accordion on the left, and a single main window on the right.
Where I can use existing navigation controls to move focus around between these windows. However, sometimes I want to swap M with the topmost window in the accordion. |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
The issue discussed here is related to #5 and possibly other issues. I admit I
didn't search too carefully.
The Issue
i3 and Aerospace both define the layout of a workspace as a tree of windows. i3
has fairly low-level operations to traverse and manipulate this tree. Aerospace
aims to be more "visual". For example, normalization (when turned on) means
that windows that appear to be simply side by side are simply side by side, as
children of a common parent. With i3 (or Aerospace without normalization), the
tree isn't always as easy to see in the actual layout. I like Aerospace's focus on
intuitive manipulation of the layout, a lot, but I also think that its current
list of operations to manipulate the layout tree is too limited.
Scenario 1
Here's a scenario I encountered recently where this becomes an issue: Consider
the following layout:
In my specific use case, the A/B/C stack is an accordion, but I show it as a
tiled stack to better illustrate what's going on. I want to turn this into the
following layout:
Intuitively, this simply means that the A/B/C accordion and the R window remain
where they are, and I want to move M below the A/B/C accordion. I use
normalization, so moving M below the A/B/C accordion means that its orientation
switches from vertical to horizontal.
Since intuitively, M is the only window that moves, this transformation should
be achievable by only moving M around, without touching the other windows. As
far as I can tell, this is not possible using the current operations. The best
I was able to come up with was:
Move M left to become part of the A/B/C accordion:
Move M down:
(Actually, the same layout can be achieved by doing this immediately, without
doing Step 1 first.)
Now focus R and move it right:
Note that this involves moving R, not just M, which has at least two issues:
Intuitively, it's only M that moves when comparing the initial layout to the
final layout, so the transformation should be achievable by moving only M.
More importantly, this example generalizes to scenarios where we have
multiple columns R1, R2, ... instead of a single column R. The above solution
would require moving each of these columns individually (repeating Step 3
above). This is awkward and is actually the situation I ran into.
Scenario 2
The second example of unintuitive behaviour of the current operations is the
following. It can be explained using a variation of the above layout. This
time, all I want to do is to move M into the leftmost column (Step 1 in the above
transformation). If I start with at least two windows in the leftmost column, as in
the above example, then moving M into the left column is achieved using
move left
and, important in this case,join-with left
does nothing. If theleftmost column contains only a single window, then
move left
would moveM
to the left of the leftmost column, resulting in this layout:
and only
join-with left
produces the desired layoutThe behaviour of
move left
kind of makes sense to me, butjoin-with left
should not do nothing when there is more than one window in the left column.
The goal is still to join the moving window with the windows in the leftmost
column, so at the very least,
move left
andjoin-with left
should behave thesame in this case.
I said that the behaviour of
move left
kind of makes sense to me. I think amore consistent behaviour would be:
move left
always swaps the current column with the column to its left, nomatter whether that column to the left contains one or more windows. (I know
this does not address what should happen if we're moving a window that itself
belongs to a column with multiple windows in it. I'm intentionally ignoring
this for now.)
join-with left
always moves the window in the current column into thenext column. (Maybe the command should be renamed to
move-into-next-column
instead of
join-with
in this case.)Conclusion
So the current focus on intuitive operations on the layout tree seems to lead to
unintuitive manipulations of the layout tree in some situations, thereby going
against what appears to be a primary design goal of Aerospace.
Towards a Solution
We need richer operations to manipulate the layout tree. There may be a place
for
focus parent|child
, as discussed in #5, as part of this set of operations.To me, this is a separate issue, so I will ignore this question and will focus
only on making it easy to place a single window into the right spot in the
layout tree with ease. I am sure that the following proposal has flaws, but I
propose it as a starting point.
I propose to have three operations:
swap (left|right|up|down)
(This replaces and mostly behaves like thecurrent
move
).move (left|right|up|down)
(This implements what I calledmove-into-next-column
above).raise (before|after)
(promote
may be a better name?)Swap
The behaviour of
swap left
depends on the layout of the parentu
of thefocused window:
If
u
uses a horizontal layout and the focused window is not its first child,then
swap left
swaps the focused window with its left sibling (even if thisleft sibling is not a window but an internal node of the tree).
If
u
uses a vertical layout or the focused window is its first child, thebehaviour depends on an option passed to
swap left
:swap --raise left
locates the closest ancestorw
ofu
that uses ahorizontal layout. If there is no such ancestor, then we add a new parent
w
of the current root and assign a horizontal layout to it. Then let
v
be theancestor of
u
that is a child ofw
. We make the focused window the leftsibling of
v
.swap --wrap-around left
makes the focused window the last child ofu
ifu
uses a horizontal layout. Ifu
uses a vertical layout, thenswap --wrap-around left
behaves likeswap --raise left
.swap --stop
does nothing.swap right
behaves analogously, only it moves the focused window right in thelist of
u
's children and makes the focused window the right sibling ofv
ifit is
u
's last child oru
uses a vertical layout, and we useswap
withoption
--raise
or--wrap-around
.swap up
andswap down
behave likeswap left
andswap right
but with theroles of horizontal and vertical layouts exchanged.
Among the options accepted by
swap
, I would choose--raise
as the default.It allows certain window movements to be achieved using only
swap
that wouldrequire a combination of
swap
andmove
when using the other two options.The reason why I think the other two options make sense is that they ensure that
the "shape" of the tree never changes and
swap
only ever permutes the childrenof a node. Thus, any change achieved using
swap
is easy to undo. Incontrast,
swap --raise
may make more significant changes to the structure ofthe tree, which then take more effort to undo. So the last two options ensure
fewer surprises, for example when using
swap
once or twice too often.Move
I describe
move left
.move right/move up/move down
behave analogously.move left
finds the closest ancestor nodeu
of the focused window that matchesthe following two conditions:
u
uses a horizontal layout.u
.If no such ancestor exists, then
move left
does nothing. Otherwise, letv
be the child ofu
that is an ancestor of the focused window, and letw
be the left sibling ofv
. Then the focused window becomes the last child ofw
.Raise
raise before
creates a new tree nodeu
with two children. The first child is the currently focused window. The second child is the current parentv
of this window.u
replacesv
as the child ofv
's parent. (In other word,u
gets inserted as a node betweenv
and its parent, and the focused window gets moved up to be the first child ofu
.)raise after
behaves the same asraise before
but the order of the children ofu
is reversed.No Lower
There is no
lower
operation, which would be the inverse ofraise
. Thereason is that an inverse of
raise
should make the focused window a child ofone of its sibling nodes. This works great if we do this immediately after a
raise
operation. In general though, the parent node of the focused window mayhave multiple children. Into which of these children should
lower
move thefocused window? It's not well defined. More importantly,
move
achieves thesame effect and specifies exactly into which sibling the focused window should
move.
Intuition Behind These Operations
The layout tree can be viewed as a hierarchy of nested boxes. Both
swap
andmove
are operations that do not modify this tree.swap
(without--raise
)only permutes the children of the parent of the current window.
move
movesthe focused window from one of these boxes to one of its sibling boxes in the
layout.
There is one exception to this rule:
move
may actually destroy an internalnode of the tree if the moved window had only one sibling before being moved. In
this case, their common parent disappears from the tree. Still, it is helpful
to think about
swap
andmove
as operations that don't change the "shape" of thelayout and simply move windows within layout boxes and between layout boxes,
respectively.
raise
is the operation needed to locally modify the tree "shape". Themove
operation currently implemented by Aerospace also allows such modifications, but
they are not guaranteed to be as local as they should be.
raise
allows tosplit the parent of the current window into two nodes, something that is not
easily achievable even when using the current
split
operation.Revisiting the Introductory Scenario
The transformation given as the motivating example can now be achieved without
ever focusing a window other than M, and using a single
move
and a singleraise
operation.Here's the starting configuration again:
Using a single
move left
operation producesraise after
now splits theA/B/C/M
stack into a node with two children, onebeing the
A/B/C
stack, the other beingM
. Assuming we use normalization,this becomes
I'm happy to add a list of other uses cases to demonstrate that these operations
behave "correctly" (i.e., as one would expect intuitively). I'd like to know
the types of transformations people are concerned about though, before adding
examples to this list.
Beta Was this translation helpful? Give feedback.
All reactions