Skip to content

Commit

Permalink
feat(i3wm): rounded corners
Browse files Browse the repository at this point in the history
Signed-off-by: Joseph Benden <joe@benden.us>
  • Loading branch information
jbenden committed Sep 25, 2023
1 parent 26608b7 commit ac9e6d6
Show file tree
Hide file tree
Showing 17 changed files with 206 additions and 2 deletions.
1 change: 1 addition & 0 deletions i3-save-tree
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ my %allowed_keys = map { ($_, 1) } qw(
fullscreen_mode
layout
border
border_radius
current_border_width
floating
percent
Expand Down
6 changes: 6 additions & 0 deletions include/commands.h
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ void cmd_resize(I3_CMD, const char *way, const char *direction, long resize_px,
*/
void cmd_border(I3_CMD, const char *border_style_str, long border_width);

/**
* Implementation of 'border_radius <px>'.
*
*/
void cmd_border_radius(I3_CMD, long border_radius);

/**
* Implementation of 'nop <comment>'.
*
Expand Down
6 changes: 6 additions & 0 deletions include/con.h
Original file line number Diff line number Diff line change
Expand Up @@ -571,3 +571,9 @@ void con_merge_into(Con *old, Con *new);
*
*/
bool con_inside_stacked_or_tabbed(Con *con);

/**
* Returns true if the containers has no configured outer gap.
*
*/
bool has_outer_gaps(gaps_t gaps);
1 change: 1 addition & 0 deletions include/config_directives.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ 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(border_radius, const long radius);
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);
Expand Down
3 changes: 3 additions & 0 deletions include/configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,9 @@ struct Config {

/* Disable gaps if there is only one container on the workspace */
smart_gaps_t smart_gaps;

/* Border radius specified in pixel units */
int32_t border_radius;
};

/**
Expand Down
3 changes: 3 additions & 0 deletions include/data.h
Original file line number Diff line number Diff line change
Expand Up @@ -809,4 +809,7 @@ struct Con {

/* The colormap for this con if a custom one is used. */
xcb_colormap_t colormap;

/* Border radius specified in pixel units */
uint32_t border_radius;
};
6 changes: 6 additions & 0 deletions parser-specs/commands.spec
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ state INITIAL:
'shmlog' -> SHMLOG
'debuglog' -> DEBUGLOG
'border' -> BORDER
'border_radius' -> BORDER_RADIUS
'layout' -> LAYOUT
'append_layout' -> APPEND_LAYOUT
'workspace' -> WORKSPACE
Expand Down Expand Up @@ -120,6 +121,11 @@ state BORDER_WIDTH:
border_width = number
-> call cmd_border($border_style, &border_width)

# border_radius <px>
state BORDER_RADIUS:
border_radius = number
-> call cmd_border_radius(&border_radius)

# 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'
Expand Down
6 changes: 6 additions & 0 deletions parser-specs/config.spec
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ state INITIAL:
'gaps' -> GAPS
'smart_borders' -> SMART_BORDERS
'smart_gaps' -> SMART_GAPS
'border_radius' -> BORDER_RADIUS
'floating_minimum_size' -> FLOATING_MINIMUM_SIZE_WIDTH
'floating_maximum_size' -> FLOATING_MAXIMUM_SIZE_WIDTH
'floating_modifier' -> FLOATING_MODIFIER
Expand Down Expand Up @@ -105,6 +106,11 @@ state INCLUDE:
pattern = string
-> call cfg_include($pattern)

# border_radius <radius>
state BORDER_RADIUS:
radius = number
-> call cfg_border_radius(&radius)

# floating_minimum_size <width> x <height>
state FLOATING_MINIMUM_SIZE_WIDTH:
width = number
Expand Down
20 changes: 20 additions & 0 deletions src/commands.c
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,26 @@ void cmd_border(I3_CMD, const char *border_style_str, long border_width) {
ysuccess(true);
}

/*
* Implementation of 'border_radius <px>'.
*
*/
void cmd_border_radius(I3_CMD, long border_radius) {
DLOG("border radius should be changed to %ld\n", border_radius);
owindow *current;

HANDLE_EMPTY_MATCH;

TAILQ_FOREACH (current, &owindows, owindows) {
DLOG("matching: %p / %s\n", current->con, current->con->name);

current->con->border_radius = border_radius;
}

cmd_output->needs_tree_render = true;
ysuccess(true);
}

