From de94f6da1a9f7d2b9701035b844154658d9d308f Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Sat, 4 Aug 2012 03:04:00 +0200 Subject: [PATCH] Introduce splith/splitv layouts, remove orientation MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit With this commit, the "default" layout is replaced by the splith and splitv layouts. splith is equivalent to default with orientation horizontal and splitv is equivalent to default with orientation vertical. The "split h" and "split v" commands continue to work as before, they split the current container and you will end up in a split container with layout splith (after "split h") or splitv (after "split v"). To change a splith container into a splitv container, use either "layout splitv" or "layout toggle split". The latter command is used in the default config as mod+l (previously "layout default"). In case you have "layout default" in your config file, it is recommended to just replace it by "layout toggle split", which will work as "layout default" did before when pressing it once, but toggle between horizontal/vertical when pressing it repeatedly. The rationale behind this commit is that it’s cleaner to have all parameters that influence how windows are rendered in the layout itself rather than having a special parameter in combination with only one layout. This enables us to change existing split containers in all cases without breaking existing features (see ticket #464). Also, users should feel more confident about whether they are actually splitting or just changing an existing split container now. As a nice side-effect, this commit brings back the "layout toggle" feature we once had in i3 version 3 (see the userguide). AFAIK, it is safe to use in-place restart to upgrade into versions after this commit (switching to an older version will break your layout, though). Fixes #464 --- docs/ipc | 7 +- docs/userguide | 66 +++++++++++------- i3.config | 4 +- i3.config.keycodes | 4 +- include/commands.h | 8 ++- include/con.h | 9 +++ include/data.h | 13 +++- parser-specs/commands.spec | 14 +++- src/click.c | 6 +- src/commands.c | 56 ++++++++++++--- src/con.c | 129 +++++++++++++++++++++++++++-------- src/floating.c | 7 +- src/ipc.c | 37 +++++++--- src/load_layout.c | 45 +++++++++--- src/randr.c | 11 ++- src/render.c | 12 ++-- src/tree.c | 28 +++++--- src/workspace.c | 62 ++++++++--------- testcases/t/116-nestedcons.t | 4 +- testcases/t/122-split.t | 10 +-- testcases/t/145-flattening.t | 2 +- testcases/t/192-layout.t | 84 +++++++++++++++++++++++ 22 files changed, 460 insertions(+), 158 deletions(-) create mode 100644 testcases/t/192-layout.t diff --git a/docs/ipc b/docs/ipc index 90718405..525e9968 100644 --- a/docs/ipc +++ b/docs/ipc @@ -1,7 +1,7 @@ IPC interface (interprocess communication) ========================================== Michael Stapelberg -July 2012 +August 2012 This document describes how to interface with i3 from a separate process. This is useful for example to remote-control i3 (to write test cases for example) or @@ -270,12 +270,15 @@ border (string):: Can be either "normal", "none" or "1pixel", dependending on the container’s border style. layout (string):: - Can be either "default", "stacked", "tabbed", "dockarea" or "output". + Can be either "splith", "splitv", "stacked", "tabbed", "dockarea" or + "output". Other values might be possible in the future, should we add new layouts. orientation (string):: Can be either "none" (for non-split containers), "horizontal" or "vertical". + THIS FIELD IS OBSOLETE. It is still present, but your code should not + use it. Instead, rely on the layout field. percent (float):: The percentage which this container takes in its parent. A value of +null+ means that the percent property does not make sense for this diff --git a/docs/userguide b/docs/userguide index 1545ac08..64063831 100644 --- a/docs/userguide +++ b/docs/userguide @@ -1,7 +1,7 @@ i3 User’s Guide =============== -Michael Stapelberg -April 2012 +Michael Stapelberg +August 2012 This document contains all the information you need to configure and use the i3 window manager. If it does not, please contact us on IRC (preferred) or post your @@ -68,9 +68,11 @@ To split a window vertically, press +mod+v+. To split it horizontally, press A split container can have one of the following layouts: -default:: +splith/splitv:: Windows are sized so that every window gets an equal amount of space in the -container. +container. splith distributes the windows horizontally (windows are right next +to each other), splitv distributes them vertically (windows are on top of each +other). stacking:: Only the focused window in the container is displayed. You get a list of windows at the top of the container. @@ -78,8 +80,8 @@ tabbed:: The same principle as +stacking+, but the list of windows at the top is only a single line which is vertically split. -To switch modes, press +mod+e+ for default, +mod+s+ for stacking and -+mod+w+ for tabbed. +To switch modes, press +mod+e+ for splith/splitv (it toggles), +mod+s+ for +stacking and +mod+w+ for tabbed. image:modes.png[Container modes] @@ -196,20 +198,21 @@ image::tree-shot4.png["shot4",title="Two terminals on standard workspace"] It is only natural to use so-called +Split Containers+ in order to build a layout when using a tree as data structure. In i3, every +Container+ has an -orientation (horizontal, vertical or unspecified). So, in our example with the -workspace, the default orientation of the workspace +Container+ is horizontal -(most monitors are widescreen nowadays). If you change the orientation to -vertical (+mod+v+ in the default config) and *then* open two terminals, i3 will -configure your windows like this: +orientation (horizontal, vertical or unspecified) and the orientation depends +on the layout the container is in (vertical for splitv and stacking, horizontal +for splith and tabbed). So, in our example with the workspace, the default +layout of the workspace +Container+ is splith (most monitors are widescreen +nowadays). If you change the layout to splitv (+mod+l+ in the default config) +and *then* open two terminals, i3 will configure your windows like this: image::tree-shot2.png["shot2",title="Vertical Workspace Orientation"] -An interesting new feature of the tree branch is the ability to split anything: -Let’s assume you have two terminals on a workspace (with horizontal -orientation), focus is on the right terminal. Now you want to open another -terminal window below the current one. If you would just open a new terminal -window, it would show up to the right due to the horizontal workspace -orientation. Instead, press +mod+v+ to create a +Vertical Split Container+ (to +An interesting new feature of i3 since version 4 is the ability to split anything: +Let’s assume you have two terminals on a workspace (with splith layout, that is +horizontal orientation), focus is on the right terminal. Now you want to open +another terminal window below the current one. If you would just open a new +terminal window, it would show up to the right due to the splith layout. +Instead, press +mod+v+ to split the container with the splitv layout (to open a +Horizontal Split Container+, use +mod+h+). Now you can open a new terminal and it will open below the current one: @@ -1190,13 +1193,15 @@ cursor for 60 seconds. === Splitting containers The split command makes the current window a split container. Split containers -can contain multiple windows. Every split container has an orientation, it is -either split horizontally (a new window gets placed to the right of the current -one) or vertically (a new window gets placed below the current one). +can contain multiple windows. Depending on the layout of the split container, +new windows get placed to the right of the current one (splith) or new windows +get placed below the current one (splitv). If you apply this command to a split container with the same orientation, nothing will happen. If you use a different orientation, the split container’s -orientation will be changed (if it does not have more than one window). +orientation will be changed (if it does not have more than one window). Use ++layout toggle split+ to change the layout of any split container from splitv +to splith or vice-versa. *Syntax*: --------------------------- @@ -1211,19 +1216,32 @@ bindsym mod+h split horizontal === Manipulating layout -Use +layout default+, +layout stacking+ or +layout tabbed+ to change the -current container layout to default, stacking or tabbed layout, respectively. +Use +layout toggle split+, +layout stacking+ or +layout tabbed+ to change the +current container layout to splith/splitv, stacking or tabbed layout, +respectively. To make the current window (!) fullscreen, use +fullscreen+, to make it floating (or tiling again) use +floating enable+ respectively +floating disable+ (or +floating toggle+): +*Syntax*: +-------------- +layout +layout toggle [split|all] +-------------- + *Examples*: -------------- bindsym mod+s layout stacking -bindsym mod+l layout default +bindsym mod+l layout toggle split bindsym mod+w layout tabbed +# Toggle between stacking/tabbed/split: +bindsym mod+x layout toggle + +# Toggle between stacking/tabbed/splith/splitv: +bindsym mod+x layout toggle all + # Toggle fullscreen bindsym mod+f fullscreen diff --git a/i3.config b/i3.config index 3225b973..d29155ed 100644 --- a/i3.config +++ b/i3.config @@ -57,10 +57,10 @@ bindsym Mod1+v split v # enter fullscreen mode for the focused container bindsym Mod1+f fullscreen -# change container layout (stacked, tabbed, default) +# change container layout (stacked, tabbed, toggle split) bindsym Mod1+s layout stacking bindsym Mod1+w layout tabbed -bindsym Mod1+e layout default +bindsym Mod1+e layout toggle split # toggle tiling / floating bindsym Mod1+Shift+space floating toggle diff --git a/i3.config.keycodes b/i3.config.keycodes index 2f014183..1bff8890 100644 --- a/i3.config.keycodes +++ b/i3.config.keycodes @@ -58,10 +58,10 @@ bindcode $mod+55 split v # enter fullscreen mode for the focused container bindcode $mod+41 fullscreen -# change container layout (stacked, tabbed, default) +# change container layout (stacked, tabbed, toggle split) bindcode $mod+39 layout stacking bindcode $mod+25 layout tabbed -bindcode $mod+26 layout default +bindcode $mod+26 layout toggle split # toggle tiling / floating bindcode $mod+Shift+65 floating toggle diff --git a/include/commands.h b/include/commands.h index 85057d19..37ee98d9 100644 --- a/include/commands.h +++ b/include/commands.h @@ -200,11 +200,17 @@ void cmd_fullscreen(I3_CMD, char *fullscreen_mode); void cmd_move_direction(I3_CMD, char *direction, char *move_px); /** - * Implementation of 'layout default|stacked|stacking|tabbed'. + * Implementation of 'layout default|stacked|stacking|tabbed|splitv|splith'. * */ void cmd_layout(I3_CMD, char *layout_str); +/** + * Implementation of 'layout toggle [all|split]'. + * + */ +void cmd_layout_toggle(I3_CMD, char *toggle_mode); + /** * Implementaiton of 'exit'. * diff --git a/include/con.h b/include/con.h index 95726147..1965da7c 100644 --- a/include/con.h +++ b/include/con.h @@ -248,6 +248,15 @@ void con_set_border_style(Con *con, int border_style); */ void con_set_layout(Con *con, int layout); +/** + * This function toggles the layout of a given container. toggle_mode can be + * either 'default' (toggle only between stacked/tabbed/last_split_layout), + * 'split' (toggle only between splitv/splith) or 'all' (toggle between all + * layouts). + * + */ +void con_toggle_layout(Con *con, const char *toggle_mode); + /** * Determines the minimum size of the given con by looking at its children (for * split/stacked/tabbed cons). Will be called when resizing floating cons diff --git a/include/data.h b/include/data.h index f4ed9a3e..d28f1756 100644 --- a/include/data.h +++ b/include/data.h @@ -423,6 +423,8 @@ struct Assignment { */ struct Con { bool mapped; + /** whether this is a split container or not */ + bool split; enum { CT_ROOT = 0, CT_OUTPUT = 1, @@ -431,7 +433,6 @@ struct Con { CT_WORKSPACE = 4, CT_DOCKAREA = 5 } type; - orientation_t orientation; struct Con *parent; struct Rect rect; @@ -496,7 +497,15 @@ struct Con { TAILQ_HEAD(swallow_head, Match) swallow_head; enum { CF_NONE = 0, CF_OUTPUT = 1, CF_GLOBAL = 2 } fullscreen_mode; - enum { L_DEFAULT = 0, L_STACKED = 1, L_TABBED = 2, L_DOCKAREA = 3, L_OUTPUT = 4 } layout; + enum { + L_DEFAULT = 0, + L_STACKED = 1, + L_TABBED = 2, + L_DOCKAREA = 3, + L_OUTPUT = 4, + L_SPLITV = 5, + L_SPLITH = 6 + } layout, last_split_layout; border_style_t border_style; /** floating? (= not in tiling layout) This cannot be simply a bool * because we want to keep track of whether the status was set by the diff --git a/parser-specs/commands.spec b/parser-specs/commands.spec index b416d968..b4c9e005 100644 --- a/parser-specs/commands.spec +++ b/parser-specs/commands.spec @@ -66,10 +66,20 @@ state BORDER: border_style = 'normal', 'none', '1pixel', 'toggle' -> call cmd_border($border_style) -# layout default|stacked|stacking|tabbed +# layout default|stacked|stacking|tabbed|splitv|splith +# layout toggle [split|all] state LAYOUT: - layout_mode = 'default', 'stacked', 'stacking', 'tabbed' + layout_mode = 'default', 'stacked', 'stacking', 'tabbed', 'splitv', 'splith' -> call cmd_layout($layout_mode) + 'toggle' + -> LAYOUT_TOGGLE + +# layout toggle [split|all] +state LAYOUT_TOGGLE: + end + -> call cmd_layout_toggle($toggle_mode) + toggle_mode = 'split', 'all' + -> call cmd_layout_toggle($toggle_mode) # append_layout state APPEND_LAYOUT: diff --git a/src/click.c b/src/click.c index ca2a1037..9f0549fa 100644 --- a/src/click.c +++ b/src/click.c @@ -35,13 +35,13 @@ static bool tiling_resize_for_border(Con *con, border_t border, xcb_button_press Con *resize_con = con; while (resize_con->type != CT_WORKSPACE && resize_con->type != CT_FLOATING_CON && - resize_con->parent->orientation != orientation) + con_orientation(resize_con->parent) != orientation) resize_con = resize_con->parent; DLOG("resize_con = %p\n", resize_con); if (resize_con->type != CT_WORKSPACE && resize_con->type != CT_FLOATING_CON && - resize_con->parent->orientation == orientation) { + con_orientation(resize_con->parent) == orientation) { first = resize_con; second = (way == 'n') ? TAILQ_NEXT(first, nodes) : TAILQ_PREV(first, nodes_head, nodes); if (second == TAILQ_END(&(first->nodes_head))) { @@ -145,7 +145,7 @@ static bool tiling_resize(Con *con, xcb_button_press_event_t *event, const click if ((check_con->layout == L_STACKED || check_con->layout == L_TABBED || - check_con->orientation == HORIZ) && + con_orientation(check_con) == HORIZ) && con_num_children(check_con) > 1) { DLOG("Not handling this resize, this container has > 1 child.\n"); return false; diff --git a/src/commands.c b/src/commands.c index c0681d26..a1ef3bbf 100644 --- a/src/commands.c +++ b/src/commands.c @@ -530,7 +530,7 @@ static bool cmd_resize_tiling_direction(I3_CMD, char *way, char *direction, int (strcmp(direction, "left") == 0 || strcmp(direction, "right") == 0 ? HORIZ : VERT); do { - if (current->parent->orientation != search_orientation) { + if (con_orientation(current->parent) != search_orientation) { current = current->parent; continue; } @@ -541,7 +541,7 @@ static bool cmd_resize_tiling_direction(I3_CMD, char *way, char *direction, int percentage = 1.0 / children; LOG("default percentage = %f\n", percentage); - orientation_t orientation = current->parent->orientation; + orientation_t orientation = con_orientation(current->parent); if ((orientation == HORIZ && (strcmp(direction, "up") == 0 || strcmp(direction, "down") == 0)) || @@ -612,7 +612,7 @@ static bool cmd_resize_tiling_width_height(I3_CMD, char *way, char *direction, i while (current->type != CT_WORKSPACE && current->type != CT_FLOATING_CON && - current->parent->orientation != search_orientation) + con_orientation(current->parent) != search_orientation) current = current->parent; /* get the default percentage */ @@ -621,7 +621,7 @@ static bool cmd_resize_tiling_width_height(I3_CMD, char *way, char *direction, i double percentage = 1.0 / children; LOG("default percentage = %f\n", percentage); - orientation_t orientation = current->parent->orientation; + orientation_t orientation = con_orientation(current->parent); if ((orientation == HORIZ && strcmp(direction, "height") == 0) || @@ -1397,17 +1397,27 @@ void cmd_move_direction(I3_CMD, char *direction, char *move_px) { } /* - * Implementation of 'layout default|stacked|stacking|tabbed'. + * Implementation of 'layout default|stacked|stacking|tabbed|splitv|splith'. * */ void cmd_layout(I3_CMD, char *layout_str) { if (strcmp(layout_str, "stacking") == 0) layout_str = "stacked"; - DLOG("changing layout to %s\n", layout_str); owindow *current; - int layout = (strcmp(layout_str, "default") == 0 ? L_DEFAULT : - (strcmp(layout_str, "stacked") == 0 ? L_STACKED : - L_TABBED)); + int layout; + /* default is a special case which will be handled in con_set_layout(). */ + if (strcmp(layout_str, "default") == 0) + layout = L_DEFAULT; + else if (strcmp(layout_str, "stacked") == 0) + layout = L_STACKED; + else if (strcmp(layout_str, "tabbed") == 0) + layout = L_TABBED; + else if (strcmp(layout_str, "splitv") == 0) + layout = L_SPLITV; + else if (strcmp(layout_str, "splith") == 0) + layout = L_SPLITH; + + DLOG("changing layout to %s (%d)\n", layout_str, layout); /* check if the match is empty, not if the result is empty */ if (match_is_empty(current_match)) @@ -1424,6 +1434,33 @@ void cmd_layout(I3_CMD, char *layout_str) { ysuccess(true); } +/* + * Implementation of 'layout toggle [all|split]'. + * + */ +void cmd_layout_toggle(I3_CMD, char *toggle_mode) { + owindow *current; + + if (toggle_mode == NULL) + toggle_mode = "default"; + + DLOG("toggling layout (mode = %s)\n", toggle_mode); + + /* check if the match is empty, not if the result is empty */ + if (match_is_empty(current_match)) + con_toggle_layout(focused->parent, toggle_mode); + else { + TAILQ_FOREACH(current, &owindows, owindows) { + DLOG("matching: %p / %s\n", current->con, current->con->name); + con_toggle_layout(current->con, toggle_mode); + } + } + + cmd_output->needs_tree_render = true; + // XXX: default reply for now, make this a better reply + ysuccess(true); +} + /* * Implementaiton of 'exit'. * @@ -1472,6 +1509,7 @@ void cmd_restart(I3_CMD) { void cmd_open(I3_CMD) { LOG("opening new container\n"); Con *con = tree_open_con(NULL, NULL); + con->layout = L_SPLITH; con_focus(con); y(map_open); diff --git a/src/con.c b/src/con.c index f804a204..ccdd5682 100644 --- a/src/con.c +++ b/src/con.c @@ -217,8 +217,8 @@ bool con_accepts_window(Con *con) { if (con->type == CT_WORKSPACE) return false; - if (con->orientation != NO_ORIENTATION) { - DLOG("container %p does not accepts windows, orientation != NO_ORIENTATION\n", con); + if (con->split) { + DLOG("container %p does not accept windows, it is a split container.\n", con); return false; } @@ -265,8 +265,11 @@ Con *con_parent_with_orientation(Con *con, orientation_t orientation) { while (con_orientation(parent) != orientation) { DLOG("Need to go one level further up\n"); parent = parent->parent; - /* Abort when we reach a floating con */ - if (parent && parent->type == CT_FLOATING_CON) + /* Abort when we reach a floating con, or an output con */ + if (parent && + (parent->type == CT_FLOATING_CON || + parent->type == CT_OUTPUT || + (parent->parent && parent->parent->type == CT_OUTPUT))) parent = NULL; if (parent == NULL) break; @@ -697,14 +700,32 @@ void con_move_to_workspace(Con *con, Con *workspace, bool fix_coordinates, bool * */ int con_orientation(Con *con) { - /* stacking containers behave like they are in vertical orientation */ - if (con->layout == L_STACKED) - return VERT; - - if (con->layout == L_TABBED) - return HORIZ; - - return con->orientation; + switch (con->layout) { + case L_SPLITV: + /* stacking containers behave like they are in vertical orientation */ + case L_STACKED: + return VERT; + + case L_SPLITH: + /* tabbed containers behave like they are in vertical orientation */ + case L_TABBED: + return HORIZ; + + case L_DEFAULT: + DLOG("Someone called con_orientation() on a con with L_DEFAULT, this is a bug in the code.\n"); + assert(false); + return HORIZ; + + case L_DOCKAREA: + case L_OUTPUT: + DLOG("con_orientation() called on dockarea/output (%d) container %p\n", con->layout, con); + assert(false); + return HORIZ; + + default: + DLOG("con_orientation() ran into default\n"); + assert(false); + } } /* @@ -1017,23 +1038,16 @@ void con_set_layout(Con *con, int layout) { Con *new = con_new(NULL, NULL); new->parent = con; - /* 2: set the requested layout on the split con */ + /* 2: Set the requested layout on the split container and mark it as + * split. */ new->layout = layout; - - /* 3: While the layout is irrelevant in stacked/tabbed mode, it needs - * to be set. Otherwise, this con will not be interpreted as a split - * container. */ - if (config.default_orientation == NO_ORIENTATION) { - new->orientation = (con->rect.height > con->rect.width) ? VERT : HORIZ; - } else { - new->orientation = config.default_orientation; - } + new->split = true; Con *old_focused = TAILQ_FIRST(&(con->focus_head)); if (old_focused == TAILQ_END(&(con->focus_head))) old_focused = NULL; - /* 4: move the existing cons of this workspace below the new con */ + /* 3: move the existing cons of this workspace below the new con */ DLOG("Moving cons\n"); Con *child; while (!TAILQ_EMPTY(&(con->nodes_head))) { @@ -1054,7 +1068,66 @@ void con_set_layout(Con *con, int layout) { return; } - con->layout = layout; + if (layout == L_DEFAULT) { + /* Special case: the layout formerly known as "default" (in combination + * with an orientation). Since we switched to splith/splitv layouts, + * using the "default" layout (which "only" should happen when using + * legacy configs) is using the last split layout (either splith or + * splitv) in order to still do the same thing. + * + * Starting from v4.6 though, we will nag users about using "layout + * default", and in v4.9 we will remove it entirely (with an + * appropriate i3-migrate-config mechanism). */ + con->layout = con->last_split_layout; + } else { + /* We fill in last_split_layout when switching to a different layout + * since there are many places in the code that don’t use + * con_set_layout(). */ + if (con->layout == L_SPLITH || con->layout == L_SPLITV) + con->last_split_layout = con->layout; + con->layout = layout; + } +} + +/* + * This function toggles the layout of a given container. toggle_mode can be + * either 'default' (toggle only between stacked/tabbed/last_split_layout), + * 'split' (toggle only between splitv/splith) or 'all' (toggle between all + * layouts). + * + */ +void con_toggle_layout(Con *con, const char *toggle_mode) { + if (strcmp(toggle_mode, "split") == 0) { + /* Toggle between splits. When the current layout is not a split + * layout, we just switch back to last_split_layout. Otherwise, we + * change to the opposite split layout. */ + if (con->layout != L_SPLITH && con->layout != L_SPLITV) + con_set_layout(con, con->last_split_layout); + else { + if (con->layout == L_SPLITH) + con_set_layout(con, L_SPLITV); + else con_set_layout(con, L_SPLITH); + } + } else { + if (con->layout == L_STACKED) + con_set_layout(con, L_TABBED); + else if (con->layout == L_TABBED) { + if (strcmp(toggle_mode, "all") == 0) + con_set_layout(con, L_SPLITH); + else con_set_layout(con, con->last_split_layout); + } else if (con->layout == L_SPLITH || con->layout == L_SPLITV) { + if (strcmp(toggle_mode, "all") == 0) { + /* When toggling through all modes, we toggle between + * splith/splitv, whereas normally we just directly jump to + * stacked. */ + if (con->layout == L_SPLITH) + con_set_layout(con, L_SPLITV); + else con_set_layout(con, L_STACKED); + } else { + con_set_layout(con, L_STACKED); + } + } + } } /* @@ -1131,12 +1204,12 @@ Rect con_minimum_size(Con *con) { /* For horizontal/vertical split containers we sum up the width (h-split) * or height (v-split) and use the maximum of the height (h-split) or width * (v-split) as minimum size. */ - if (con->orientation == HORIZ || con->orientation == VERT) { + if (con->split) { uint32_t width = 0, height = 0; Con *child; TAILQ_FOREACH(child, &(con->nodes_head), nodes) { Rect min = con_minimum_size(child); - if (con->orientation == HORIZ) { + if (con->layout == L_SPLITH) { width += min.width; height = max(height, min.height); } else { @@ -1148,8 +1221,8 @@ Rect con_minimum_size(Con *con) { return (Rect){ 0, 0, width, height }; } - ELOG("Unhandled case, type = %d, layout = %d, orientation = %d\n", - con->type, con->layout, con->orientation); + ELOG("Unhandled case, type = %d, layout = %d, split = %d\n", + con->type, con->layout, con->split); assert(false); } diff --git a/src/floating.c b/src/floating.c index c0154aca..90c4d560 100644 --- a/src/floating.c +++ b/src/floating.c @@ -40,7 +40,7 @@ void floating_enable(Con *con, bool automatic) { } /* 1: If the container is a workspace container, we need to create a new - * split-container with the same orientation and make that one floating. We + * split-container with the same layout and make that one floating. We * cannot touch the workspace container itself because floating containers * are children of the workspace. */ if (con->type == CT_WORKSPACE) { @@ -52,7 +52,7 @@ void floating_enable(Con *con, bool automatic) { /* TODO: refactor this with src/con.c:con_set_layout */ Con *new = con_new(NULL, NULL); new->parent = con; - new->orientation = con->orientation; + new->layout = con->layout; /* since the new container will be set into floating mode directly * afterwards, we need to copy the workspace rect. */ @@ -97,8 +97,9 @@ void floating_enable(Con *con, bool automatic) { * otherwise. */ Con *ws = con_get_workspace(con); nc->parent = ws; - nc->orientation = NO_ORIENTATION; + nc->split = true; nc->type = CT_FLOATING_CON; + nc->layout = L_SPLITH; /* We insert nc already, even though its rect is not yet calculated. This * is necessary because otherwise the workspace might be empty (and get * closed in tree_close()) even though it’s not. */ diff --git a/src/ipc.c b/src/ipc.c index 60ce8145..1d19fc6c 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -161,17 +161,14 @@ void dump_node(yajl_gen gen, struct Con *con, bool inplace_restart) { ystr("type"); y(integer, con->type); + /* provided for backwards compatibility only. */ ystr("orientation"); - switch (con->orientation) { - case NO_ORIENTATION: - ystr("none"); - break; - case HORIZ: + if (!con->split) + ystr("none"); + else { + if (con_orientation(con) == HORIZ) ystr("horizontal"); - break; - case VERT: - ystr("vertical"); - break; + else ystr("vertical"); } ystr("scratchpad_state"); @@ -203,10 +200,20 @@ void dump_node(yajl_gen gen, struct Con *con, bool inplace_restart) { ystr("focused"); y(bool, (con == focused)); + ystr("split"); + y(bool, con->split); + ystr("layout"); switch (con->layout) { case L_DEFAULT: - ystr("default"); + DLOG("About to dump layout=default, this is a bug in the code.\n"); + assert(false); + break; + case L_SPLITV: + ystr("splitv"); + break; + case L_SPLITH: + ystr("splith"); break; case L_STACKED: ystr("stacked"); @@ -222,6 +229,16 @@ void dump_node(yajl_gen gen, struct Con *con, bool inplace_restart) { break; } + ystr("last_split_layout"); + switch (con->layout) { + case L_SPLITV: + ystr("splitv"); + break; + default: + ystr("splith"); + break; + } + ystr("border"); switch (con->border_style) { case BS_NORMAL: diff --git a/src/load_layout.c b/src/load_layout.c index a8063dca..54735d91 100644 --- a/src/load_layout.c +++ b/src/load_layout.c @@ -156,15 +156,25 @@ static int json_string(void *ctx, const unsigned char *val, unsigned int len) { memcpy(json_node->sticky_group, val, len); LOG("sticky_group of this container is %s\n", json_node->sticky_group); } else if (strcasecmp(last_key, "orientation") == 0) { + /* Upgrade path from older versions of i3 (doing an inplace restart + * to a newer version): + * "orientation" is dumped before "layout". Therefore, we store + * whether the orientation was horizontal or vertical in the + * last_split_layout. When we then encounter layout == "default", + * we will use the last_split_layout as layout instead. */ char *buf = NULL; sasprintf(&buf, "%.*s", (int)len, val); - if (strcasecmp(buf, "none") == 0) - json_node->orientation = NO_ORIENTATION; - else if (strcasecmp(buf, "horizontal") == 0) - json_node->orientation = HORIZ; + if (strcasecmp(buf, "none") == 0 || + strcasecmp(buf, "horizontal") == 0) + json_node->last_split_layout = L_SPLITH; else if (strcasecmp(buf, "vertical") == 0) - json_node->orientation = VERT; + json_node->last_split_layout = L_SPLITV; else LOG("Unhandled orientation: %s\n", buf); + + /* What used to be an implicit check whether orientation != + * NO_ORIENTATION is now a proper separate flag. */ + if (strcasecmp(buf, "none") != 0) + json_node->split = true; free(buf); } else if (strcasecmp(last_key, "border") == 0) { char *buf = NULL; @@ -181,17 +191,33 @@ static int json_string(void *ctx, const unsigned char *val, unsigned int len) { char *buf = NULL; sasprintf(&buf, "%.*s", (int)len, val); if (strcasecmp(buf, "default") == 0) - json_node->layout = L_DEFAULT; + /* This set above when we read "orientation". */ + json_node->layout = json_node->last_split_layout; else if (strcasecmp(buf, "stacked") == 0) json_node->layout = L_STACKED; else if (strcasecmp(buf, "tabbed") == 0) json_node->layout = L_TABBED; - else if (strcasecmp(buf, "dockarea") == 0) + else if (strcasecmp(buf, "dockarea") == 0) { json_node->layout = L_DOCKAREA; - else if (strcasecmp(buf, "output") == 0) + /* Necessary for migrating from older versions of i3. */ + json_node->split = false; + } else if (strcasecmp(buf, "output") == 0) json_node->layout = L_OUTPUT; + else if (strcasecmp(buf, "splith") == 0) + json_node->layout = L_SPLITH; + else if (strcasecmp(buf, "splitv") == 0) + json_node->layout = L_SPLITV; else LOG("Unhandled \"layout\": %s\n", buf); free(buf); + } else if (strcasecmp(last_key, "last_split_layout") == 0) { + char *buf = NULL; + sasprintf(&buf, "%.*s", (int)len, val); + if (strcasecmp(buf, "splith") == 0) + json_node->last_split_layout = L_SPLITH; + else if (strcasecmp(buf, "splitv") == 0) + json_node->last_split_layout = L_SPLITV; + else LOG("Unhandled \"last_splitlayout\": %s\n", buf); + free(buf); } else if (strcasecmp(last_key, "mark") == 0) { char *buf = NULL; sasprintf(&buf, "%.*s", (int)len, val); @@ -288,6 +314,9 @@ static int json_bool(void *ctx, int val) { to_focus = json_node; } + if (strcasecmp(last_key, "split") == 0) + json_node->split = val; + if (parsing_swallows) { if (strcasecmp(last_key, "restart_mode") == 0) current_swallow->restart_mode = val; diff --git a/src/randr.c b/src/randr.c index 73adbf0e..f69d15a8 100644 --- a/src/randr.c +++ b/src/randr.c @@ -256,7 +256,6 @@ void output_init_con(Output *output) { Con *topdock = con_new(NULL, NULL); topdock->type = CT_DOCKAREA; topdock->layout = L_DOCKAREA; - topdock->orientation = VERT; /* this container swallows dock clients */ Match *match = scalloc(sizeof(Match)); match_init(match); @@ -278,6 +277,7 @@ void output_init_con(Output *output) { DLOG("adding main content container\n"); Con *content = con_new(NULL, NULL); content->type = CT_CON; + content->layout = L_SPLITH; FREE(content->name); content->name = sstrdup("content"); @@ -290,7 +290,6 @@ void output_init_con(Output *output) { Con *bottomdock = con_new(NULL, NULL); bottomdock->type = CT_DOCKAREA; bottomdock->layout = L_DOCKAREA; - bottomdock->orientation = VERT; /* this container swallows dock clients */ match = scalloc(sizeof(Match)); match_init(match); @@ -447,11 +446,11 @@ static void output_change_mode(xcb_connection_t *conn, Output *output) { if (con_num_children(workspace) > 1) continue; - workspace->orientation = (output->rect.height > output->rect.width) ? VERT : HORIZ; - DLOG("Setting workspace [%d,%s]'s orientation to %d.\n", workspace->num, workspace->name, workspace->orientation); + workspace->layout = (output->rect.height > output->rect.width) ? L_SPLITV : L_SPLITH; + DLOG("Setting workspace [%d,%s]'s layout to %d.\n", workspace->num, workspace->name, workspace->layout); if ((child = TAILQ_FIRST(&(workspace->nodes_head)))) { - child->orientation = workspace->orientation; - DLOG("Setting child [%d,%s]'s orientation to %d.\n", child->num, child->name, child->orientation); + child->layout = workspace->layout; + DLOG("Setting child [%d,%s]'s layout to %d.\n", child->num, child->name, child->layout); } } } diff --git a/src/render.c b/src/render.c index 860219df..bad67f04 100644 --- a/src/render.c +++ b/src/render.c @@ -106,9 +106,9 @@ static void render_l_output(Con *con) { */ void render_con(Con *con, bool render_fullscreen) { int children = con_num_children(con); - DLOG("Rendering %snode %p / %s / layout %d / children %d / orient %d\n", + DLOG("Rendering %snode %p / %s / layout %d / children %d\n", (render_fullscreen ? "fullscreen " : ""), con, con->name, con->layout, - children, con->orientation); + children); /* Copy container rect, subtract container border */ /* This is the actually usable space inside this container for clients */ @@ -208,11 +208,11 @@ void render_con(Con *con, bool render_fullscreen) { /* precalculate the sizes to be able to correct rounding errors */ int sizes[children]; - if (con->layout == L_DEFAULT && children > 0) { + if ((con->layout == L_SPLITH || con->layout == L_SPLITV) && children > 0) { assert(!TAILQ_EMPTY(&con->nodes_head)); Con *child; int i = 0, assigned = 0; - int total = con->orientation == HORIZ ? rect.width : rect.height; + int total = con_orientation(con) == HORIZ ? rect.width : rect.height; TAILQ_FOREACH(child, &(con->nodes_head), nodes) { double percentage = child->percent > 0.0 ? child->percent : 1.0 / children; assigned += sizes[i++] = percentage * total; @@ -289,8 +289,8 @@ void render_con(Con *con, bool render_fullscreen) { assert(children > 0); /* default layout */ - if (con->layout == L_DEFAULT) { - if (con->orientation == HORIZ) { + if (con->layout == L_SPLITH || con->layout == L_SPLITV) { + if (con->layout == L_SPLITH) { child->rect.x = x; child->rect.y = y; child->rect.width = sizes[i]; diff --git a/src/tree.c b/src/tree.c index bbcad7a4..068b0570 100644 --- a/src/tree.c +++ b/src/tree.c @@ -39,6 +39,7 @@ static Con *_create___i3(void) { content->type = CT_CON; FREE(content->name); content->name = sstrdup("content"); + content->layout = L_SPLITH; x_set_name(content, "[i3 con] content __i3"); con_attach(content, __i3, false); @@ -48,6 +49,7 @@ static Con *_create___i3(void) { ws->type = CT_WORKSPACE; ws->num = -1; ws->name = sstrdup("__i3_scratch"); + ws->layout = L_SPLITH; con_attach(ws, content, false); x_set_name(ws, "[i3 con] workspace __i3_scratch"); ws->fullscreen_mode = CF_OUTPUT; @@ -112,6 +114,7 @@ void tree_init(xcb_get_geometry_reply_t *geometry) { FREE(croot->name); croot->name = "root"; croot->type = CT_ROOT; + croot->layout = L_SPLITH; croot->rect = (Rect){ geometry->x, geometry->y, @@ -151,6 +154,7 @@ Con *tree_open_con(Con *con, i3Window *window) { /* 3. create the container and attach it to its parent */ Con *new = con_new(con, window); + new->layout = L_SPLITH; /* 4: re-calculate child->percent for each child */ con_fix_percent(con); @@ -337,7 +341,7 @@ void tree_split(Con *con, orientation_t orientation) { /* for a workspace, we just need to change orientation */ if (con->type == CT_WORKSPACE) { DLOG("Workspace, simply changing orientation to %d\n", orientation); - con->orientation = orientation; + con->layout = (orientation == HORIZ) ? L_SPLITH : L_SPLITV; return; } @@ -351,8 +355,9 @@ void tree_split(Con *con, orientation_t orientation) { * child (its split functionality is unused so far), we just change the * orientation (more intuitive than splitting again) */ if (con_num_children(parent) == 1 && - parent->layout == L_DEFAULT) { - parent->orientation = orientation; + (parent->layout == L_SPLITH || + parent->layout == L_SPLITV)) { + parent->layout = (orientation == HORIZ) ? L_SPLITH : L_SPLITV; DLOG("Just changing orientation of existing container\n"); return; } @@ -364,7 +369,8 @@ void tree_split(Con *con, orientation_t orientation) { TAILQ_REPLACE(&(parent->nodes_head), con, new, nodes); TAILQ_REPLACE(&(parent->focus_head), con, new, focused); new->parent = parent; - new->orientation = orientation; + new->layout = (orientation == HORIZ) ? L_SPLITH : L_SPLITV; + new->split = true; /* 3: swap 'percent' (resize factor) */ new->percent = con->percent; @@ -594,7 +600,9 @@ void tree_flatten(Con *con) { DLOG("Checking if I can flatten con = %p / %s\n", con, con->name); /* We only consider normal containers without windows */ - if (con->type != CT_CON || con->window != NULL) + if (con->type != CT_CON || + parent->layout == L_OUTPUT || /* con == "content" */ + con->window != NULL) goto recurse; /* Ensure it got only one child */ @@ -602,12 +610,14 @@ void tree_flatten(Con *con) { if (child == NULL || TAILQ_NEXT(child, nodes) != NULL) goto recurse; + DLOG("child = %p, con = %p, parent = %p\n", child, con, parent); + /* The child must have a different orientation than the con but the same as * the con’s parent to be redundant */ - if (con->orientation == NO_ORIENTATION || - child->orientation == NO_ORIENTATION || - con->orientation == child->orientation || - child->orientation != parent->orientation) + if (con->split || + child->split || + con_orientation(con) == con_orientation(child) || + con_orientation(child) != con_orientation(parent)) goto recurse; DLOG("Alright, I have to flatten this situation now. Stay calm.\n"); diff --git a/src/workspace.c b/src/workspace.c index 3d08fa4c..6394084a 100644 --- a/src/workspace.c +++ b/src/workspace.c @@ -14,6 +14,25 @@ * back-and-forth switching. */ static char *previous_workspace_name = NULL; +/* + * Sets ws->layout to splith/splitv if default_orientation was specified in the + * configfile. Otherwise, it uses splith/splitv depending on whether the output + * is higher than wide. + * + */ +static void _workspace_apply_default_orientation(Con *ws) { + /* If default_orientation is set to NO_ORIENTATION we determine + * orientation depending on output resolution. */ + if (config.default_orientation == NO_ORIENTATION) { + Con *output = con_get_output(ws); + ws->layout = (output->rect.height > output->rect.width) ? L_SPLITV : L_SPLITH; + DLOG("Auto orientation. Workspace size set to (%d,%d), setting layout to %d.\n", + output->rect.width, output->rect.height, ws->layout); + } else { + ws->layout = (config.default_orientation == HORIZ) ? L_SPLITH : L_SPLITV; + } +} + /* * Returns a pointer to the workspace with the given number (starting at 0), * creating the workspace if necessary (by allocating the necessary amount of @@ -64,16 +83,8 @@ Con *workspace_get(const char *num, bool *created) { else workspace->num = parsed_num; LOG("num = %d\n", workspace->num); - /* If default_orientation is set to NO_ORIENTATION we - * determine workspace orientation from workspace size. - * Otherwise we just set the orientation to default_orientation. */ - if (config.default_orientation == NO_ORIENTATION) { - workspace->orientation = (output->rect.height > output->rect.width) ? VERT : HORIZ; - DLOG("Auto orientation. Output resolution set to (%d,%d), setting orientation to %d.\n", - workspace->rect.width, workspace->rect.height, workspace->orientation); - } else { - workspace->orientation = config.default_orientation; - } + workspace->parent = content; + _workspace_apply_default_orientation(workspace); con_attach(workspace, content, false); @@ -198,19 +209,12 @@ Con *create_workspace_on_output(Output *output, Con *content) { ws->fullscreen_mode = CF_OUTPUT; - /* If default_orientation is set to NO_ORIENTATION we determine - * orientation depending on output resolution. */ - if (config.default_orientation == NO_ORIENTATION) { - ws->orientation = (output->rect.height > output->rect.width) ? VERT : HORIZ; - DLOG("Auto orientation. Workspace size set to (%d,%d), setting orientation to %d.\n", - output->rect.width, output->rect.height, ws->orientation); - } else { - ws->orientation = config.default_orientation; - } + _workspace_apply_default_orientation(ws); return ws; } + /* * Returns true if the workspace is currently visible. Especially important for * multi-monitor environments, as they can have multiple currenlty active @@ -686,8 +690,7 @@ void workspace_update_urgent_flag(Con *ws) { /* * 'Forces' workspace orientation by moving all cons into a new split-con with - * the same orientation as the workspace and then changing the workspace - * orientation. + * the same layout as the workspace and then changing the workspace layout. * */ void ws_force_orientation(Con *ws, orientation_t orientation) { @@ -695,9 +698,8 @@ void ws_force_orientation(Con *ws, orientation_t orientation) { Con *split = con_new(NULL, NULL); split->parent = ws; - /* 2: copy layout and orientation from workspace */ + /* 2: copy layout from workspace */ split->layout = ws->layout; - split->orientation = ws->orientation; Con *old_focused = TAILQ_FIRST(&(ws->focus_head)); @@ -709,8 +711,8 @@ void ws_force_orientation(Con *ws, orientation_t orientation) { con_attach(child, split, true); } - /* 4: switch workspace orientation */ - ws->orientation = orientation; + /* 4: switch workspace layout */ + ws->layout = (orientation == HORIZ) ? L_SPLITH : L_SPLITV; /* 5: attach the new split container to the workspace */ DLOG("Attaching new split to ws\n"); @@ -745,19 +747,11 @@ Con *workspace_attach_to(Con *ws) { /* 1: create a new split container */ Con *new = con_new(NULL, NULL); new->parent = ws; + new->split = true; /* 2: set the requested layout on the split con */ new->layout = config.default_layout; - /* 3: While the layout is irrelevant in stacked/tabbed mode, it needs - * to be set. Otherwise, this con will not be interpreted as a split - * container. */ - if (config.default_orientation == NO_ORIENTATION) { - new->orientation = (ws->rect.height > ws->rect.width) ? VERT : HORIZ; - } else { - new->orientation = config.default_orientation; - } - /* 4: attach the new split container to the workspace */ DLOG("Attaching new split %p to workspace %p\n", new, ws); con_attach(new, ws, false); diff --git a/testcases/t/116-nestedcons.t b/testcases/t/116-nestedcons.t index 3a495e27..18e21019 100644 --- a/testcases/t/116-nestedcons.t +++ b/testcases/t/116-nestedcons.t @@ -39,14 +39,16 @@ my $expected = { name => 'root', orientation => $ignore, type => 0, + split => JSON::XS::false, id => $ignore, rect => $ignore, window_rect => $ignore, geometry => $ignore, swallows => $ignore, percent => undef, - layout => 'default', + layout => 'splith', floating => 'auto_off', + last_split_layout => 'splith', scratchpad_state => 'none', focus => $ignore, focused => JSON::XS::false, diff --git a/testcases/t/122-split.t b/testcases/t/122-split.t index f672e9d6..d491c37a 100644 --- a/testcases/t/122-split.t +++ b/testcases/t/122-split.t @@ -19,10 +19,10 @@ sub verify_split_layout { $tmp = fresh_workspace; $ws = get_ws($tmp); - is($ws->{orientation}, 'horizontal', 'orientation horizontal by default'); + is($ws->{layout}, 'splith', 'orientation horizontal by default'); cmd 'split v'; $ws = get_ws($tmp); - is($ws->{orientation}, 'vertical', 'split v changes workspace orientation'); + is($ws->{layout}, 'splitv', 'split v changes workspace orientation'); cmd 'open'; cmd 'open'; @@ -47,7 +47,7 @@ sub verify_split_layout { is(@{$first->{nodes}}, 0, 'first container has no children'); isnt($second->{name}, $old_name, 'second container was replaced'); - is($second->{orientation}, 'horizontal', 'orientation is horizontal'); + is($second->{layout}, 'splith', 'orientation is horizontal'); is(@{$second->{nodes}}, 2, 'second container has 2 children'); is($second->{nodes}->[0]->{name}, $old_name, 'found old second container'); } @@ -66,10 +66,10 @@ verify_split_layout(split_command => 'split horizontal'); $tmp = fresh_workspace; $ws = get_ws($tmp); -is($ws->{orientation}, 'horizontal', 'orientation horizontal by default'); +is($ws->{layout}, 'splith', 'orientation horizontal by default'); cmd 'split v'; $ws = get_ws($tmp); -is($ws->{orientation}, 'vertical', 'split v changes workspace orientation'); +is($ws->{layout}, 'splitv', 'split v changes workspace orientation'); cmd 'open'; my @content = @{get_ws_content($tmp)}; diff --git a/testcases/t/145-flattening.t b/testcases/t/145-flattening.t index 9d22afc3..dbd1f246 100644 --- a/testcases/t/145-flattening.t +++ b/testcases/t/145-flattening.t @@ -22,7 +22,7 @@ cmd 'move up'; cmd 'move right'; my $ws = get_ws($tmp); -is($ws->{orientation}, 'horizontal', 'workspace orientation is horizontal'); +is($ws->{layout}, 'splith', 'workspace layout is splith'); is(@{$ws->{nodes}}, 3, 'all three windows on workspace level'); done_testing; diff --git a/testcases/t/192-layout.t b/testcases/t/192-layout.t new file mode 100644 index 00000000..e410d513 --- /dev/null +++ b/testcases/t/192-layout.t @@ -0,0 +1,84 @@ +#!perl +# vim:ts=4:sw=4:expandtab +# Verifies that switching between the different layouts works as expected. +use i3test; + +my $tmp = fresh_workspace; + +open_window; +open_window; +cmd 'split v'; +open_window; + +my ($nodes, $focus) = get_ws_content($tmp); +is($nodes->[1]->{layout}, 'splitv', 'layout is splitv currently'); + +cmd 'layout stacked'; +($nodes, $focus) = get_ws_content($tmp); +is($nodes->[1]->{layout}, 'stacked', 'layout now stacked'); + +cmd 'layout tabbed'; +($nodes, $focus) = get_ws_content($tmp); +is($nodes->[1]->{layout}, 'tabbed', 'layout now tabbed'); + +cmd 'layout toggle split'; +($nodes, $focus) = get_ws_content($tmp); +is($nodes->[1]->{layout}, 'splitv', 'layout now splitv again'); + +cmd 'layout toggle split'; +($nodes, $focus) = get_ws_content($tmp); +is($nodes->[1]->{layout}, 'splith', 'layout now splith'); + +cmd 'layout toggle split'; +($nodes, $focus) = get_ws_content($tmp); +is($nodes->[1]->{layout}, 'splitv', 'layout now splitv'); + +cmd 'layout toggle split'; +($nodes, $focus) = get_ws_content($tmp); +is($nodes->[1]->{layout}, 'splith', 'layout now splith'); + +cmd 'layout toggle'; +($nodes, $focus) = get_ws_content($tmp); +is($nodes->[1]->{layout}, 'stacked', 'layout now stacked'); + +cmd 'layout toggle'; +($nodes, $focus) = get_ws_content($tmp); +is($nodes->[1]->{layout}, 'tabbed', 'layout now tabbed'); + +cmd 'layout toggle'; +($nodes, $focus) = get_ws_content($tmp); +is($nodes->[1]->{layout}, 'splith', 'layout now splith'); + +cmd 'layout toggle'; +($nodes, $focus) = get_ws_content($tmp); +is($nodes->[1]->{layout}, 'stacked', 'layout now stacked'); + +cmd 'layout toggle all'; +($nodes, $focus) = get_ws_content($tmp); +is($nodes->[1]->{layout}, 'tabbed', 'layout now tabbed'); + +cmd 'layout toggle all'; +($nodes, $focus) = get_ws_content($tmp); +is($nodes->[1]->{layout}, 'splith', 'layout now splith'); + +cmd 'layout toggle all'; +($nodes, $focus) = get_ws_content($tmp); +is($nodes->[1]->{layout}, 'splitv', 'layout now splitv'); + +cmd 'layout toggle all'; +($nodes, $focus) = get_ws_content($tmp); +is($nodes->[1]->{layout}, 'stacked', 'layout now stacked'); + +cmd 'layout toggle all'; +($nodes, $focus) = get_ws_content($tmp); +is($nodes->[1]->{layout}, 'tabbed', 'layout now tabbed'); + +cmd 'layout toggle all'; +($nodes, $focus) = get_ws_content($tmp); +is($nodes->[1]->{layout}, 'splith', 'layout now splith'); + +cmd 'layout toggle all'; +($nodes, $focus) = get_ws_content($tmp); +is($nodes->[1]->{layout}, 'splitv', 'layout now splitv'); + +done_testing; -- 2.39.5