#include "backend/x11/tray.h" #include #include #include #include #include #include #include #include #include "backend/x11/window.h" #include "base/array.h" #include "base/log.h" #include "base/macros.h" #include "base/memory.h" #include "interface/tray.h" #include "interface/types.h" #include "internal.h" typedef enum tray_opcode_t { SYSTEM_TRAY_REQUEST_DOCK = 0, SYSTEM_TRAY_BEGIN_MESSAGE = 1, SYSTEM_TRAY_CANCEL_MESSAGE = 2, } tray_opcode_t; typedef enum xembed_message_t { XEMBED_EMBEDDED_NOTIFY = 0, } xembed_message_t; static constexpr auto XEMBED_VERSION = 0; typedef struct tray_icon_t { xcb_window_t window; int32_t natural_width; int32_t natural_height; } tray_icon_t; typedef struct tray_host_t { bool enabled; bool initialized; xcb_atom_t selection_atom; xcb_window_t host_window; xcb_window_t container; int32_t icon_size; tray_icon_t *icons; size_t icon_count; size_t icon_capacity; tray_icons_change_cb_t icon_change_cb; void *cb_user_data; } tray_host_t; static tray_host_t *get_tray_host(void *handle) { backend_t *backend = handle; if (!backend) return nullptr; auto tray = backend->tray; if (!tray || !tray->enabled) return nullptr; return tray; } static size_t tray_find_icon(tray_host_t *tray, xcb_window_t window) { for (size_t i = 0; i < tray->icon_count; ++i) { if (tray->icons[i].window == window) return i; } return SIZE_MAX; } static tray_icon_t *tray_get_icon(tray_host_t *tray, xcb_window_t window) { for (size_t i = 0; i < tray->icon_count; ++i) { auto icon = &tray->icons[i]; if (icon->window == window) return icon; } return nullptr; } static inline void tray_dispatch_callback(tray_host_t *tray) { assert(tray); if (tray->icon_change_cb) tray->icon_change_cb(tray->cb_user_data); } static void tray_release_icon_window(backend_t *backend, tray_icon_t *icon) { if (!icon || icon->window == XCB_WINDOW_NONE) return; auto tray = get_tray_host(backend); if (!tray || !tray->initialized) return; auto conn = backend->conn; auto root = backend->screen->root; auto window = icon->window; auto mask = XCB_CW_EVENT_MASK; xcb_params_cw_t params = {.event_mask = XCB_EVENT_MASK_NO_EVENT}; xcb_change_save_set(conn, XCB_SET_MODE_DELETE, window); xcb_aux_change_window_attributes(conn, window, mask, ¶ms); xcb_reparent_window(conn, window, root, 0, 0); } static void tray_remove_icon_by_index( backend_t *backend, size_t index, bool release_window ) { auto tray = backend->tray; if (index >= tray->icon_count) return; auto icon = &tray->icons[index]; if (release_window) tray_release_icon_window(backend, icon); array_erase(tray->icons, tray->icon_count, index); tray_dispatch_callback(tray); } static zdwm_size_t tray_get_icon_size(tray_host_t *tray, size_t index) { auto target_size = MAX(tray->icon_size, (int32_t)1); auto natural_width = tray->icons[index].natural_width; auto natural_height = tray->icons[index].natural_height; if (natural_width == 0) natural_width = target_size; if (natural_height == 0) natural_height = target_size; double width_scale = (double)target_size / (double)natural_width; double height_scale = (double)target_size / (double)natural_height; auto scale = MIN(width_scale, height_scale); zdwm_size_t size = { .width = (int32_t)(natural_width * scale), .height = (int32_t)(natural_height * scale), }; return size; } static void tray_layout_icons(backend_t *backend, int32_t start_x) { auto tray = get_tray_host(backend); if (!tray || !tray->initialized) return; if (tray->container == XCB_WINDOW_NONE) return; auto conn = backend->conn; if (tray->icon_count == 0) { xcb_unmap_window(conn, tray->container); return; } auto height = tray->icon_size; auto width = height * (int32_t)tray->icon_count; xcb_configure_window_value_list_t value_list = { .x = start_x, .y = 0, .width = (uint32_t)width, .height = (uint32_t)height, }; uint16_t value_mask = XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y | XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT; xcb_configure_window_aux(conn, tray->container, value_mask, &value_list); xcb_map_window(conn, tray->container); for (size_t i = 0; i < tray->icon_count; ++i) { auto window = tray->icons[i].window; auto icon_size = tray_get_icon_size(tray, i); value_mask |= XCB_CONFIG_WINDOW_BORDER_WIDTH; auto offset_x = height * (int32_t)i; value_list = (xcb_configure_window_value_list_t){ .x = offset_x + (icon_size.width - height) / 2, .y = (icon_size.height - height) / 2, .width = icon_size.width, .height = icon_size.height, .border_width = 0, }; xcb_configure_window_aux(conn, window, value_mask, &value_list); xcb_map_window(conn, window); } } static window_id_t tray_host_window(void *handle) { auto tray = get_tray_host(handle); if (!tray) return ZDWM_WINDOW_ID_INVALID; return tray->host_window; } static size_t tray_icon_count(void *handle) { auto tray = get_tray_host(handle); return tray ? tray->icon_count : 0; } static int32_t tray_icon_size(void *handle) { auto tray = get_tray_host(handle); return tray ? tray->icon_size : 0; } static void tray_place(void *handle, int32_t x) { auto tray = get_tray_host(handle); if (!tray) return; tray_layout_icons((backend_t *)handle, x); } static void tray_set_listeners(void *handle, tray_icons_change_cb_t cb, void *user_data) { auto tray = get_tray_host(handle); if (!tray) return; tray->icon_change_cb = cb; tray->cb_user_data = user_data; } const tray_api_t tray_api = { .host_window = tray_host_window, .icon_count = tray_icon_count, .icon_size = tray_icon_size, .place = tray_place, .set_listener = tray_set_listeners, }; static bool tray_intern_selection_atom(tray_host_t *tray, backend_t *backend) { auto conn = backend->conn; auto screenp = backend->screenp; char selection_name[64] = {0}; auto atom_name_format = "_NET_SYSTEM_TRAY_S%d"; snprintf(selection_name, sizeof(selection_name), atom_name_format, screenp); uint16_t name_length = strlen(selection_name); auto cookie = xcb_intern_atom(conn, false, name_length, selection_name); auto reply = xcb_intern_atom_reply(conn, cookie, nullptr); if (!reply) { warn("cannot intern tray selection atom: %s", selection_name); return false; } tray->selection_atom = reply->atom; p_delete(&reply); return true; } static bool tray_create_container_window( tray_host_t *tray, backend_t *backend, tray_host_window_t host_window ) { auto conn = backend->conn; auto window_id = xcb_generate_id(conn); auto depth = backend->screen->root_depth; auto visual_id = backend->screen->root_visual; static xcb_colormap_t colormap = XCB_NONE; if (colormap == XCB_NONE) { colormap = xcb_generate_id(conn); auto root = backend->screen->root; uint8_t alloc = XCB_COLORMAP_ALLOC_NONE; xcb_create_colormap(conn, alloc, colormap, root, visual_id); } uint32_t value_mask = XCB_CW_BACK_PIXEL | XCB_CW_BORDER_PIXEL | XCB_CW_OVERRIDE_REDIRECT | XCB_CW_EVENT_MASK | XCB_CW_COLORMAP; xcb_create_window_value_list_t value_list = { .background_pixel = host_window.bg_pixel, .border_pixel = 0, .override_redirect = true, .event_mask = XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY | XCB_EVENT_MASK_STRUCTURE_NOTIFY, .colormap = colormap, }; auto cookie = xcb_create_window_aux_checked( conn, depth, window_id, host_window.window, (int16_t)host_window.width, 0, 1, (uint16_t)MAX(1, host_window.height), 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, visual_id, value_mask, &value_list ); if (xcb_request_check(conn, cookie)) { warn("cannot create system tray container window: 0x%x", window_id); return false; } tray->container = window_id; window_set_class_instance(conn, window_id); #define TRAY_CONTAINER_WINDOW_NAME APP_NAME "_tray" window_set_name_static(conn, window_id, TRAY_CONTAINER_WINDOW_NAME); auto name = TRAY_CONTAINER_WINDOW_NAME; auto name_len = sizeof(TRAY_CONTAINER_WINDOW_NAME) - 1; #undef TRAY_CONTAINER_WINDOW_NAME uint8_t mode = XCB_PROP_MODE_REPLACE; auto atoms = &backend->atoms; auto prop = atoms->_NET_WM_NAME; auto type = atoms->UTF8_STRING; xcb_change_property(conn, mode, window_id, prop, type, 8, name_len, name); return true; } static bool tray_take_selection_owner(tray_host_t *tray, backend_t *backend) { auto conn = backend->conn; auto atom = tray->selection_atom; auto cookie = xcb_get_selection_owner(conn, atom); auto reply = xcb_get_selection_owner_reply(conn, cookie, nullptr); if (reply && reply->owner != XCB_WINDOW_NONE) { warn( "system tray selection is already owned by window: 0x%x", reply->owner ); p_delete(&reply); return false; } p_delete(&reply); auto owner = tray->container; xcb_set_selection_owner(conn, owner, atom, XCB_CURRENT_TIME); cookie = xcb_get_selection_owner(conn, atom); reply = xcb_get_selection_owner_reply(conn, cookie, nullptr); bool success = reply && reply->owner == owner; p_delete(&reply); if (!success) warn("cannot acquire system tray selection"); return success; } static void tray_broadcast_manager(tray_host_t *tray, backend_t *backend) { auto conn = backend->conn; auto root = backend->screen->root; xcb_client_message_event_t event = { .response_type = XCB_CLIENT_MESSAGE, .window = root, .format = 32, .type = backend->atoms.MANAGER, .data.data32 = { [0] = XCB_CURRENT_TIME, [1] = tray->selection_atom, [2] = tray->container, }, }; uint32_t event_mask = XCB_EVENT_MASK_STRUCTURE_NOTIFY; xcb_send_event(conn, false, root, event_mask, (char *)&event); } void tray_init(backend_t *backend, tray_host_window_t host_window) { auto tray = get_tray_host(backend); if (!tray) { tray = p_new(tray_host_t, 1); backend->tray = tray; } if (tray->initialized) return; if (!tray_intern_selection_atom(tray, backend)) return; if (!tray_create_container_window(tray, backend, host_window)) return; if (!tray_take_selection_owner(tray, backend)) { tray_cleanup(backend); return; } tray->enabled = true; tray->initialized = true; tray->host_window = host_window.window; tray->icon_size = host_window.height; tray_broadcast_manager(tray, backend); tray_layout_icons(backend, host_window.width); xcb_flush(backend->conn); } void tray_cleanup(backend_t *backend) { auto tray = backend->tray; if (!tray) return; for (size_t i = 0; i < tray->icon_count; ++i) { tray_release_icon_window(backend, &tray->icons[i]); } p_delete(&tray->icons); tray->icon_count = 0; tray->icon_capacity = 0; auto conn = backend->conn; auto atom = tray->selection_atom; if (atom != XCB_ATOM_NONE) { xcb_set_selection_owner(conn, XCB_WINDOW_NONE, atom, XCB_CURRENT_TIME); } if (tray->container != XCB_WINDOW_NONE) { xcb_destroy_window(conn, tray->container); } p_clear(backend->tray, 1); p_delete(&backend->tray); } static void tray_select_icon_events(xcb_connection_t *conn, xcb_window_t window) { xcb_params_cw_t params = { .event_mask = XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_PROPERTY_CHANGE, }; uint32_t mask = XCB_CW_EVENT_MASK; xcb_aux_change_window_attributes(conn, window, mask, ¶ms); xcb_clear_area(conn, false, window, 0, 0, 0, 0); } /* reference: * https://specifications.freedesktop.org/xembed/latest-single/#id-1.7.4 */ typedef struct xembed_message_data_t { uint32_t message; /* message opcode */ uint32_t detail; /* message detail */ uint32_t data1; /* message data 1 */ uint32_t data2; /* message data 2 */ } xembed_message_data_t; static void tray_send_xembed_message( backend_t *backend, xcb_window_t window, xembed_message_data_t message ) { auto conn = backend->conn; auto message_type = backend->atoms._XEMBED; xcb_client_message_event_t event = { .response_type = XCB_CLIENT_MESSAGE, .format = 32, .window = window, .type = message_type, .data.data32 = { [0] = XCB_CURRENT_TIME, [1] = message.message, [2] = message.detail, [3] = message.data1, [4] = message.data2, }, }; uint32_t event_mask = XCB_EVENT_MASK_NO_EVENT; xcb_send_event(conn, false, window, event_mask, (char *)&event); } static void tray_add_icon(backend_t *backend, xcb_window_t window) { if (window == XCB_WINDOW_NONE) return; auto tray = get_tray_host(backend); if (!tray || !tray->initialized || tray_get_icon(tray, window)) return; rect_t geometry = {0}; if (!window_get_geometry(backend, window, &geometry)) return; auto icon = array_push(tray->icons, tray->icon_count, tray->icon_capacity); *icon = (tray_icon_t){ .window = window, .natural_width = geometry.width, .natural_height = geometry.height, }; auto conn = backend->conn; auto container = tray->container; xembed_message_data_t message = { .message = XEMBED_EMBEDDED_NOTIFY, .detail = 0, .data1 = container, .data2 = XEMBED_VERSION, }; xcb_change_save_set(conn, XCB_SET_MODE_INSERT, window); xcb_reparent_window(conn, window, tray->container, 0, 0); tray_select_icon_events(conn, window); tray_send_xembed_message(backend, window, message); xcb_map_window(conn, window); tray_dispatch_callback(tray); } bool tray_handle_client_message( backend_t *backend, const xcb_client_message_event_t *ev ) { auto tray = get_tray_host(backend); if (!tray || !tray->enabled || !tray->initialized) return false; if (ev->window != tray->container && ev->window != backend->screen->root) { return false; } auto opcode = ev->data.data32[1]; switch (opcode) { case SYSTEM_TRAY_REQUEST_DOCK: auto window = ev->data.data32[2]; tray_add_icon(backend, window); break; case SYSTEM_TRAY_BEGIN_MESSAGE: case SYSTEM_TRAY_CANCEL_MESSAGE: default: break; } xcb_flush(backend->conn); return true; } bool tray_handle_configure_request( backend_t *backend, const xcb_configure_request_event_t *ev ) { auto tray = get_tray_host(backend); if (!tray || !tray->initialized) return false; if (ev->window == tray->container) { tray_dispatch_callback(tray); return true; } auto icon = tray_get_icon(tray, ev->window); if (!icon) return false; if ((ev->value_mask & XCB_CONFIG_WINDOW_WIDTH) && ev->width > 0) { icon->natural_width = (int32_t)ev->width; } if ((ev->value_mask & XCB_CONFIG_WINDOW_HEIGHT) && ev->height > 0) { icon->natural_height = (int32_t)ev->height; } tray_dispatch_callback(tray); return true; } bool tray_handle_map_request( backend_t *backend, const xcb_map_request_event_t *ev ) { auto tray = get_tray_host(backend); if (!tray || !tray->initialized) return false; auto conn = backend->conn; if (ev->window == tray->container) { tray_dispatch_callback(tray); return true; } auto icon = tray_get_icon(tray, ev->window); if (!icon && ev->parent != tray->container) return false; xcb_map_window(conn, ev->window); xcb_flush(conn); tray_dispatch_callback(tray); return true; } bool tray_handle_property_notify( backend_t *backend, const xcb_property_notify_event_t *ev ) { auto tray = get_tray_host(backend); if (!tray || !tray->initialized) return false; if (ev->window == XCB_WINDOW_NONE) return false; if (ev->window == tray->container) return true; return tray_get_icon(tray, ev->window) != nullptr; } bool tray_handle_unmap_notify( backend_t *backend, const xcb_unmap_notify_event_t *ev ) { auto tray = get_tray_host(backend); if (!tray || !tray->initialized) return false; auto index = tray_find_icon(tray, ev->window); if (index == SIZE_MAX) return ev->window == tray->container; tray_remove_icon_by_index(backend, index, true); return true; } bool tray_handle_destroy_notify( backend_t *backend, const xcb_destroy_notify_event_t *ev ) { auto tray = get_tray_host(backend); if (!tray || !tray->initialized) return false; if (ev->window == tray->container) { tray_cleanup(backend); return true; } auto index = tray_find_icon(tray, ev->window); if (index == SIZE_MAX) return false; tray_remove_icon_by_index(backend, index, false); return true; }