/*
* Implementation of 'nop <comment>'.
*
Expand Down
4 changes: 3 additions & 1 deletion src/con.c
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Con *con_new_skeleton(Con *parent, i3Window *window) {
new->border_style = new->max_user_border_style = config.default_border;
new->current_border_width = -1;
new->window_icon_padding = -1;
new->border_radius = config.border_radius;
if (window) {
new->depth = window->depth;
} else {
Expand Down Expand Up @@ -1688,7 +1689,7 @@ Con *con_descend_direction(Con *con, direction_t direction) {
return con_descend_direction(most, direction);
}

static bool has_outer_gaps(gaps_t gaps) {
bool has_outer_gaps(gaps_t gaps) {
return gaps.top > 0 ||
gaps.right > 0 ||
gaps.bottom > 0 ||
Expand Down Expand Up @@ -1761,6 +1762,7 @@ static Rect con_border_style_rect_without_title(Con *con) {
if (borders_to_hide & ADJ_LOWER_SCREEN_EDGE) {
result.height += border_width;
}

return result;
}

Expand Down
2 changes: 2 additions & 0 deletions src/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,8 @@ bool load_configuration(const char *override_configpath, config_load_t load_type
config.gaps.bottom = 0;
config.gaps.left = 0;

config.border_radius = 0;

/* Set default urgency reset delay to 500ms */
if (config.workspace_urgency_timer == 0)
config.workspace_urgency_timer = 0.5;
Expand Down
4 changes: 4 additions & 0 deletions src/config_directives.c
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,10 @@ CFGFUN(smart_gaps, const char *enable) {
config.smart_gaps = boolstr(enable) ? SMART_GAPS_ON : SMART_GAPS_OFF;
}

CFGFUN(border_radius, const long radius) {
config.border_radius = radius;
}

