diff options
author | Michael Stapelberg <michael@stapelberg.de> | 2022-10-30 16:19:04 +0100 |
---|---|---|
committer | Michael Stapelberg <stapelberg@users.noreply.github.com> | 2022-11-01 17:55:46 +0100 |
commit | b825dc124a33f247cf35efd5e2d0646003a16f23 (patch) | |
tree | 0a1e657f29d85dd451acc5ad55b0208770f6e0ca | |
parent | 0b89d4b2a7ae84a852a707d71cf2697e55581ee7 (diff) | |
download | i3-b825dc124a33f247cf35efd5e2d0646003a16f23.tar.gz i3-b825dc124a33f247cf35efd5e2d0646003a16f23.zip |
Merge gaps support as-is
This code was copied over unmodified from https://github.com/Airblader/i3-gaps.
I have split out the differences between i3-gaps and i3 into three areas:
1. Gaps
2. i3bar height
3. rgba colors
-rw-r--r-- | include/commands.h | 6 | ||||
-rw-r--r-- | include/con.h | 7 | ||||
-rw-r--r-- | include/config_directives.h | 3 | ||||
-rw-r--r-- | include/configuration.h | 9 | ||||
-rw-r--r-- | include/data.h | 26 | ||||
-rw-r--r-- | include/render.h | 2 | ||||
-rw-r--r-- | parser-specs/commands.spec | 18 | ||||
-rw-r--r-- | parser-specs/config.spec | 40 | ||||
-rw-r--r-- | src/commands.c | 114 | ||||
-rw-r--r-- | src/con.c | 62 | ||||
-rw-r--r-- | src/config.c | 6 | ||||
-rw-r--r-- | src/config_directives.c | 166 | ||||
-rw-r--r-- | src/floating.c | 6 | ||||
-rw-r--r-- | src/ipc.c | 24 | ||||
-rw-r--r-- | src/load_layout.c | 22 | ||||
-rw-r--r-- | src/manage.c | 4 | ||||
-rw-r--r-- | src/render.c | 88 | ||||
-rw-r--r-- | src/tree.c | 2 | ||||
-rw-r--r-- | src/workspace.c | 25 | ||||
-rw-r--r-- | testcases/t/187-commands-parser.t | 1 | ||||
-rw-r--r-- | testcases/t/201-config-parser.t | 5 | ||||
-rw-r--r-- | testcases/t/287-edge-borders.t | 65 |
22 files changed, 668 insertions, 33 deletions
diff --git a/include/commands.h b/include/commands.h index d0f0140b..2ae2643c 100644 --- a/include/commands.h +++ b/include/commands.h @@ -333,6 +333,12 @@ void cmd_shmlog(I3_CMD, const char *argument); void cmd_debuglog(I3_CMD, const char *argument); /** + * Implementation of 'gaps inner|outer|top|right|bottom|left|horizontal|vertical current|all set|plus|minus|toggle <px>' + * + */ +void cmd_gaps(I3_CMD, const char *type, const char *scope, const char *mode, const char *value); + +/** * Implementation of 'title_window_icon <yes|no|toggle>' and 'title_window_icon <padding|toggle> <px>' * */ diff --git a/include/con.h b/include/con.h index 8d344994..6577c2d6 100644 --- a/include/con.h +++ b/include/con.h @@ -526,6 +526,13 @@ void con_set_urgency(Con *con, bool urgent); char *con_get_tree_representation(Con *con); /** + * Calculates the effective gap sizes for a container depending + * on whether it is adjacent to the edge of the screen or another + * container. + */ +gaps_t calculate_effective_gaps(Con *con); + +/** * force parent split containers to be redrawn * */ diff --git a/include/config_directives.h b/include/config_directives.h index 1cc0340f..600226e9 100644 --- a/include/config_directives.h +++ b/include/config_directives.h @@ -43,6 +43,9 @@ CFGFUN(include, const char *pattern); CFGFUN(font, const char *font); CFGFUN(exec, const char *exectype, const char *no_startup_id, const char *command); CFGFUN(for_window, const char *command); +CFGFUN(gaps, const char *workspace, const char *type, const long value); +CFGFUN(smart_borders, const char *enable); +CFGFUN(smart_gaps, const char *enable); CFGFUN(floating_minimum_size, const long width, const long height); CFGFUN(floating_maximum_size, const long width, const long height); CFGFUN(default_orientation, const char *orientation); diff --git a/include/configuration.h b/include/configuration.h index 5eae5f87..99f4b64e 100644 --- a/include/configuration.h +++ b/include/configuration.h @@ -268,6 +268,15 @@ struct Config { int number_barconfigs; tiling_drag_t tiling_drag; + + /* Gap sizes */ + gaps_t gaps; + + /* Should single containers on a workspace receive a border? */ + smart_borders_t smart_borders; + + /* Disable gaps if there is only one container on the workspace */ + smart_gaps_t smart_gaps; }; /** diff --git a/include/data.h b/include/data.h index 8078fd3e..67a573c2 100644 --- a/include/data.h +++ b/include/data.h @@ -47,6 +47,7 @@ typedef struct Con Con; typedef struct Match Match; typedef struct Assignment Assignment; typedef struct Window i3Window; +typedef struct gaps_t gaps_t; typedef struct mark_t mark_t; /****************************************************************************** @@ -80,11 +81,20 @@ typedef enum { ADJ_NONE = 0, ADJ_UPPER_SCREEN_EDGE = (1 << 2), ADJ_LOWER_SCREEN_EDGE = (1 << 4) } adjacent_t; +typedef enum { SMART_BORDERS_OFF, + SMART_BORDERS_ON, + SMART_BORDERS_NO_GAPS } smart_borders_t; + +typedef enum { SMART_GAPS_OFF, + SMART_GAPS_ON, + SMART_GAPS_INVERSE_OUTER } smart_gaps_t; + typedef enum { HEBM_NONE = ADJ_NONE, HEBM_VERTICAL = ADJ_LEFT_SCREEN_EDGE | ADJ_RIGHT_SCREEN_EDGE, HEBM_HORIZONTAL = ADJ_UPPER_SCREEN_EDGE | ADJ_LOWER_SCREEN_EDGE, HEBM_BOTH = HEBM_VERTICAL | HEBM_HORIZONTAL, - HEBM_SMART = (1 << 5) } hide_edge_borders_mode_t; + HEBM_SMART = (1 << 5), + HEBM_SMART_NO_GAPS = (1 << 6) } hide_edge_borders_mode_t; typedef enum { MM_REPLACE, MM_ADD } mark_mode_t; @@ -137,6 +147,14 @@ typedef enum { POINTER_WARPING_NONE = 1 } warping_t; +struct gaps_t { + int inner; + int top; + int right; + int bottom; + int left; +}; + /** * Focus wrapping modes. */ @@ -204,12 +222,13 @@ struct deco_render_params { }; /** - * Stores which workspace (by name or number) goes to which output. + * Stores which workspace (by name or number) goes to which output and its gaps config. * */ struct Workspace_Assignment { char *name; char *output; + gaps_t gaps; TAILQ_ENTRY(Workspace_Assignment) ws_assignments; }; @@ -645,6 +664,9 @@ struct Con { * workspace is not a named workspace (for named workspaces, num == -1) */ int num; + /** Only applicable for containers of type CT_WORKSPACE. */ + gaps_t gaps; + struct Con *parent; /* The position and size for this con. These coordinates are absolute. Note diff --git a/include/render.h b/include/render.h index 03751c01..8500b71d 100644 --- a/include/render.h +++ b/include/render.h @@ -40,7 +40,7 @@ typedef struct render_params { * updated in X11. * */ -void render_con(Con *con); +void render_con(Con *con, bool already_inset); /** * Returns the height for the decorations diff --git a/parser-specs/commands.spec b/parser-specs/commands.spec index 486f20dd..c618dfda 100644 --- a/parser-specs/commands.spec +++ b/parser-specs/commands.spec @@ -43,6 +43,7 @@ state INITIAL: 'title_window_icon' -> TITLE_WINDOW_ICON 'mode' -> MODE 'bar' -> BAR + 'gaps' -> GAPS state CRITERIA: ctype = 'class' -> CRITERION @@ -95,6 +96,23 @@ state BORDER: '1pixel' -> call cmd_border("pixel", 1) +# gaps inner|outer|horizontal|vertical|top|right|bottom|left [current] [set|plus|minus|toggle] <px> +state GAPS: + type = 'inner', 'outer', 'horizontal', 'vertical', 'top', 'right', 'bottom', 'left' + -> GAPS_WITH_TYPE + +state GAPS_WITH_TYPE: + scope = 'current', 'all' + -> GAPS_WITH_SCOPE + +state GAPS_WITH_SCOPE: + mode = 'plus', 'minus', 'set', 'toggle' + -> GAPS_WITH_MODE + +state GAPS_WITH_MODE: + value = word + -> call cmd_gaps($type, $scope, $mode, $value) + state BORDER_WIDTH: end -> call cmd_border($border_style, -1) diff --git a/parser-specs/config.spec b/parser-specs/config.spec index e8ea2f9d..c918f142 100644 --- a/parser-specs/config.spec +++ b/parser-specs/config.spec @@ -25,6 +25,9 @@ state INITIAL: 'bar' -> BARBRACE 'font' -> FONT 'mode' -> MODENAME + 'gaps' -> GAPS + 'smart_borders' -> SMART_BORDERS + 'smart_gaps' -> SMART_GAPS 'floating_minimum_size' -> FLOATING_MINIMUM_SIZE_WIDTH 'floating_maximum_size' -> FLOATING_MAXIMUM_SIZE_WIDTH 'floating_modifier' -> FLOATING_MODIFIER @@ -65,6 +68,32 @@ state IGNORE_LINE: line -> INITIAL +# gaps inner|outer|horizontal|vertical|top|right|bottom|left <px> +state GAPS: + scope = 'inner', 'outer', 'horizontal', 'vertical', 'top', 'right', 'bottom', 'left' + -> GAPS_WITH_SCOPE + +state GAPS_WITH_SCOPE: + value = number + -> call cfg_gaps($workspace, $scope, &value) + +# smart_borders true|false +# smart_borders no_gaps +state SMART_BORDERS: + enabled = '1', 'yes', 'true', 'on', 'enable', 'active' + -> call cfg_smart_borders($enabled) + enabled = 'no_gaps' + -> call cfg_smart_borders($enabled) + +# smart_gaps on|off +state SMART_GAPS: + enabled = '1', 'yes', 'true', 'on', 'enable', 'active' + -> call cfg_smart_gaps($enabled) + enabled = '0', 'no', 'false', 'off', 'disable', 'inactive' + -> call cfg_smart_gaps($enabled) + enabled = 'inverse_outer' + -> call cfg_smart_gaps($enabled) + # include <pattern> state INCLUDE: pattern = string @@ -135,10 +164,10 @@ state DEFAULT_BORDER_PIXELS_PX: end -> call cfg_default_border($windowtype, $border, &width) -# hide_edge_borders <none|vertical|horizontal|both|smart> +# hide_edge_borders <none|vertical|horizontal|both|smart|no_gaps> # also hide_edge_borders <bool> for compatibility state HIDE_EDGE_BORDERS: - hide_borders = 'none', 'vertical', 'horizontal', 'both', 'smart' + hide_borders = 'none', 'vertical', 'horizontal', 'both', 'smart_no_gaps', 'smart' -> call cfg_hide_edge_borders($hide_borders) hide_borders = '1', 'yes', 'true', 'on', 'enable', 'active' -> call cfg_hide_edge_borders($hide_borders) @@ -297,13 +326,16 @@ state FOCUS_ON_WINDOW_ACTIVATION: -> call cfg_focus_on_window_activation($mode) # workspace <workspace> output <output> +# workspace <workspace> gaps inner|outer <px> state WORKSPACE: workspace = word - -> WORKSPACE_OUTPUT + -> WORKSPACE_COMMAND -state WORKSPACE_OUTPUT: +state WORKSPACE_COMMAND: 'output' -> WORKSPACE_OUTPUT_WORD + 'gaps' + -> GAPS state WORKSPACE_OUTPUT_WORD: output = word diff --git a/src/commands.c b/src/commands.c index 0a39c55a..52c3fcd1 100644 --- a/src/commands.c +++ b/src/commands.c @@ -831,7 +831,7 @@ void cmd_append_layout(I3_CMD, const char *cpath) { // is not executed yet and will be batched with append_layout’s // needs_tree_render after the parser finished. We should check if that is // necessary at all. - render_con(croot); + render_con(croot, false); restore_open_placeholder_windows(parent); @@ -2374,3 +2374,115 @@ void cmd_debuglog(I3_CMD, const char *argument) { // XXX: default reply for now, make this a better reply ysuccess(true); } + +/** + * Implementation of 'gaps inner|outer|top|right|bottom|left|horizontal|vertical current|all set|plus|minus|toggle <px>' + * + */ +void cmd_gaps(I3_CMD, const char *type, const char *scope, const char *mode, const char *value) { + int pixels = logical_px(atoi(value)); + Con *workspace = con_get_workspace(focused); + +#define CMD_SET_GAPS_VALUE(type, value, reset) \ + do { \ + if (!strcmp(scope, "all")) { \ + Con *output, *cur_ws = NULL; \ + TAILQ_FOREACH (output, &(croot->nodes_head), nodes) { \ + Con *content = output_get_content(output); \ + TAILQ_FOREACH (cur_ws, &(content->nodes_head), nodes) { \ + if (reset) \ + cur_ws->gaps.type = 0; \ + else if (value + cur_ws->gaps.type < 0) \ + cur_ws->gaps.type = -value; \ + } \ + } \ + \ + config.gaps.type = value; \ + } else { \ + workspace->gaps.type = value - config.gaps.type; \ + } \ + } while (0) + +#define CMD_GAPS(type) \ + do { \ + int current_value = config.gaps.type; \ + if (strcmp(scope, "current") == 0) \ + current_value += workspace->gaps.type; \ + \ + bool reset = false; \ + if (!strcmp(mode, "plus")) \ + current_value += pixels; \ + else if (!strcmp(mode, "minus")) \ + current_value -= pixels; \ + else if (!strcmp(mode, "set")) { \ + current_value = pixels; \ + reset = true; \ + } else if (!strcmp(mode, "toggle")) { \ + current_value = !current_value * pixels; \ + reset = true; \ + } else { \ + ELOG("Invalid mode %s when changing gaps", mode); \ + ysuccess(false); \ + return; \ + } \ + \ + /* see issue 262 */ \ + int min_value = 0; \ + if (strcmp(#type, "inner") != 0) { \ + min_value = strcmp(scope, "all") ? -config.gaps.inner - workspace->gaps.inner : -config.gaps.inner; \ + } \ + \ + if (current_value < min_value) \ + current_value = min_value; \ + \ + CMD_SET_GAPS_VALUE(type, current_value, reset); \ + } while (0) + +#define CMD_UPDATE_GAPS(type) \ + do { \ + if (!strcmp(scope, "all")) { \ + if (config.gaps.type + config.gaps.inner < 0) \ + CMD_SET_GAPS_VALUE(type, -config.gaps.inner, true); \ + } else { \ + if (config.gaps.type + workspace->gaps.type + config.gaps.inner + workspace->gaps.inner < 0) { \ + CMD_SET_GAPS_VALUE(type, -config.gaps.inner - workspace->gaps.inner, true); \ + } \ + } \ + } while (0) + + if (!strcmp(type, "inner")) { + CMD_GAPS(inner); + // update inconsistent values + CMD_UPDATE_GAPS(top); + CMD_UPDATE_GAPS(bottom); + CMD_UPDATE_GAPS(right); + CMD_UPDATE_GAPS(left); + } else if (!strcmp(type, "outer")) { + CMD_GAPS(top); + CMD_GAPS(bottom); + CMD_GAPS(right); + CMD_GAPS(left); + } else if (!strcmp(type, "vertical")) { + CMD_GAPS(top); + CMD_GAPS(bottom); + } else if (!strcmp(type, "horizontal")) { + CMD_GAPS(right); + CMD_GAPS(left); + } else if (!strcmp(type, "top")) { + CMD_GAPS(top); + } else if (!strcmp(type, "bottom")) { + CMD_GAPS(bottom); + } else if (!strcmp(type, "right")) { + CMD_GAPS(right); + } else if (!strcmp(type, "left")) { + CMD_GAPS(left); + } else { + ELOG("Invalid type %s when changing gaps", type); + ysuccess(false); + return; + } + + cmd_output->needs_tree_render = true; + // XXX: default reply for now, make this a better reply + ysuccess(true); +} @@ -1685,6 +1685,13 @@ Con *con_descend_direction(Con *con, direction_t direction) { return con_descend_direction(most, direction); } +static bool has_outer_gaps(gaps_t gaps) { + return gaps.top > 0 || + gaps.right > 0 || + gaps.bottom > 0 || + gaps.left > 0; +} + /* * Returns a "relative" Rect which contains the amount of pixels that need to * be added to the original Rect to get the final position (obviously the @@ -1692,10 +1699,12 @@ Con *con_descend_direction(Con *con, direction_t direction) { * */ Rect con_border_style_rect(Con *con) { - if (config.hide_edge_borders == HEBM_SMART && con_num_visible_children(con_get_workspace(con)) <= 1) { - if (!con_is_floating(con)) { + if ((config.smart_borders == SMART_BORDERS_ON && con_num_visible_children(con_get_workspace(con)) <= 1) || + (config.smart_borders == SMART_BORDERS_NO_GAPS && !has_outer_gaps(calculate_effective_gaps(con))) || + (config.hide_edge_borders == HEBM_SMART && con_num_visible_children(con_get_workspace(con)) <= 1) || + (config.hide_edge_borders == HEBM_SMART_NO_GAPS && con_num_visible_children(con_get_workspace(con)) <= 1 && !has_outer_gaps(calculate_effective_gaps(con)))) { + if (!con_is_floating(con)) return (Rect){0, 0, 0, 0}; - } } adjacent_t borders_to_hide = ADJ_NONE; @@ -1720,7 +1729,13 @@ Rect con_border_style_rect(Con *con) { result = (Rect){border_width, border_width, -(2 * border_width), -(2 * border_width)}; } - borders_to_hide = con_adjacent_borders(con) & config.hide_edge_borders; + /* If hide_edge_borders is set to no_gaps and it did not pass the no border check, show all borders */ + if (config.hide_edge_borders == HEBM_SMART_NO_GAPS) { + borders_to_hide = con_adjacent_borders(con) & HEBM_NONE; + } else { + borders_to_hide = con_adjacent_borders(con) & config.hide_edge_borders; + } + if (borders_to_hide & ADJ_LEFT_SCREEN_EDGE) { result.x -= border_width; result.width += border_width; @@ -2327,6 +2342,45 @@ char *con_get_tree_representation(Con *con) { return complete_buf; } +/** + * Calculates the effective gap sizes for a container. + */ +gaps_t calculate_effective_gaps(Con *con) { + Con *workspace = con_get_workspace(con); + if (workspace == NULL) + return (gaps_t){0, 0, 0, 0, 0}; + + bool one_child = con_num_visible_children(workspace) <= 1 || + (con_num_children(workspace) == 1 && + (TAILQ_FIRST(&(workspace->nodes_head))->layout == L_TABBED || + TAILQ_FIRST(&(workspace->nodes_head))->layout == L_STACKED)); + + if (config.smart_gaps == SMART_GAPS_ON && one_child) + return (gaps_t){0, 0, 0, 0, 0}; + + gaps_t gaps = { + .inner = (workspace->gaps.inner + config.gaps.inner) / 2, + .top = 0, + .right = 0, + .bottom = 0, + .left = 0}; + + if (config.smart_gaps != SMART_GAPS_INVERSE_OUTER || one_child) { + gaps.top = workspace->gaps.top + config.gaps.top; + gaps.right = workspace->gaps.right + config.gaps.right; + gaps.bottom = workspace->gaps.bottom + config.gaps.bottom; + gaps.left = workspace->gaps.left + config.gaps.left; + } + + /* Outer gaps are added on top of inner gaps. */ + gaps.top += 2 * gaps.inner; + gaps.right += 2 * gaps.inner; + gaps.bottom += 2 * gaps.inner; + gaps.left += 2 * gaps.inner; + + return gaps; +} + /* * Returns the container's title considering the current title format. * diff --git a/src/config.c b/src/config.c index 236d682b..7cbdad1f 100644 --- a/src/config.c +++ b/src/config.c @@ -216,6 +216,12 @@ bool load_configuration(const char *override_configpath, config_load_t load_type /* Set default_orientation to NO_ORIENTATION for auto orientation. */ config.default_orientation = NO_ORIENTATION; + config.gaps.inner = 0; + config.gaps.top = 0; + config.gaps.right = 0; + config.gaps.bottom = 0; + config.gaps.left = 0; + /* Set default urgency reset delay to 500ms */ if (config.workspace_urgency_timer == 0) config.workspace_urgency_timer = 0.5; diff --git a/src/config_directives.c b/src/config_directives.c index ae68685b..78a246dd 100644 --- a/src/config_directives.c +++ b/src/config_directives.c @@ -231,6 +231,160 @@ CFGFUN(for_window, const char *command) { TAILQ_INSERT_TAIL(&assignments, assignment, assignments); } +static void create_gaps_assignment(const char *workspace, const char *scope, gaps_t gaps) { + DLOG("Setting gaps for workspace %s", workspace); + + struct Workspace_Assignment *assignment; + TAILQ_FOREACH (assignment, &ws_assignments, ws_assignments) { + if (strcasecmp(assignment->name, workspace) == 0) { + if (!strcmp(scope, "inner")) { + assignment->gaps.inner = gaps.inner; + } else if (!strcmp(scope, "outer")) { + assignment->gaps.top = gaps.top; + assignment->gaps.right = gaps.right; + assignment->gaps.bottom = gaps.bottom; + assignment->gaps.left = gaps.left; + } else if (!strcmp(scope, "vertical")) { + assignment->gaps.top = gaps.top; + assignment->gaps.bottom = gaps.bottom; + } else if (!strcmp(scope, "horizontal")) { + assignment->gaps.right = gaps.right; + assignment->gaps.left = gaps.left; + } else if (!strcmp(scope, "top")) { + assignment->gaps.top = gaps.top; + } else if (!strcmp(scope, "right")) { + assignment->gaps.right = gaps.right; + } else if (!strcmp(scope, "bottom")) { + assignment->gaps.bottom = gaps.bottom; + } else if (!strcmp(scope, "left")) { + assignment->gaps.left = gaps.left; + } else { + ELOG("Invalid command, cannot process scope %s", scope); + } + + return; + } + } + + // Assignment does not yet exist, let's create it. + assignment = scalloc(1, sizeof(struct Workspace_Assignment)); + assignment->name = sstrdup(workspace); + assignment->output = NULL; + if (!strcmp(scope, "inner")) { + assignment->gaps.inner = gaps.inner; + } else if (!strcmp(scope, "outer")) { + assignment->gaps.top = gaps.top; + assignment->gaps.right = gaps.right; + assignment->gaps.bottom = gaps.bottom; + assignment->gaps.left = gaps.left; + } else if (!strcmp(scope, "vertical")) { + assignment->gaps.top = gaps.top; + assignment->gaps.bottom = gaps.bottom; + } else if (!strcmp(scope, "horizontal")) { + assignment->gaps.right = gaps.right; + assignment->gaps.left = gaps.left; + } else if (!strcmp(scope, "top")) { + assignment->gaps.top = gaps.top; + } else if (!strcmp(scope, "right")) { + assignment->gaps.right = gaps.right; + } else if (!strcmp(scope, "bottom")) { + assignment->gaps.bottom = gaps.bottom; + } else if (!strcmp(scope, "left")) { + assignment->gaps.left = gaps.left; + } else { + ELOG("Invalid command, cannot process scope %s", scope); + } + TAILQ_INSERT_TAIL(&ws_assignments, assignment, ws_assignments); +} + +CFGFUN(gaps, const char *workspace, const char *scope, const long value) { + int pixels = logical_px(value); + gaps_t gaps = (gaps_t){0, 0, 0, 0, 0}; + if (!strcmp(scope, "inner")) { + if (workspace == NULL) + config.gaps.inner = pixels; + else { + gaps.inner = pixels - config.gaps.inner; + create_gaps_assignment(workspace, scope, gaps); + } + } else if (!strcmp(scope, "outer")) { + if (workspace == NULL) { + config.gaps.top = pixels; + config.gaps.right = pixels; + config.gaps.bottom = pixels; + config.gaps.left = pixels; + } else { + gaps.top = pixels - config.gaps.top; + gaps.right = pixels - config.gaps.right; + gaps.bottom = pixels - config.gaps.bottom; + gaps.left = pixels - config.gaps.left; + create_gaps_assignment(workspace, scope, gaps); + } + } else if (!strcmp(scope, "vertical")) { + if (workspace == NULL) { + config.gaps.top = pixels; + config.gaps.bottom = pixels; + } else { + gaps.top = pixels - config.gaps.top; + gaps.bottom = pixels - config.gaps.bottom; + create_gaps_assignment(workspace, scope, gaps); + } + } else if (!strcmp(scope, "horizontal")) { + if (workspace == NULL) { + config.gaps.right = pixels; + config.gaps.left = pixels; + } else { + gaps.right = pixels - config.gaps.right; + gaps.left = pixels - config.gaps.left; + create_gaps_assignment(workspace, scope, gaps); + } + } else if (!strcmp(scope, "top")) { + if (workspace == NULL) + config.gaps.top = pixels; + else { + gaps.top = pixels - config.gaps.top; + create_gaps_assignment(workspace, scope, gaps); + } + } else if (!strcmp(scope, "right")) { + if (workspace == NULL) + config.gaps.right = pixels; + else { + gaps.right = pixels - config.gaps.right; + create_gaps_assignment(workspace, scope, gaps); + } + } else if (!strcmp(scope, "bottom")) { + if (workspace == NULL) + config.gaps.bottom = pixels; + else { + gaps.bottom = pixels - config.gaps.bottom; + create_gaps_assignment(workspace, scope, gaps); + } + } else if (!strcmp(scope, "left")) { + if (workspace == NULL) + config.gaps.left = pixels; + else { + gaps.left = pixels - config.gaps.left; + create_gaps_assignment(workspace, scope, gaps); + } + } else { + ELOG("Invalid command, cannot process scope %s", scope); + } +} + +CFGFUN(smart_borders, const char *enable) { + if (!strcmp(enable, "no_gaps")) + config.smart_borders = SMART_BORDERS_NO_GAPS; + else + config.smart_borders = boolstr(enable) ? SMART_BORDERS_ON : SMART_BORDERS_OFF; +} + +CFGFUN(smart_gaps, const char *enable) { + if (!strcmp(enable, "inverse_outer")) + config.smart_gaps = SMART_GAPS_INVERSE_OUTER; + else + config.smart_gaps = boolstr(enable) ? SMART_GAPS_ON : SMART_GAPS_OFF; +} + CFGFUN(floating_minimum_size, const long width, const long height) { config.floating_minimum_width = width; config.floating_minimum_height = height; @@ -297,7 +451,9 @@ CFGFUN(default_border, const char *windowtype, const char *border, const long wi } CFGFUN(hide_edge_borders, const char *borders) { - if (strcmp(borders, "smart") == 0) + if (strcmp(borders, "smart_no_gaps") == 0) + config.hide_edge_borders = HEBM_SMART_NO_GAPS; + else if (strcmp(borders, "smart") == 0) config.hide_edge_borders = HEBM_SMART; else if (strcmp(borders, "vertical") == 0) config.hide_edge_borders = HEBM_VERTICAL; @@ -417,9 +573,11 @@ CFGFUN(workspace, const char *workspace, const char *output) { TAILQ_FOREACH (assignment, &ws_assignments, ws_assignments) { if (strcasecmp(assignment->name, workspace) == 0) { - ELOG("You have a duplicate workspace assignment for workspace \"%s\"\n", - workspace); - return; + if (assignment->output != NULL) { + ELOG("You have a duplicate workspace assignment for workspace \"%s\"\n", + workspace); + return; + } } } diff --git a/src/floating.c b/src/floating.c index 992ca23c..5eaf639c 100644 --- a/src/floating.c +++ b/src/floating.c @@ -415,7 +415,7 @@ bool floating_enable(Con *con, bool automatic) { DLOG("Corrected y = %d (deco_height = %d)\n", nc->rect.y, deco_height); /* render the cons to get initial window_rect correct */ - render_con(nc); + render_con(nc, true); if (set_focus) con_activate(con); @@ -580,7 +580,7 @@ DRAGGING_CB(drag_window_callback) { con->rect.x = old_rect->x + (new_x - event->root_x); con->rect.y = old_rect->y + (new_y - event->root_y); - render_con(con); + render_con(con, true); x_push_node(con); xcb_flush(conn); @@ -685,7 +685,7 @@ DRAGGING_CB(resize_window_callback) { con->rect.x = dest_x; con->rect.y = dest_y; - render_con(con); + render_con(con, true); x_push_changes(croot); } @@ -245,6 +245,28 @@ static void dump_rect(yajl_gen gen, const char *name, Rect r) { y(map_close); } +static void dump_gaps(yajl_gen gen, const char *name, gaps_t gaps) { + ystr(name); + y(map_open); + ystr("inner"); + y(integer, gaps.inner); + + // TODO: the i3ipc Python modules recognize gaps, but only inner/outer + // This is currently here to preserve compatibility with that + ystr("outer"); + y(integer, gaps.top); + + ystr("top"); + y(integer, gaps.top); + ystr("right"); + y(integer, gaps.right); + ystr("bottom"); + y(integer, gaps.bottom); + ystr("left"); + y(integer, gaps.left); + y(map_close); +} + static void dump_event_state_mask(yajl_gen gen, Binding *bind) { y(array_open); for (int i = 0; i < 20; i++) { @@ -504,6 +526,8 @@ void dump_node(yajl_gen gen, struct Con *con, bool inplace_restart) { if (con->type == CT_WORKSPACE) { ystr("num"); y(integer, con->num); + + dump_gaps(gen, "gaps", con->gaps); } ystr("window"); diff --git a/src/load_layout.c b/src/load_layout.c index 7bdf5521..9789fb33 100644 --- a/src/load_layout.c +++ b/src/load_layout.c @@ -20,6 +20,7 @@ static char *last_key; static int incomplete; static Con *json_node; static Con *to_focus; +static bool parsing_gaps; static bool parsing_swallows; static bool parsing_rect; static bool parsing_deco_rect; @@ -60,7 +61,7 @@ static int json_start_map(void *ctx) { TAILQ_INSERT_TAIL(&(json_node->swallow_head), current_swallow, matches); swallow_is_empty = true; } else { - if (!parsing_rect && !parsing_deco_rect && !parsing_window_rect && !parsing_geometry) { + if (!parsing_rect && !parsing_deco_rect && !parsing_window_rect && !parsing_geometry && !parsing_gaps) { if (last_key && strcasecmp(last_key, "floating_nodes") == 0) { DLOG("New floating_node\n"); Con *ws = con_get_workspace(json_node); @@ -84,7 +85,7 @@ static int json_start_map(void *ctx) { static int json_end_map(void *ctx) { LOG("end of map\n"); - if (!parsing_swallows && !parsing_rect && !parsing_deco_rect && !parsing_window_rect && !parsing_geometry) { + if (!parsing_swallows && !parsing_rect && !parsing_deco_rect && !parsing_window_rect && !parsing_geometry && !parsing_gaps) { /* Set a few default values to simplify manually crafted layout files. */ if (json_node->layout == L_DEFAULT) { DLOG("Setting layout = L_SPLITH\n"); @@ -192,6 +193,7 @@ static int json_end_map(void *ctx) { return 0; } + parsing_gaps = false; parsing_rect = false; parsing_deco_rect = false; parsing_window_rect = false; @@ -245,6 +247,9 @@ static int json_key(void *ctx, const unsigned char *val, size_t len) { if (strcasecmp(last_key, "swallows") == 0) parsing_swallows = true; + if (strcasecmp(last_key, "gaps") == 0) + parsing_gaps = true; + if (strcasecmp(last_key, "rect") == 0) parsing_rect = true; @@ -506,6 +511,18 @@ static int json_int(void *ctx, long long val) { swallow_is_empty = false; } } + if (parsing_gaps) { + if (strcasecmp(last_key, "inner") == 0) + json_node->gaps.inner = val; + else if (strcasecmp(last_key, "top") == 0) + json_node->gaps.top = val; + else if (strcasecmp(last_key, "right") == 0) + json_node->gaps.right = val; + else if (strcasecmp(last_key, "bottom") == 0) + json_node->gaps.bottom = val; + else if (strcasecmp(last_key, "left") == 0) + json_node->gaps.left = val; + } return 1; } @@ -653,6 +670,7 @@ void tree_append_json(Con *con, const char *buf, const size_t len, char **errorm yajl_config(hand, yajl_dont_validate_strings, true); json_node = con; to_focus = NULL; + parsing_gaps = false; incomplete = 0; parsing_swallows = false; parsing_rect = false; diff --git a/src/manage.c b/src/manage.c index a7de243e..6125af34 100644 --- a/src/manage.c +++ b/src/manage.c @@ -591,13 +591,13 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki * workspace at all. However, just calling render_con() on the * workspace isn’t enough either — it needs the rect. */ ws->rect = ws->parent->rect; - render_con(ws); + render_con(ws, false); /* Disable setting focus, otherwise we’d move focus to an invisible * workspace, which we generally prevent (e.g. in * con_move_to_workspace). */ set_focus = false; } - render_con(croot); + render_con(croot, false); cwindow->managed_since = time(NULL); diff --git a/src/render.c b/src/render.c index cc1c01f8..6917efd1 100644 --- a/src/render.c +++ b/src/render.c @@ -20,6 +20,8 @@ static void render_con_split(Con *con, Con *child, render_params *p, int i); static void render_con_stacked(Con *con, Con *child, render_params *p, int i); static void render_con_tabbed(Con *con, Con *child, render_params *p, int i); static void render_con_dockarea(Con *con, Con *child, render_params *p); +bool should_inset_con(Con *con, int children); +bool has_adjacent_container(Con *con, direction_t direction); /* * Returns the height for the decorations @@ -39,7 +41,7 @@ int render_deco_height(void) { * updated in X11. * */ -void render_con(Con *con) { +void render_con(Con *con, bool already_inset) { render_params params = { .rect = con->rect, .x = con->rect.x, @@ -49,6 +51,33 @@ void render_con(Con *con) { DLOG("Rendering node %p / %s / layout %d / children %d\n", con, con->name, con->layout, params.children); + bool should_inset = should_inset_con(con, params.children); + if (!already_inset && should_inset) { + gaps_t gaps = calculate_effective_gaps(con); + Rect inset = (Rect){ + has_adjacent_container(con, D_LEFT) ? gaps.inner : gaps.left, + has_adjacent_container(con, D_UP) ? gaps.inner : gaps.top, + has_adjacent_container(con, D_RIGHT) ? -gaps.inner : -gaps.right, + has_adjacent_container(con, D_DOWN) ? -gaps.inner : -gaps.bottom}; + inset.width -= inset.x; + inset.height -= inset.y; + + if (con->fullscreen_mode == CF_NONE) { + params.rect = rect_add(params.rect, inset); + con->rect = rect_add(con->rect, inset); + if (con->window) { + con->window_rect = rect_add(con->window_rect, inset); + } + } + inset.height = 0; + if (con->deco_rect.width != 0 && con->deco_rect.height != 0) { + con->deco_rect = rect_add(con->deco_rect, inset); + } + + params.x = con->rect.x; + params.y = con->rect.y; + } + int i = 0; con->mapped = true; @@ -85,7 +114,7 @@ void render_con(Con *con) { if (fullscreen) { fullscreen->rect = params.rect; x_raise_con(fullscreen); - render_con(fullscreen); + render_con(fullscreen, false); /* Fullscreen containers are either global (underneath the CT_ROOT * container) or per-output (underneath the CT_CONTENT container). For * global fullscreen containers, we cannot abort rendering here yet, @@ -130,7 +159,7 @@ void render_con(Con *con) { DLOG("child at (%d, %d) with (%d x %d)\n", child->rect.x, child->rect.y, child->rect.width, child->rect.height); x_raise_con(child); - render_con(child); + render_con(child, should_inset || already_inset); i++; } @@ -144,7 +173,7 @@ void render_con(Con *con) { * that we have a non-leaf-container inside the stack. In that * case, the children of the non-leaf-container need to be * raised as well. */ - render_con(child); + render_con(child, true); } if (params.children != 1) @@ -193,7 +222,7 @@ static void render_root(Con *con, Con *fullscreen) { Con *output; if (!fullscreen) { TAILQ_FOREACH (output, &(con->nodes_head), nodes) { - render_con(output); + render_con(output, false); } } @@ -237,7 +266,7 @@ static void render_root(Con *con, Con *fullscreen) { DLOG("floating child at (%d,%d) with %d x %d\n", child->rect.x, child->rect.y, child->rect.width, child->rect.height); x_raise_con(child); - render_con(child); + render_con(child, true); } } } @@ -287,7 +316,7 @@ static void render_output(Con *con) { if (fullscreen) { fullscreen->rect = con->rect; x_raise_con(fullscreen); - render_con(fullscreen); + render_con(fullscreen, false); return; } @@ -328,7 +357,7 @@ static void render_output(Con *con) { DLOG("child at (%d, %d) with (%d x %d)\n", child->rect.x, child->rect.y, child->rect.width, child->rect.height); x_raise_con(child); - render_con(child); + render_con(child, child->type == CT_DOCKAREA); } } @@ -430,3 +459,46 @@ static void render_con_dockarea(Con *con, Con *child, render_params *p) { child->deco_rect.height = 0; p->y += child->rect.height; } + +/* + * Decides whether the container should be inset. + */ +bool should_inset_con(Con *con, int children) { + /* Don't inset floating containers and workspaces. */ + if (con->type == CT_FLOATING_CON || con->type == CT_WORKSPACE) + return false; + + if (con_is_leaf(con)) + return true; + + return (con->layout == L_STACKED || con->layout == L_TABBED) && children > 0; +} + +/* + * Returns whether the given container has an adjacent container in the + * specified direction. In other words, this returns true if and only if + * the container is not touching the edge of the screen in that direction. + */ +bool has_adjacent_container(Con *con, direction_t direction) { + Con *workspace = con_get_workspace(con); + Con *fullscreen = con_get_fullscreen_con(workspace, CF_GLOBAL); + if (fullscreen == NULL) + fullscreen = con_get_fullscreen_con(workspace, CF_OUTPUT); + + /* If this container is fullscreen by itself, there's no adjacent container. */ + if (con == fullscreen) + return false; + + Con *first = con; + Con *second = NULL; + bool found_neighbor = resize_find_tiling_participants(&first, &second, direction, false); + if (!found_neighbor) + return false; + + /* If we have an adjacent container and nothing is fullscreen, we consider it. */ + if (fullscreen == NULL) + return true; + + /* For fullscreen containers, only consider the adjacent container if it is also fullscreen. */ + return con_has_parent(con, fullscreen) && con_has_parent(second, fullscreen); +} @@ -458,7 +458,7 @@ void tree_render(void) { mark_unmapped(croot); croot->mapped = true; - render_con(croot); + render_con(croot, false); x_push_changes(croot); DLOG("-- END RENDERING --\n"); diff --git a/src/workspace.c b/src/workspace.c index e1ac49d3..73917639 100644 --- a/src/workspace.c +++ b/src/workspace.c @@ -85,6 +85,10 @@ Con *get_assigned_output(const char *name, long parsed_num) { Con *output = NULL; struct Workspace_Assignment *assignment; TAILQ_FOREACH (assignment, &ws_assignments, ws_assignments) { + if (assignment->output == NULL) { + continue; + } + if (name && strcmp(assignment->name, name) == 0) { DLOG("Found workspace name=\"%s\" assignment to output \"%s\"\n", name, assignment->output); @@ -131,11 +135,22 @@ Con *workspace_get(const char *num) { } LOG("Creating new workspace \"%s\"\n", num); + gaps_t gaps = (gaps_t){0, 0, 0, 0, 0}; /* We set workspace->num to the number if this workspace’s name begins with * a positive number. Otherwise it’s a named ws and num will be 1. */ const int parsed_num = ws_name_to_number(num); + struct Workspace_Assignment *assignment; + TAILQ_FOREACH (assignment, &ws_assignments, ws_assignments) { + if (strcmp(assignment->name, num) == 0) { + gaps = assignment->gaps; + break; + } else if (parsed_num != -1 && name_is_digits(assignment->name) && ws_name_to_number(assignment->name) == parsed_num) { + gaps = assignment->gaps; + } + } + Con *output = get_assigned_output(num, parsed_num); /* if an assignment is not found, we create this workspace on the current output */ if (!output) { @@ -156,6 +171,7 @@ Con *workspace_get(const char *num) { workspace->workspace_layout = config.default_layout; workspace->num = parsed_num; workspace->type = CT_WORKSPACE; + workspace->gaps = gaps; con_attach(workspace, output_get_content(output), false); _workspace_apply_default_orientation(workspace); @@ -281,6 +297,15 @@ Con *create_workspace_on_output(Output *output, Con *content) { ws->num = c; sasprintf(&(ws->name), "%d", c); } + + struct Workspace_Assignment *assignment; + TAILQ_FOREACH (assignment, &ws_assignments, ws_assignments) { + if (strcmp(assignment->name, ws->name) == 0) { + ws->gaps = assignment->gaps; + break; + } + } + con_attach(ws, content, false); char *name; diff --git a/testcases/t/187-commands-parser.t b/testcases/t/187-commands-parser.t index 06dbce7e..3404df1f 100644 --- a/testcases/t/187-commands-parser.t +++ b/testcases/t/187-commands-parser.t @@ -179,6 +179,7 @@ is(parser_calls('unknown_literal'), title_window_icon mode bar + gaps )) . "'\n" . "ERROR: Your command: unknown_literal\n" . "ERROR: ^^^^^^^^^^^^^^^", diff --git a/testcases/t/201-config-parser.t b/testcases/t/201-config-parser.t index 3ab18483..af50d81d 100644 --- a/testcases/t/201-config-parser.t +++ b/testcases/t/201-config-parser.t @@ -515,6 +515,9 @@ my $expected_all_tokens = "ERROR: CONFIG: Expected one of these tokens: <end>, ' bar font mode + gaps + smart_borders + smart_gaps floating_minimum_size floating_maximum_size floating_modifier @@ -581,7 +584,7 @@ client.focused #4c7899 #285577 #ffffff #2e9ef4 EOT $expected = <<'EOT'; -ERROR: CONFIG: Expected one of these tokens: 'none', 'vertical', 'horizontal', 'both', 'smart', '1', 'yes', 'true', 'on', 'enable', 'active' +ERROR: CONFIG: Expected one of these tokens: 'none', 'vertical', 'horizontal', 'both', 'smart_no_gaps', 'smart', '1', 'yes', 'true', 'on', 'enable', 'active' ERROR: CONFIG: (in file <stdin>) ERROR: CONFIG: Line 1: hide_edge_borders FOOBAR ERROR: CONFIG: ^^^^^^ diff --git a/testcases/t/287-edge-borders.t b/testcases/t/287-edge-borders.t index 1fd69786..3dfc3407 100644 --- a/testcases/t/287-edge-borders.t +++ b/testcases/t/287-edge-borders.t @@ -160,4 +160,69 @@ is($tilewindow2->rect->width, $tiled[1]->{rect}->{width} - 2*2, 'second tiled bo exit_gracefully($pid); +##################################################################### +# 5: check that the borders are visible on a workspace with one tiled +# window and edge gaps +##################################################################### + +$config = <<EOT; +# i3 config file (v4) +font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1 + +new_window pixel 2 +new_float pixel 2 +gaps outer 5 +hide_edge_borders smart_no_gaps +EOT + +$pid = launch_with_config($config); + +$tmp = fresh_workspace; + +ok(@{get_ws_content($tmp)} == 0, 'no containers yet'); + +$tilewindow = open_window; + +$wscontent = get_ws($tmp); + +@tiled = @{$wscontent->{nodes}}; +ok(@tiled == 1, 'one tiled container opened'); +is($tiled[0]->{current_border_width}, 2, 'tiled current border width set to 2'); +is($tilewindow->rect->width, $tiled[0]->{rect}->{width} - 2*2, 'single tiled border width 2'); + +exit_gracefully($pid); + +##################################################################### +# 5: check that the borders are hidden on a workspace with one tiled +# window with no gaps +##################################################################### + +$config = <<EOT; +# i3 config file (v4) +font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1 + +new_window pixel 2 +new_float pixel 2 +hide_edge_borders smart_no_gaps +EOT + +$pid = launch_with_config($config); + +$tmp = fresh_workspace; + +ok(@{get_ws_content($tmp)} == 0, 'no containers yet'); + +$tilewindow = open_window; + +$wscontent = get_ws($tmp); + +@tiled = @{$wscontent->{nodes}}; +ok(@tiled == 1, 'one tiled container opened'); +is($tiled[0]->{current_border_width}, 2, 'tiled current border width set to 2'); +is($tilewindow->rect->width, $tiled[0]->{rect}->{width} - 2*0, 'single tiled border width 0'); + +exit_gracefully($pid); + + + done_testing; |