From abbf6a85d712ef1790b02122eb432e4bafe20576 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Sun, 13 Jun 2021 08:35:52 +0200 Subject: [PATCH] Implement showing window icons in titlebar (#4439) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This feature defaults to off, and can be turned on for individual windows, or (with for_window) for all new windows. See the userguide change. This commit is partially based on work by: • Marius Muja • mickael9 • Esteve Varela Colominas • Bernardo Menicagli --- RELEASE-NOTES-next | 1 + docs/userguide | 26 ++++++ include/commands.h | 6 ++ include/data.h | 9 +++ include/i3-atoms_rest.xmacro.h | 1 + include/libi3.h | 11 +++ include/window.h | 6 ++ libi3/boolstr.c | 25 ++++++ libi3/draw_util.c | 24 ++++++ meson.build | 1 + parser-specs/commands.spec | 15 ++++ src/commands.c | 29 +++++++ src/con.c | 1 + src/config_directives.c | 35 +++----- src/handlers.c | 12 ++- src/ipc.c | 3 + src/load_layout.c | 4 + src/manage.c | 4 + src/window.c | 110 ++++++++++++++++++++++++++ src/x.c | 32 +++++++- testcases/t/116-nestedcons.t | 1 + testcases/t/187-commands-parser.t | 1 + testcases/t/314-window-icon-padding.t | 62 +++++++++++++++ 23 files changed, 392 insertions(+), 27 deletions(-) create mode 100644 libi3/boolstr.c create mode 100644 testcases/t/314-window-icon-padding.t diff --git a/RELEASE-NOTES-next b/RELEASE-NOTES-next index e6a892d6a..3b6de2711 100644 --- a/RELEASE-NOTES-next +++ b/RELEASE-NOTES-next @@ -36,6 +36,7 @@ option is enabled and only then sets a screenshot as background. • i3-dump-log -f now uses UNIX sockets instead of pthreads. The UNIX socket approach should be more reliable and also more portable. • Implement the include config directive + • Implement optionally showing window icons in titlebar • Allow for_window to match against WM_CLIENT_MACHINE • Add %machine placeholder (WM_CLIENT_MACHINE) to title_format • Allow multiple output names in 'move container|workspace to output' diff --git a/docs/userguide b/docs/userguide index 944f7b398..05eb4aaca 100644 --- a/docs/userguide +++ b/docs/userguide @@ -2703,6 +2703,32 @@ for_window [class=".*"] title_format "%title" for_window [class="(?i)firefox"] title_format "%title" ------------------------------------------------------------------------------------- +[[title_window_icon]] +=== Window title icon + +By default, i3 does not display the window icon in the title bar. + +Starting with i3 v4.20, you can optionally enable window icons either for +specific windows or for all windows (using the <> directive). + +*Syntax*: +----------------------------- +title_window_icon +title_window_icon padding +------------------------------ + +*Examples*: +------------------------------------------------------------------------------------- +# show the window icon for the focused window to make it stand out +bindsym $mod+p title_window_icon on + +# enable window icons for all windows +for_window [class=".*"] title_window_icon on + +# enable window icons for all windows with extra horizontal padding +for_window [class=".*"] title_window_icon padding 3px +------------------------------------------------------------------------------------- + === Changing border style To change the border of the current client, you can use +border normal+ to use the normal diff --git a/include/commands.h b/include/commands.h index 47ac5dbf7..c27cba4a7 100644 --- a/include/commands.h +++ b/include/commands.h @@ -331,3 +331,9 @@ void cmd_shmlog(I3_CMD, const char *argument); * */ void cmd_debuglog(I3_CMD, const char *argument); + +/** + * Implementation of 'title_window_icon ' and 'title_window_icon padding ' + * + */ +void cmd_title_window_icon(I3_CMD, const char *enable, int padding); diff --git a/include/data.h b/include/data.h index 1d47af643..0679adf77 100644 --- a/include/data.h +++ b/include/data.h @@ -15,6 +15,7 @@ #include #include #include +#include #include "queue.h" @@ -471,6 +472,9 @@ struct Window { double min_aspect_ratio; double max_aspect_ratio; + /** Window icon, as Cairo surface */ + cairo_surface_t *icon; + /** The window has a nonrectangular shape. */ bool shaped; /** The window has a nonrectangular input shape. */ @@ -656,6 +660,11 @@ struct Con { /** The format with which the window's name should be displayed. */ char *title_format; + /** Whether the window icon should be displayed, and with what padding. -1 + * means display no window icon (default behavior), 0 means display without + * any padding, 1 means display with 1 pixel of padding and so on. */ + int window_icon_padding; + /* a sticky-group is an identifier which bundles several containers to a * group. The contents are shared between all of them, that is they are * displayed on whichever of the containers is currently visible */ diff --git a/include/i3-atoms_rest.xmacro.h b/include/i3-atoms_rest.xmacro.h index 288a4211b..ef50d81ba 100644 --- a/include/i3-atoms_rest.xmacro.h +++ b/include/i3-atoms_rest.xmacro.h @@ -3,6 +3,7 @@ xmacro(_NET_WM_USER_TIME) \ xmacro(_NET_STARTUP_ID) \ xmacro(_NET_WORKAREA) \ +xmacro(_NET_WM_ICON) \ xmacro(WM_PROTOCOLS) \ xmacro(WM_DELETE_WINDOW) \ xmacro(UTF8_STRING) \ diff --git a/include/libi3.h b/include/libi3.h index a93b3ff96..d98fa1fe5 100644 --- a/include/libi3.h +++ b/include/libi3.h @@ -611,6 +611,11 @@ color_t draw_util_hex_to_color(const char *color); */ void draw_util_text(i3String *text, surface_t *surface, color_t fg_color, color_t bg_color, int x, int y, int max_width); +/** + * Draw the given image using libi3. + */ +void draw_util_image(cairo_surface_t *image, surface_t *surface, int x, int y, int width, int height); + /** * Draws a filled rectangle. * This function is a convenience wrapper and takes care of flushing the @@ -668,3 +673,9 @@ void set_screenshot_as_wallpaper(xcb_connection_t *conn, xcb_screen_t *screen); * content of the window. */ bool is_background_set(xcb_connection_t *conn, xcb_screen_t *screen); + +/** + * Reports whether str represents the enabled state (1, yes, true, …). + * + */ +bool boolstr(const char *str); diff --git a/include/window.h b/include/window.h index 858bf0cdc..9759cc4b8 100644 --- a/include/window.h +++ b/include/window.h @@ -101,3 +101,9 @@ void window_update_motif_hints(i3Window *win, xcb_get_property_reply_t *prop, bo * */ void window_update_machine(i3Window *win, xcb_get_property_reply_t *prop); + +/** + * Updates the _NET_WM_ICON + * + */ +void window_update_icon(i3Window *win, xcb_get_property_reply_t *prop); diff --git a/libi3/boolstr.c b/libi3/boolstr.c new file mode 100644 index 000000000..0fa417dd8 --- /dev/null +++ b/libi3/boolstr.c @@ -0,0 +1,25 @@ +/* + * vim:ts=4:sw=4:expandtab + * + * i3 - an improved dynamic tiling window manager + * © 2009 Michael Stapelberg and contributors (see also: LICENSE) + * + */ +#include "libi3.h" + +#include +#include +#include + +/* + * Reports whether str represents the enabled state (1, yes, true, …). + * + */ +bool boolstr(const char *str) { + return (strcasecmp(str, "1") == 0 || + strcasecmp(str, "yes") == 0 || + strcasecmp(str, "true") == 0 || + strcasecmp(str, "on") == 0 || + strcasecmp(str, "enable") == 0 || + strcasecmp(str, "active") == 0); +} diff --git a/libi3/draw_util.c b/libi3/draw_util.c index ad2e6aa21..dd0900ceb 100644 --- a/libi3/draw_util.c +++ b/libi3/draw_util.c @@ -141,6 +141,30 @@ void draw_util_text(i3String *text, surface_t *surface, color_t fg_color, color_ cairo_surface_mark_dirty(surface->surface); } +/** + * Draw the given image using libi3. + * This function is a convenience wrapper and takes care of flushing the + * surface as well as restoring the cairo state. + * + */ +void draw_util_image(cairo_surface_t *image, surface_t *surface, int x, int y, int width, int height) { + RETURN_UNLESS_SURFACE_INITIALIZED(surface); + + cairo_save(surface->cr); + + cairo_translate(surface->cr, x, y); + + const int src_width = cairo_image_surface_get_width(image); + const int src_height = cairo_image_surface_get_height(image); + double scale = MIN((double)width / src_width, (double)height / src_height); + cairo_scale(surface->cr, scale, scale); + + cairo_set_source_surface(surface->cr, image, 0, 0); + cairo_paint(surface->cr); + + cairo_restore(surface->cr); +} + /* * Draws a filled rectangle. * This function is a convenience wrapper and takes care of flushing the diff --git a/meson.build b/meson.build index d86e9dcdf..00cd9e94d 100644 --- a/meson.build +++ b/meson.build @@ -325,6 +325,7 @@ ev_dep = cc.find_library('ev') inc = include_directories('include') libi3srcs = [ + 'libi3/boolstr.c', 'libi3/create_socket.c', 'libi3/dpi.c', 'libi3/draw_util.c', diff --git a/parser-specs/commands.spec b/parser-specs/commands.spec index 4ec3e22f3..43c784a20 100644 --- a/parser-specs/commands.spec +++ b/parser-specs/commands.spec @@ -40,6 +40,7 @@ state INITIAL: 'scratchpad' -> SCRATCHPAD 'swap' -> SWAP 'title_format' -> TITLE_FORMAT + 'title_window_icon' -> TITLE_WINDOW_ICON 'mode' -> MODE 'bar' -> BAR @@ -462,6 +463,20 @@ state TITLE_FORMAT: format = string -> call cmd_title_format($format) +state TITLE_WINDOW_ICON: + 'padding' + -> TITLE_WINDOW_ICON_PADDING + enable = '1', 'yes', 'true', 'on', 'enable', 'active', '0', 'no', 'false', 'off', 'disable', 'inactive' + -> call cmd_title_window_icon($enable, 0) + +state TITLE_WINDOW_ICON_PADDING: + end + -> call cmd_title_window_icon($enable, &padding) + 'px' + -> call cmd_title_window_icon($enable, &padding) + padding = number + -> + # bar (hidden_state hide|show|toggle)|(mode dock|hide|invisible|toggle) [] state BAR: 'hidden_state' diff --git a/src/commands.c b/src/commands.c index d256299df..798e2f6c1 100644 --- a/src/commands.c +++ b/src/commands.c @@ -2037,6 +2037,35 @@ void cmd_title_format(I3_CMD, const char *format) { ysuccess(true); } +/* + * Implementation of 'title_window_icon ' and 'title_window_icon padding ' + * + */ +void cmd_title_window_icon(I3_CMD, const char *enable, int padding) { + if (enable != NULL && !boolstr(enable)) { + padding = -1; + } + DLOG("setting window_icon=%d\n", padding); + HANDLE_EMPTY_MATCH; + + owindow *current; + TAILQ_FOREACH (current, &owindows, owindows) { + DLOG("setting window_icon for %p / %s\n", current->con, current->con->name); + current->con->window_icon_padding = padding; + + if (current->con->window != NULL) { + /* Make sure the window title is redrawn immediately. */ + current->con->window->name_x_changed = true; + } else { + /* For windowless containers we also need to force the redrawing. */ + FREE(current->con->deco_render_params); + } + } + + cmd_output->needs_tree_render = true; + ysuccess(true); +} + /* * Implementation of 'rename workspace [] to ' * diff --git a/src/con.c b/src/con.c index b935bed66..183382354 100644 --- a/src/con.c +++ b/src/con.c @@ -43,6 +43,7 @@ Con *con_new_skeleton(Con *parent, i3Window *window) { new->window = window; new->border_style = config.default_border; new->current_border_width = -1; + new->window_icon_padding = -1; if (window) { new->depth = window->depth; } else { diff --git a/src/config_directives.c b/src/config_directives.c index 1e792fe07..c6b839c2c 100644 --- a/src/config_directives.c +++ b/src/config_directives.c @@ -124,15 +124,6 @@ CFGFUN(criteria_add, const char *ctype, const char *cvalue) { * Utility functions ******************************************************************************/ -static bool eval_boolstr(const char *str) { - return (strcasecmp(str, "1") == 0 || - strcasecmp(str, "yes") == 0 || - strcasecmp(str, "true") == 0 || - strcasecmp(str, "on") == 0 || - strcasecmp(str, "enable") == 0 || - strcasecmp(str, "active") == 0); -} - /* * A utility function to convert a string containing the group and modifiers to * the corresponding bit mask. @@ -316,14 +307,14 @@ CFGFUN(hide_edge_borders, const char *borders) { config.hide_edge_borders = HEBM_BOTH; else if (strcmp(borders, "none") == 0) config.hide_edge_borders = HEBM_NONE; - else if (eval_boolstr(borders)) + else if (boolstr(borders)) config.hide_edge_borders = HEBM_VERTICAL; else config.hide_edge_borders = HEBM_NONE; } CFGFUN(focus_follows_mouse, const char *value) { - config.disable_focus_follows_mouse = !eval_boolstr(value); + config.disable_focus_follows_mouse = !boolstr(value); } CFGFUN(mouse_warping, const char *value) { @@ -334,11 +325,11 @@ CFGFUN(mouse_warping, const char *value) { } CFGFUN(force_xinerama, const char *value) { - config.force_xinerama = eval_boolstr(value); + config.force_xinerama = boolstr(value); } CFGFUN(disable_randr15, const char *value) { - config.disable_randr15 = eval_boolstr(value); + config.disable_randr15 = boolstr(value); } CFGFUN(focus_wrapping, const char *value) { @@ -346,7 +337,7 @@ CFGFUN(focus_wrapping, const char *value) { config.focus_wrapping = FOCUS_WRAPPING_FORCE; } else if (strcmp(value, "workspace") == 0) { config.focus_wrapping = FOCUS_WRAPPING_WORKSPACE; - } else if (eval_boolstr(value)) { + } else if (boolstr(value)) { config.focus_wrapping = FOCUS_WRAPPING_ON; } else { config.focus_wrapping = FOCUS_WRAPPING_OFF; @@ -355,7 +346,7 @@ CFGFUN(focus_wrapping, const char *value) { CFGFUN(force_focus_wrapping, const char *value) { /* Legacy syntax. */ - if (eval_boolstr(value)) { + if (boolstr(value)) { config.focus_wrapping = FOCUS_WRAPPING_FORCE; } else { /* For "force_focus_wrapping off", don't enable or disable @@ -367,7 +358,7 @@ CFGFUN(force_focus_wrapping, const char *value) { } CFGFUN(workspace_back_and_forth, const char *value) { - config.workspace_auto_back_and_forth = eval_boolstr(value); + config.workspace_auto_back_and_forth = boolstr(value); } CFGFUN(fake_outputs, const char *outputs) { @@ -409,7 +400,7 @@ CFGFUN(title_align, const char *alignment) { } CFGFUN(show_marks, const char *value) { - config.show_marks = eval_boolstr(value); + config.show_marks = boolstr(value); } static char *current_workspace = NULL; @@ -597,7 +588,7 @@ CFGFUN(bar_output, const char *output) { } CFGFUN(bar_verbose, const char *verbose) { - current_bar->verbose = eval_boolstr(verbose); + current_bar->verbose = boolstr(verbose); } CFGFUN(bar_modifier, const char *modifiers) { @@ -717,11 +708,11 @@ CFGFUN(bar_status_command, const char *command) { } CFGFUN(bar_binding_mode_indicator, const char *value) { - current_bar->hide_binding_mode_indicator = !eval_boolstr(value); + current_bar->hide_binding_mode_indicator = !boolstr(value); } CFGFUN(bar_workspace_buttons, const char *value) { - current_bar->hide_workspace_buttons = !eval_boolstr(value); + current_bar->hide_workspace_buttons = !boolstr(value); } CFGFUN(bar_workspace_min_width, const long width) { @@ -729,11 +720,11 @@ CFGFUN(bar_workspace_min_width, const long width) { } CFGFUN(bar_strip_workspace_numbers, const char *value) { - current_bar->strip_workspace_numbers = eval_boolstr(value); + current_bar->strip_workspace_numbers = boolstr(value); } CFGFUN(bar_strip_workspace_name, const char *value) { - current_bar->strip_workspace_name = eval_boolstr(value); + current_bar->strip_workspace_name = boolstr(value); } CFGFUN(bar_start) { diff --git a/src/handlers.c b/src/handlers.c index fe9677738..f8016b032 100644 --- a/src/handlers.c +++ b/src/handlers.c @@ -1185,6 +1185,14 @@ static bool handle_i3_floating(Con *con, xcb_get_property_reply_t *prop) { return true; } +static bool handle_windowicon_change(Con *con, xcb_get_property_reply_t *prop) { + window_update_icon(con->window, prop); + + x_push_changes(croot); + + return true; +} + /* Returns false if the event could not be processed (e.g. the window could not * be found), true otherwise */ typedef bool (*cb_property_handler_t)(Con *con, xcb_get_property_reply_t *property); @@ -1208,7 +1216,8 @@ static struct property_handler_t property_handlers[] = { {0, UINT_MAX, handle_window_type}, {0, UINT_MAX, handle_i3_floating}, {0, 128, handle_machine_change}, - {0, 5 * sizeof(uint64_t), handle_motif_hints_change}}; + {0, 5 * sizeof(uint64_t), handle_motif_hints_change}, + {0, UINT_MAX, handle_windowicon_change}}; #define NUM_HANDLERS (sizeof(property_handlers) / sizeof(struct property_handler_t)) /* @@ -1232,6 +1241,7 @@ void property_handlers_init(void) { property_handlers[10].atom = A_I3_FLOATING_WINDOW; property_handlers[11].atom = XCB_ATOM_WM_CLIENT_MACHINE; property_handlers[12].atom = A__MOTIF_WM_HINTS; + property_handlers[13].atom = A__NET_WM_ICON; } static void property_notify(uint8_t state, xcb_window_t window, xcb_atom_t atom) { diff --git a/src/ipc.c b/src/ipc.c index b21d79a10..b50648e3e 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -498,6 +498,9 @@ void dump_node(yajl_gen gen, struct Con *con, bool inplace_restart) { ystr(con->title_format); } + ystr("window_icon_padding"); + y(integer, con->window_icon_padding); + if (con->type == CT_WORKSPACE) { ystr("num"); y(integer, con->num); diff --git a/src/load_layout.c b/src/load_layout.c index 3d8033e8f..7bdf5521b 100644 --- a/src/load_layout.c +++ b/src/load_layout.c @@ -455,6 +455,10 @@ static int json_int(void *ctx, long long val) { if (strcasecmp(last_key, "current_border_width") == 0) json_node->current_border_width = val; + if (strcasecmp(last_key, "window_icon_padding") == 0) { + json_node->window_icon_padding = val; + } + if (strcasecmp(last_key, "depth") == 0) json_node->depth = val; diff --git a/src/manage.c b/src/manage.c index 8ea820de5..ee046f0f3 100644 --- a/src/manage.c +++ b/src/manage.c @@ -119,6 +119,8 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki wm_normal_hints_cookie, motif_wm_hints_cookie, wm_user_time_cookie, wm_desktop_cookie, wm_machine_cookie; + xcb_get_property_cookie_t wm_icon_cookie; + geomc = xcb_get_geometry(conn, d); /* Check if the window is mapped (it could be not mapped when intializing and @@ -191,6 +193,7 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki wm_user_time_cookie = GET_PROPERTY(A__NET_WM_USER_TIME, UINT32_MAX); wm_desktop_cookie = GET_PROPERTY(A__NET_WM_DESKTOP, UINT32_MAX); wm_machine_cookie = GET_PROPERTY(XCB_ATOM_WM_CLIENT_MACHINE, UINT32_MAX); + wm_icon_cookie = GET_PROPERTY(A__NET_WM_ICON, UINT32_MAX); i3Window *cwindow = scalloc(1, sizeof(i3Window)); cwindow->id = window; @@ -204,6 +207,7 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki window_update_class(cwindow, xcb_get_property_reply(conn, class_cookie, NULL)); window_update_name_legacy(cwindow, xcb_get_property_reply(conn, title_cookie, NULL)); window_update_name(cwindow, xcb_get_property_reply(conn, utf8_title_cookie, NULL)); + window_update_icon(cwindow, xcb_get_property_reply(conn, wm_icon_cookie, NULL)); window_update_leader(cwindow, xcb_get_property_reply(conn, leader_cookie, NULL)); window_update_transient_for(cwindow, xcb_get_property_reply(conn, transient_cookie, NULL)); window_update_strut_partial(cwindow, xcb_get_property_reply(conn, strut_cookie, NULL)); diff --git a/src/window.c b/src/window.c index 2c2f6c0a8..c1fde4f97 100644 --- a/src/window.c +++ b/src/window.c @@ -19,6 +19,7 @@ void window_free(i3Window *win) { FREE(win->class_class); FREE(win->class_instance); i3string_free(win->name); + cairo_surface_destroy(win->icon); FREE(win->ran_assignments); FREE(win); } @@ -484,3 +485,112 @@ void window_update_machine(i3Window *win, xcb_get_property_reply_t *prop) { free(prop); } + +void window_update_icon(i3Window *win, xcb_get_property_reply_t *prop) { + uint32_t *data = NULL; + uint32_t width, height; + uint64_t len = 0; + const uint32_t pref_size = (uint32_t)(render_deco_height() - logical_px(2)); + + if (!prop || prop->type != XCB_ATOM_CARDINAL || prop->format != 32) { + DLOG("_NET_WM_ICON is not set\n"); + FREE(prop); + return; + } + + uint32_t prop_value_len = xcb_get_property_value_length(prop); + uint32_t *prop_value = (uint32_t *)xcb_get_property_value(prop); + + /* Find an icon matching the preferred size. + * If there is no such icon, take the smallest icon having at least + * the preferred size. + * If all icons are smaller than the preferred size, chose the largest. + */ + while (prop_value_len > (sizeof(uint32_t) * 2) && prop_value && + prop_value[0] && prop_value[1]) { + const uint32_t cur_width = prop_value[0]; + const uint32_t cur_height = prop_value[1]; + /* Check that the property is as long as it should be (in bytes), + handling integer overflow. "+2" to handle the width and height + fields. */ + const uint64_t cur_len = cur_width * (uint64_t)cur_height; + const uint64_t expected_len = (cur_len + 2) * 4; + + if (expected_len > prop_value_len) { + break; + } + + DLOG("Found _NET_WM_ICON of size: (%d,%d)\n", cur_width, cur_height); + + const bool at_least_preferred_size = (cur_width >= pref_size && + cur_height >= pref_size); + const bool smaller_than_current = (cur_width < width || + cur_height < height); + const bool larger_than_current = (cur_width > width || + cur_height > height); + const bool not_yet_at_preferred = (width < pref_size || + height < pref_size); + if (len == 0 || + (at_least_preferred_size && + (smaller_than_current || not_yet_at_preferred)) || + (!at_least_preferred_size && + not_yet_at_preferred && + larger_than_current)) { + len = cur_len; + width = cur_width; + height = cur_height; + data = prop_value; + } + + if (width == pref_size && height == pref_size) { + break; + } + + /* Find pointer to next icon in the reply. */ + prop_value_len -= expected_len; + prop_value = (uint32_t *)(((uint8_t *)prop_value) + expected_len); + } + + if (!data) { + DLOG("Could not get _NET_WM_ICON\n"); + FREE(prop); + return; + } + + DLOG("Using icon of size (%d,%d) (preferred size: %d)\n", + width, height, pref_size); + + win->name_x_changed = true; /* trigger a redraw */ + + uint32_t *icon = smalloc(len * 4); + + for (uint64_t i = 0; i < len; i++) { + uint8_t r, g, b, a; + const uint32_t pixel = data[2 + i]; + a = (pixel >> 24) & 0xff; + r = (pixel >> 16) & 0xff; + g = (pixel >> 8) & 0xff; + b = (pixel >> 0) & 0xff; + + /* Cairo uses premultiplied alpha */ + r = (r * a) / 0xff; + g = (g * a) / 0xff; + b = (b * a) / 0xff; + + icon[i] = ((uint32_t)a << 24) | (r << 16) | (g << 8) | b; + } + + if (win->icon != NULL) { + cairo_surface_destroy(win->icon); + } + win->icon = cairo_image_surface_create_for_data( + (unsigned char *)icon, + CAIRO_FORMAT_ARGB32, + width, + height, + width * 4); + static cairo_user_data_key_t free_data; + cairo_surface_set_user_data(win->icon, &free_data, icon, free); + + FREE(prop); +} diff --git a/src/x.c b/src/x.c index c9ef07f7a..55dc63a0b 100644 --- a/src/x.c +++ b/src/x.c @@ -612,11 +612,36 @@ void x_draw_decoration(Con *con) { /* 5: draw title border */ x_draw_title_border(con, p); - /* 6: draw the title */ + /* 6: draw the icon and title */ int text_offset_y = (con->deco_rect.height - config.font.height) / 2; + int text_offset_x = 0; + + struct Window *win = con->window; const int title_padding = logical_px(2); const int deco_width = (int)con->deco_rect.width; + + /* Draw the icon */ + if (con->window_icon_padding > -1 && win && win->icon) { + /* icon_padding is applied horizontally only, + * the icon will always use all available vertical space. */ + const int icon_padding = logical_px(1 + con->window_icon_padding); + + const uint16_t icon_size = con->deco_rect.height - 2 * logical_px(1); + + const int icon_offset_y = logical_px(1); + + text_offset_x += icon_size + 2 * icon_padding; + + draw_util_image( + win->icon, + &(parent->frame_buffer), + con->deco_rect.x + icon_padding, + con->deco_rect.y + icon_offset_y, + icon_size, + icon_size); + } + int mark_width = 0; if (config.show_marks && !TAILQ_EMPTY(&(con->marks_head))) { char *formatted_mark = sstrdup(""); @@ -655,7 +680,6 @@ void x_draw_decoration(Con *con) { } i3String *title = NULL; - struct Window *win = con->window; if (win == NULL) { if (con->title_format == NULL) { char *_title; @@ -699,9 +723,9 @@ void x_draw_decoration(Con *con) { draw_util_text(title, &(parent->frame_buffer), p->color->text, p->color->background, - con->deco_rect.x + title_offset_x, + con->deco_rect.x + text_offset_x + title_offset_x, con->deco_rect.y + text_offset_y, - deco_width - mark_width - 2 * title_padding); + deco_width - text_offset_x - mark_width - 2 * title_padding); if (win == NULL || con->title_format != NULL) { I3STRING_FREE(title); diff --git a/testcases/t/116-nestedcons.t b/testcases/t/116-nestedcons.t index 6a2274d4d..7b15c5f0e 100644 --- a/testcases/t/116-nestedcons.t +++ b/testcases/t/116-nestedcons.t @@ -73,6 +73,7 @@ my $expected = { workspace_layout => 'default', current_border_width => -1, marks => $ignore, + window_icon_padding => -1, }; # a shallow copy is sufficient, since we only ignore values at the root diff --git a/testcases/t/187-commands-parser.t b/testcases/t/187-commands-parser.t index 2ff5e2123..06dbce7e7 100644 --- a/testcases/t/187-commands-parser.t +++ b/testcases/t/187-commands-parser.t @@ -176,6 +176,7 @@ is(parser_calls('unknown_literal'), scratchpad swap title_format + title_window_icon mode bar )) . "'\n" . diff --git a/testcases/t/314-window-icon-padding.t b/testcases/t/314-window-icon-padding.t new file mode 100644 index 000000000..5320ab9f8 --- /dev/null +++ b/testcases/t/314-window-icon-padding.t @@ -0,0 +1,62 @@ +#!perl +# vim:ts=4:sw=4:expandtab +# +# Please read the following documents before working on tests: +# • https://build.i3wm.org/docs/testsuite.html +# (or docs/testsuite) +# +# • https://build.i3wm.org/docs/lib-i3test.html +# (alternatively: perldoc ./testcases/lib/i3test.pm) +# +# • https://build.i3wm.org/docs/ipc.html +# (or docs/ipc) +# +# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf +# (unless you are already familiar with Perl) +# +# Verifies title_window_icon behavior. +use i3test i3_autostart => 0; + +sub window_icon_padding { + my ($ws) = @_; + my ($nodes, $focus) = get_ws_content($ws); + ok(@{$nodes} == 1, 'precisely one container on workspace'); + return $nodes->[0]->{'window_icon_padding'}; +} + +my $config = <<"EOT"; +# i3 config file (v4) +font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1 +EOT +my $pid = launch_with_config($config); + +my $tmp = fresh_workspace; + +cmd 'open'; +is(window_icon_padding($tmp), -1, 'window_icon_padding defaults to -1'); + +cmd 'title_window_icon on'; +isnt(window_icon_padding($tmp), -1, 'window_icon_padding no longer -1'); + +exit_gracefully($pid); + +################################################################################ +# Verify title_window_icon can be used with for_window as expected +################################################################################ + +$config = <<"EOT"; +# i3 config file (v4) +font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1 + +for_window [class=".*"] title_window_icon padding 3px +EOT +$pid = launch_with_config($config); + +$tmp = fresh_workspace; + +open_window; +is(window_icon_padding($tmp), 3, 'window_icon_padding set to 3'); + +exit_gracefully($pid); + +done_testing;