CFGFUN(floating_minimum_size, const long width, const long height) {
config.floating_minimum_width = width;
config.floating_minimum_height = height;
Expand Down
3 changes: 3 additions & 0 deletions src/ipc.c
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,9 @@ void dump_node(yajl_gen gen, struct Con *con, bool inplace_restart) {
ystr("current_border_width");
y(integer, con->current_border_width);

ystr("border_radius");
y(integer, con->border_radius);

dump_rect(gen, "rect", con->rect);
if (con_draw_decoration_into_frame(con)) {
Rect simulated_deco_rect = con->deco_rect;
Expand Down
9 changes: 9 additions & 0 deletions src/load_layout.c
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,12 @@ static int json_string(void *ctx, const unsigned char *val, size_t len) {
LOG("Unhandled \"border\": %s\n", buf);
}
free(buf);
} else if (strcasecmp(last_key, "border_radius") == 0) {
long r;
if (!parse_long((const char *)val, &r, 10)) {
LOG("Unhandled \"border_radius\": %s\n", val);
}
json_node->border_radius = r;
} else if (strcasecmp(last_key, "type") == 0) {
char *buf = NULL;
sasprintf(&buf, "%.*s", (int)len, val);
Expand Down Expand Up @@ -474,6 +480,9 @@ static int json_int(void *ctx, long long val) {
if (strcasecmp(last_key, "num") == 0)
json_node->num = val;

if (strcasecmp(last_key, "border_radius") == 0)
json_node->border_radius = val;

if (strcasecmp(last_key, "current_border_width") == 0)
json_node->current_border_width = val;

Expand Down
132 changes: 131 additions & 1 deletion src/x.c
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,133 @@ static size_t x_get_border_rectangles(Con *con, xcb_rectangle_t rectangles[4]) {
return count;
}

static void x_shape_title(Con *con) {
if (con->border_radius == 0) {
return;
}

if (con->layout != L_TABBED && con->layout != L_STACKED) {
return;
}

uint16_t w = con->rect.width;
uint16_t h = con->rect.height;

xcb_pixmap_t pid = xcb_generate_id(conn);

xcb_create_pixmap(conn, 1, pid, con->frame.id, w, h);

xcb_gcontext_t black = xcb_generate_id(conn);
xcb_gcontext_t white = xcb_generate_id(conn);

xcb_create_gc(conn, black, pid, XCB_GC_FOREGROUND, (uint32_t[]){0, 0});
xcb_create_gc(conn, white, pid, XCB_GC_FOREGROUND, (uint32_t[]){1, 0});

int32_t r = con->border_radius;
int32_t d = r * 2;

// clang-format off
xcb_rectangle_t bounding = { 0, 0, w, h };

xcb_arc_t arcs[] = {
{ 0 , 1, d, d, 0, 360 << 6 },
{ w - d - 1, 1, d, d, 0, 360 << 6 },
};

xcb_rectangle_t rects[] = {
{ r, 0, w - d, h },
{ 0, r, w , h - r },
};
// clang-format on

xcb_poly_fill_rectangle(conn, pid, black, 1, &bounding);
xcb_poly_fill_rectangle(conn, pid, white, 2, rects);
xcb_poly_fill_arc(conn, pid, white, 2, arcs);

xcb_shape_mask(conn, XCB_SHAPE_SO_SET, XCB_SHAPE_SK_BOUNDING, con->frame.id, 0, 0, pid);
xcb_shape_mask(conn, XCB_SHAPE_SO_SET, XCB_SHAPE_SK_CLIP, con->frame.id, 0, 0, pid);

xcb_free_pixmap(conn, pid);
}

static bool smart_gaps_active(Con *con) {
return config.smart_gaps == SMART_GAPS_ON && con_num_visible_children(con->parent) <= 1;
}

static bool smart_gaps_has_gaps(Con *con) {
return smart_gaps_active(con) && !has_outer_gaps(calculate_effective_gaps(con));
}

/*
* Round window corners when possible
*
*/
static void x_shape_window(Con *con) {
if (con->border_radius == 0) {
return;
}

if (con->parent->type == CT_DOCKAREA) {
return;
}

if (con->fullscreen_mode ||
(!con_is_floating(con) && smart_gaps_has_gaps(con))) {
xcb_shape_mask(conn, XCB_SHAPE_SO_SET, XCB_SHAPE_SK_BOUNDING, con->frame.id, 0, 0, XCB_NONE);
xcb_shape_mask(conn, XCB_SHAPE_SO_SET, XCB_SHAPE_SK_CLIP, con->frame.id, 0, 0, XCB_NONE);
return;
}

const xcb_query_extension_reply_t *shape_query;
shape_query = xcb_get_extension_data(conn, &xcb_shape_id);

if (!shape_query->present) {
return;
}

uint16_t w = con->rect.width;
uint16_t h = con->rect.height;
uint16_t dh = con->deco_rect.height;

xcb_pixmap_t pid = xcb_generate_id(conn);

xcb_create_pixmap(conn, 1, pid, con->frame.id, w, h);

xcb_gcontext_t black = xcb_generate_id(conn);
xcb_gcontext_t white = xcb_generate_id(conn);

xcb_create_gc(conn, black, pid, XCB_GC_FOREGROUND, (uint32_t[]){0, 0});
xcb_create_gc(conn, white, pid, XCB_GC_FOREGROUND, (uint32_t[]){1, 0});

int32_t r = con->border_radius;
int32_t d = r * 2;

// clang-format off
xcb_rectangle_t bounding = { 0, 0, w, h };

xcb_arc_t arcs[] = {
{ 0 , -dh , d, d, 0, 360 << 6 },
{ 0 , h - d - 1, d, d, 0, 360 << 6 },
{ w - d - 1, -dh , d, d, 0, 360 << 6 },
{ w - d - 1, h - d - 1, d, d, 0, 360 << 6 },
};

xcb_rectangle_t rects[] = {
{ r, 0 , w - d, h },
{ 0, r - dh, w , h - d + dh },
};
// clang-format on

xcb_poly_fill_rectangle(conn, pid, black, 1, &bounding);
xcb_poly_fill_rectangle(conn, pid, white, 2, rects);
xcb_poly_fill_arc(conn, pid, white, 4, arcs);

xcb_shape_mask(conn, XCB_SHAPE_SO_SET, XCB_SHAPE_SK_BOUNDING, con->frame.id, 0, 0, pid);
xcb_shape_mask(conn, XCB_SHAPE_SO_SET, XCB_SHAPE_SK_CLIP, con->frame.id, 0, 0, pid);

xcb_free_pixmap(conn, pid);
}

/*
* Draws the decoration of the given container onto its parent.
*
Expand Down Expand Up @@ -757,6 +884,7 @@ void x_draw_decoration(Con *con) {

x_draw_decoration_after_title(con, p, dest_surface);
copy_pixmaps:
x_shape_window(con);
draw_util_copy_surface(&(con->frame_buffer), &(con->frame), 0, 0, 0, 0, con->rect.width, con->rect.height);
}

Expand Down Expand Up @@ -1035,11 +1163,13 @@ void x_push_node(Con *con) {
* TODO: Should this work the same way for L_TABBED? */
if (!con->parent ||
con->parent->layout != L_STACKED ||
TAILQ_FIRST(&(con->parent->focus_head)) == con)
TAILQ_FIRST(&(con->parent->focus_head)) == con) {
/* Render the decoration now to make the correct decoration visible
* from the very first moment. Later calls will be cached, so this
* doesn’t hurt performance. */
x_deco_recurse(con);
x_shape_title(con);
}
}

DLOG("setting rect (%d, %d, %d, %d)\n", rect.x, rect.y, rect.width, rect.height);
Expand Down
1 change: 1 addition & 0 deletions testcases/t/187-commands-parser.t
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ is(parser_calls('unknown_literal'),
shmlog
debuglog
border
border_radius
layout
append_layout
workspace
Expand Down
1 change: 1 addition & 0 deletions testcases/t/201-config-parser.t
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,7 @@ my $expected_all_tokens = "ERROR: CONFIG: Expected one of these tokens: <end>, '
gaps
smart_borders
smart_gaps
border_radius
floating_minimum_size
floating_maximum_size
floating_modifier
Expand Down

0 comments on commit ac9e6d6

Please sign in to comment.