From 1e92def4b1eacffff3dd2c530c631511d24d5a85 Mon Sep 17 00:00:00 2001 From: Ian Fan Date: Sun, 27 Sep 2020 15:16:30 +0100 Subject: [PATCH] tray: implement d-bus menu --- include/swaybar/input.h | 4 +- include/swaybar/tray/item.h | 9 +- include/swaybar/tray/menu.h | 102 ++++ include/swaybar/tray/tray.h | 3 + swaybar/bar.c | 2 +- swaybar/input.c | 45 +- swaybar/meson.build | 1 + swaybar/render.c | 8 +- swaybar/tray/item.c | 53 +- swaybar/tray/menu.c | 952 ++++++++++++++++++++++++++++++++++++ swaybar/tray/tray.c | 2 + 11 files changed, 1145 insertions(+), 36 deletions(-) create mode 100644 include/swaybar/tray/menu.h create mode 100644 swaybar/tray/menu.c diff --git a/include/swaybar/input.h b/include/swaybar/input.h index e8735d883..a3e4f4d13 100644 --- a/include/swaybar/input.h +++ b/include/swaybar/input.h @@ -48,8 +48,8 @@ struct swaybar_hotspot { struct wl_list link; // swaybar_output::hotspots int x, y, width, height; enum hotspot_event_handling (*callback)(struct swaybar_output *output, - struct swaybar_hotspot *hotspot, double x, double y, uint32_t button, - void *data); + struct swaybar_hotspot *hotspot, struct wl_seat *seat, uint32_t serial, + double x, double y, uint32_t button, void *data); void (*destroy)(void *data); void *data; }; diff --git a/include/swaybar/tray/item.h b/include/swaybar/tray/item.h index c02a55823..44cc9bf38 100644 --- a/include/swaybar/tray/item.h +++ b/include/swaybar/tray/item.h @@ -7,6 +7,7 @@ #include "swaybar/tray/tray.h" #include "list.h" +struct swaybar_menu; struct swaybar_output; struct swaybar_pixmap { @@ -21,6 +22,7 @@ struct swaybar_sni_slot { const char *type; void *dest; sd_bus_slot *slot; + int menu_id; }; struct swaybar_sni { @@ -35,7 +37,7 @@ struct swaybar_sni { char *watcher_id; char *service; char *path; - char *interface; + const char *interface; char *status; char *icon_name; @@ -43,10 +45,13 @@ struct swaybar_sni { char *attention_icon_name; list_t *attention_icon_pixmap; // struct swaybar_pixmap * bool item_is_menu; - char *menu; + char *menu_path; char *icon_theme_path; // non-standard KDE property struct wl_list slots; // swaybar_sni_slot::link + + struct swaybar_menu_item *menu; + char **menu_icon_theme_paths; }; struct swaybar_sni *create_sni(char *id, struct swaybar_tray *tray); diff --git a/include/swaybar/tray/menu.h b/include/swaybar/tray/menu.h new file mode 100644 index 000000000..bd601ac6d --- /dev/null +++ b/include/swaybar/tray/menu.h @@ -0,0 +1,102 @@ +#ifndef _SWAYBAR_TRAY_MENU_H +#define _SWAYBAR_TRAY_MENU_H + +#include +#include +#include +#include "pool-buffer.h" +#include "xdg-shell-client-protocol.h" + +struct swaybar_tray; +struct swaybar_output; +struct swaybar_sni; +struct wl_seat; + + +/* MENU */ + +struct swaybar_menu_item { + struct swaybar_sni *sni; + struct swaybar_menu_item *parent; + + int32_t id; + + bool is_separator; // instead of type + char *label; + bool enabled; + bool visible; + char *icon_name; + cairo_surface_t *icon; + cairo_surface_t *icon_data; + + enum { + MENU_NONE, + MENU_CHECKMARK, + MENU_RADIO + } toggle_type; + int toggle_state; + + list_t *children; // struct swaybar_menu_item * +}; + +void destroy_menu(struct swaybar_menu_item *menu); + + +/* POPUP */ + +struct swaybar_popup_hotspot { + int y; + struct swaybar_menu_item *item; +}; + +struct swaybar_popup_surface { + struct swaybar_menu_item *item; + struct swaybar_popup_surface *child; + list_t *hotspots; // struct swaybar_popup_hotspot * + + struct xdg_popup *xdg_popup; + struct xdg_surface *xdg_surface; + struct wl_surface *surface; + struct pool_buffer buffers[2]; + struct pool_buffer *current_buffer; +}; + +struct swaybar_popup { + struct swaybar_tray *tray; + struct swaybar_sni *sni; + struct swaybar_output *output; + struct wl_seat *seat; + struct xdg_wm_base *wm_base; + + struct swaybar_popup_surface *popup_surface; + struct swaybar_popup_surface *pointer_focus; + struct swaybar_popup_hotspot **last_hover; + + // used to track clicks across callbacks + uint32_t serial; + int x, y; +}; + +void open_popup(struct swaybar_sni *sni, struct swaybar_output *output, + struct wl_seat *seat, uint32_t serial, int x, int y); +void destroy_popup(struct swaybar_popup *popup); + + +/* INPUT HOOKS + * These functions are called at the start of their respective counterparts in + * input.c, returning true if the event occurs on the popup instead of the bar + */ + +bool popup_pointer_enter(void *data, struct wl_pointer *wl_pointer, + uint32_t serial, struct wl_surface *surface, + wl_fixed_t surface_x, wl_fixed_t surface_y); +bool popup_pointer_leave(void *data, struct wl_pointer *wl_pointer, + uint32_t serial, struct wl_surface *surface); +bool popup_pointer_motion(void *data, struct wl_pointer *wl_pointer, + uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y); +bool popup_pointer_button(void *data, struct wl_pointer *wl_pointer, + uint32_t serial, uint32_t time, uint32_t button, uint32_t state); +bool popup_pointer_axis(void *data, struct wl_pointer *wl_pointer, + uint32_t time, uint32_t axis, wl_fixed_t value); + +#endif diff --git a/include/swaybar/tray/tray.h b/include/swaybar/tray/tray.h index 8958b69aa..a847cdbf4 100644 --- a/include/swaybar/tray/tray.h +++ b/include/swaybar/tray/tray.h @@ -14,6 +14,7 @@ struct swaybar; struct swaybar_output; +struct swaybar_popup; struct swaybar_watcher; struct swaybar_tray { @@ -30,6 +31,8 @@ struct swaybar_tray { list_t *basedirs; // char * list_t *themes; // struct swaybar_theme * + + struct swaybar_popup *popup; }; struct swaybar_tray *create_tray(struct swaybar *bar); diff --git a/swaybar/bar.c b/swaybar/bar.c index 231c1ad7a..6e09c4592 100644 --- a/swaybar/bar.c +++ b/swaybar/bar.c @@ -363,7 +363,7 @@ static void handle_global(void *data, struct wl_registry *registry, } } else if (strcmp(interface, zwlr_layer_shell_v1_interface.name) == 0) { bar->layer_shell = wl_registry_bind( - registry, name, &zwlr_layer_shell_v1_interface, 1); + registry, name, &zwlr_layer_shell_v1_interface, 2); } else if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) { bar->xdg_output_manager = wl_registry_bind(registry, name, &zxdg_output_manager_v1_interface, 2); diff --git a/swaybar/input.c b/swaybar/input.c index 4fe6dd934..13b7ac20b 100644 --- a/swaybar/input.c +++ b/swaybar/input.c @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -10,6 +11,10 @@ #include "swaybar/input.h" #include "swaybar/ipc.h" +#if HAVE_TRAY +#include "swaybar/tray/menu.h" +#endif + void free_hotspots(struct wl_list *list) { struct swaybar_hotspot *hotspot, *tmp; wl_list_for_each_safe(hotspot, tmp, list, link) { @@ -99,6 +104,11 @@ void update_cursor(struct swaybar_seat *seat) { static void wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, uint32_t serial, struct wl_surface *surface, wl_fixed_t surface_x, wl_fixed_t surface_y) { +#if HAVE_TRAY + if (popup_pointer_enter(data, wl_pointer, serial, surface, surface_x, surface_y)) { + return; + } +#endif struct swaybar_seat *seat = data; struct swaybar_pointer *pointer = &seat->pointer; pointer->serial = serial; @@ -114,6 +124,11 @@ static void wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, static void wl_pointer_leave(void *data, struct wl_pointer *wl_pointer, uint32_t serial, struct wl_surface *surface) { +#if HAVE_TRAY + if (popup_pointer_leave(data, wl_pointer, serial, surface)) { + return; + } +#endif struct swaybar_seat *seat = data; seat->pointer.current = NULL; } @@ -123,6 +138,11 @@ static void wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, struct swaybar_seat *seat = data; seat->pointer.x = wl_fixed_to_double(surface_x); seat->pointer.y = wl_fixed_to_double(surface_y); +#if HAVE_TRAY + if (popup_pointer_motion(data, wl_pointer, time, surface_x, surface_y)) { + return; + } +#endif } static bool check_bindings(struct swaybar *bar, uint32_t button, @@ -138,7 +158,7 @@ static bool check_bindings(struct swaybar *bar, uint32_t button, return false; } -static bool process_hotspots(struct swaybar_output *output, +static bool process_hotspots(struct swaybar_output *output, struct wl_seat *seat, uint32_t serial, double x, double y, uint32_t button) { double px = x * output->scale; double py = y * output->scale; @@ -147,8 +167,8 @@ static bool process_hotspots(struct swaybar_output *output, if (px >= hotspot->x && py >= hotspot->y && px < hotspot->x + hotspot->width && py < hotspot->y + hotspot->height) { - if (HOTSPOT_IGNORE == hotspot->callback(output, hotspot, x, y, - button, hotspot->data)) { + if (HOTSPOT_IGNORE == hotspot->callback(output, hotspot, seat, serial, + x, y, button, hotspot->data)) { return true; } } @@ -159,6 +179,12 @@ static bool process_hotspots(struct swaybar_output *output, static void wl_pointer_button(void *data, struct wl_pointer *wl_pointer, uint32_t serial, uint32_t time, uint32_t button, uint32_t state) { +#if HAVE_TRAY + if (popup_pointer_button(data, wl_pointer, serial, time, button, state)) { + return; + } +#endif + struct swaybar_seat *seat = data; struct swaybar_pointer *pointer = &seat->pointer; struct swaybar_output *output = pointer->current; @@ -173,7 +199,7 @@ static void wl_pointer_button(void *data, struct wl_pointer *wl_pointer, if (state != WL_POINTER_BUTTON_STATE_PRESSED) { return; } - process_hotspots(output, pointer->x, pointer->y, button); + process_hotspots(output, seat->wl_seat, serial, pointer->x, pointer->y, button); } static void workspace_next(struct swaybar *bar, struct swaybar_output *output, @@ -230,7 +256,7 @@ static void process_discrete_scroll(struct swaybar_seat *seat, return; } - if (process_hotspots(output, pointer->x, pointer->y, button)) { + if (process_hotspots(output, seat->wl_seat, 0, pointer->x, pointer->y, button)) { return; } @@ -269,6 +295,12 @@ static void process_continuous_scroll(struct swaybar_seat *seat, static void wl_pointer_axis(void *data, struct wl_pointer *wl_pointer, uint32_t time, uint32_t axis, wl_fixed_t value) { +#if HAVE_TRAY + if (popup_pointer_axis(data, wl_pointer, time, axis, value)) { + return; + } +#endif + struct swaybar_seat *seat = data; struct swaybar_pointer *pointer = &seat->pointer; struct swaybar_output *output = pointer->current; @@ -371,6 +403,7 @@ static struct touch_slot *get_touch_slot(struct swaybar_touch *touch, int32_t id static void wl_touch_down(void *data, struct wl_touch *wl_touch, uint32_t serial, uint32_t time, struct wl_surface *surface, int32_t id, wl_fixed_t _x, wl_fixed_t _y) { + // TODO popup struct swaybar_seat *seat = data; struct swaybar_output *_output = NULL, *output = NULL; wl_list_for_each(_output, &seat->bar->outputs, link) { @@ -403,7 +436,7 @@ static void wl_touch_up(void *data, struct wl_touch *wl_touch, } if (time - slot->time < 500) { // Tap, treat it like a pointer click - process_hotspots(slot->output, slot->x, slot->y, BTN_LEFT); + process_hotspots(slot->output, seat->wl_seat, serial, slot->x, slot->y, BTN_LEFT); } slot->output = NULL; } diff --git a/swaybar/meson.build b/swaybar/meson.build index 469145ae6..9a14666ba 100644 --- a/swaybar/meson.build +++ b/swaybar/meson.build @@ -2,6 +2,7 @@ tray_files = have_tray ? [ 'tray/host.c', 'tray/icon.c', 'tray/item.c', + 'tray/menu.c', 'tray/tray.c', 'tray/watcher.c' ] : [] diff --git a/swaybar/render.c b/swaybar/render.c index 8816abeff..95001e719 100644 --- a/swaybar/render.c +++ b/swaybar/render.c @@ -130,8 +130,8 @@ static void render_sharp_line(cairo_t *cairo, uint32_t color, } static enum hotspot_event_handling block_hotspot_callback( - struct swaybar_output *output, struct swaybar_hotspot *hotspot, - double x, double y, uint32_t button, void *data) { + struct swaybar_output *output, struct swaybar_hotspot *hotspot, struct wl_seat *seat, + uint32_t serial, double x, double y, uint32_t button, void *data) { struct i3bar_block *block = data; struct status_line *status = output->bar->status; return i3bar_block_send_click(status, block, x, y, @@ -554,8 +554,8 @@ static uint32_t render_binding_mode_indicator(cairo_t *cairo, } static enum hotspot_event_handling workspace_hotspot_callback( - struct swaybar_output *output, struct swaybar_hotspot *hotspot, - double x, double y, uint32_t button, void *data) { + struct swaybar_output *output, struct swaybar_hotspot *hotspot, struct wl_seat *seat, + uint32_t serial, double x, double y, uint32_t button, void *data) { if (button != BTN_LEFT) { return HOTSPOT_PROCESS; } diff --git a/swaybar/tray/item.c b/swaybar/tray/item.c index abb94cc2c..a43d92eb8 100644 --- a/swaybar/tray/item.c +++ b/swaybar/tray/item.c @@ -11,6 +11,7 @@ #include "swaybar/tray/host.h" #include "swaybar/tray/icon.h" #include "swaybar/tray/item.h" +#include "swaybar/tray/menu.h" #include "swaybar/tray/tray.h" #include "background-image.h" #include "cairo.h" @@ -18,8 +19,6 @@ #include "log.h" #include "wlr-layer-shell-unstable-v1-client-protocol.h" -// TODO menu - static bool sni_ready(struct swaybar_sni *sni) { return sni->status && (sni->status[0] == 'N' ? // NeedsAttention sni->attention_icon_name || sni->attention_icon_pixmap : @@ -74,7 +73,8 @@ static int read_pixmap(sd_bus_message *msg, struct swaybar_sni *sni, } if (height > 0 && width == height) { - sway_log(SWAY_DEBUG, "%s %s: found icon w:%d h:%d", sni->watcher_id, prop, width, height); + sway_log(SWAY_DEBUG, "%s %s: found icon %dx%d", sni->watcher_id, + prop, width, height); struct swaybar_pixmap *pixmap = malloc(sizeof(struct swaybar_pixmap) + npixels); pixmap->size = height; @@ -86,7 +86,8 @@ static int read_pixmap(sd_bus_message *msg, struct swaybar_sni *sni, list_add(pixmaps, pixmap); } else { - sway_log(SWAY_DEBUG, "%s %s: discard invalid icon w:%d h:%d", sni->watcher_id, prop, width, height); + sway_log(SWAY_DEBUG, "%s %s: discard invalid icon %dx%d", + sni->watcher_id, prop, width, height); } sd_bus_message_exit_container(msg); @@ -259,7 +260,7 @@ static void sni_match_signal_async(struct swaybar_sni *sni, char *signal, wl_list_insert(&sni->slots, &slot->link); } else { sway_log(SWAY_ERROR, "%s failed to subscribe to signal %s: %s", - sni->service, signal, strerror(-ret)); + sni->watcher_id, signal, strerror(-ret)); free(slot); } } @@ -292,7 +293,7 @@ struct swaybar_sni *create_sni(char *id, struct swaybar_tray *tray) { sni_get_property_async(sni, "AttentionIconName", "s", &sni->attention_icon_name); sni_get_property_async(sni, "AttentionIconPixmap", NULL, &sni->attention_icon_pixmap); sni_get_property_async(sni, "ItemIsMenu", "b", &sni->item_is_menu); - sni_get_property_async(sni, "Menu", "o", &sni->menu); + sni_get_property_async(sni, "Menu", "o", &sni->menu_path); sni_match_signal_async(sni, "NewIcon", handle_new_icon); sni_match_signal_async(sni, "NewAttentionIcon", handle_new_attention_icon); @@ -306,6 +307,14 @@ void destroy_sni(struct swaybar_sni *sni) { return; } + destroy_menu(sni->menu); + if (sni->menu_icon_theme_paths) { + for (char **path = sni->menu_icon_theme_paths; *path; ++path) { + free(path); + } + free(sni->menu_icon_theme_paths); + } + cairo_surface_destroy(sni->icon); free(sni->watcher_id); free(sni->service); @@ -315,7 +324,7 @@ void destroy_sni(struct swaybar_sni *sni) { list_free_items_and_destroy(sni->icon_pixmap); free(sni->attention_icon_name); list_free_items_and_destroy(sni->attention_icon_pixmap); - free(sni->menu); + free(sni->menu_path); free(sni->icon_theme_path); struct swaybar_sni_slot *slot, *slot_tmp; @@ -327,8 +336,8 @@ void destroy_sni(struct swaybar_sni *sni) { free(sni); } -static void handle_click(struct swaybar_sni *sni, int x, int y, - uint32_t button, int delta) { +static void handle_click(struct swaybar_sni *sni, struct swaybar_output *output, + struct wl_seat *seat, uint32_t serial, int x, int y, uint32_t button, int delta) { const char *method = NULL; struct tray_binding *binding = NULL; wl_list_for_each(binding, &sni->tray->bar->config->tray_bindings, link) { @@ -359,7 +368,9 @@ static void handle_click(struct swaybar_sni *sni, int x, int y, method = "ContextMenu"; } - if (strncmp(method, "Scroll", strlen("Scroll")) == 0) { + if (sni->menu_path && strcmp(method, "ContextMenu") == 0) { + open_popup(sni, output, seat, serial, x, y); + } else if (strncmp(method, "Scroll", strlen("Scroll")) == 0) { char dir = method[strlen("Scroll")]; char *orientation = (dir == 'U' || dir == 'D') ? "vertical" : "horizontal"; int sign = (dir == 'U' || dir == 'L') ? -1 : 1; @@ -367,8 +378,16 @@ static void handle_click(struct swaybar_sni *sni, int x, int y, sd_bus_call_method_async(sni->tray->bus, NULL, sni->service, sni->path, sni->interface, "Scroll", NULL, NULL, "is", delta*sign, orientation); } else { + // guess global position since wayland doesn't expose it + struct swaybar_config *config = sni->tray->bar->config; + int global_x = output->output_x + config->gaps.left + x; + bool top_bar = config->position & ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP; + int global_y = output->output_y + (top_bar ? config->gaps.top + y: + (int) output->output_height - config->gaps.bottom - y); + + sway_log(SWAY_DEBUG, "Guessing click position at (%d, %d)", global_x, global_y); sd_bus_call_method_async(sni->tray->bus, NULL, sni->service, sni->path, - sni->interface, method, NULL, NULL, "ii", x, y); + sni->interface, method, NULL, NULL, "ii", global_x, global_y); } } @@ -379,7 +398,7 @@ static int cmp_sni_id(const void *item, const void *cmp_to) { static enum hotspot_event_handling icon_hotspot_callback( struct swaybar_output *output, struct swaybar_hotspot *hotspot, - double x, double y, uint32_t button, void *data) { + struct wl_seat *seat, uint32_t serial, double x, double y, uint32_t button, void *data) { sway_log(SWAY_DEBUG, "Clicked on %s", (char *)data); struct swaybar_tray *tray = output->bar->tray; @@ -387,15 +406,7 @@ static enum hotspot_event_handling icon_hotspot_callback( if (idx != -1) { struct swaybar_sni *sni = tray->items->items[idx]; - // guess global position since wayland doesn't expose it - struct swaybar_config *config = tray->bar->config; - int global_x = output->output_x + config->gaps.left + x; - bool top_bar = config->position & ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP; - int global_y = output->output_y + (top_bar ? config->gaps.top + y: - (int) output->output_height - config->gaps.bottom - y); - - sway_log(SWAY_DEBUG, "Guessing click position at (%d, %d)", global_x, global_y); - handle_click(sni, global_x, global_y, button, 1); // TODO get delta from event + handle_click(sni, output, seat, serial, x, y, button, 1); // TODO get delta from event return HOTSPOT_IGNORE; } else { sway_log(SWAY_DEBUG, "but it doesn't exist"); diff --git a/swaybar/tray/menu.c b/swaybar/tray/menu.c new file mode 100644 index 000000000..f0176d621 --- /dev/null +++ b/swaybar/tray/menu.c @@ -0,0 +1,952 @@ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include "swaybar/bar.h" +#include "swaybar/config.h" +#include "swaybar/tray/icon.h" +#include "swaybar/tray/item.h" +#include "swaybar/tray/menu.h" +#include "swaybar/tray/tray.h" +#include "background-image.h" +#include "cairo.h" +#include "list.h" +#include "log.h" +#include "pango.h" +#include "wlr-layer-shell-unstable-v1-client-protocol.h" +#include "xdg-shell-client-protocol.h" + + +/* MENU */ + +static void close_popup(struct swaybar_popup *popup); +static void open_popup_id(struct swaybar_sni *sni, int id); + +static const char *menu_interface = "com.canonical.dbusmenu"; + +void destroy_menu(struct swaybar_menu_item *menu) { + if (!menu) { + return; + } + + struct swaybar_popup *popup = menu->sni->tray->popup; + if (popup && popup->sni == menu->sni) { + close_popup(popup); + } + + free(menu->label); + free(menu->icon_name); + cairo_surface_destroy(menu->icon_data); + if (menu->children) { + for (int i = 0; i < menu->children->length; ++i) { + destroy_menu(menu->children->items[i]); + } + list_free(menu->children); + } + free(menu); +} + +static struct swaybar_menu_item **menu_find_item(struct swaybar_menu_item **root, + int id) { + struct swaybar_menu_item *item = *root; + if (item->id == id) { + return root; + } + + if (item->children) { + for (int i = 0; i < item->children->length; ++i) { + struct swaybar_menu_item *child = item->children->items[i]; + struct swaybar_menu_item **res = menu_find_item(&child, id); + if (res) { + return res; + } + } + } + return NULL; +} + +struct png_stream { + const void *data; + size_t left; +}; + +static cairo_status_t read_png_stream(void *closure, unsigned char *data, + unsigned int length) { + struct png_stream *png_stream = closure; + if (length > png_stream->left) { + return CAIRO_STATUS_READ_ERROR; + } + memcpy(data, png_stream->data, length); + png_stream->data += length; + png_stream->left -= length; + return CAIRO_STATUS_SUCCESS; +} + +static cairo_surface_t *read_png(const void *data, size_t data_size) { + struct png_stream *png_stream = malloc(sizeof(struct png_stream)); + png_stream->data = data; + png_stream->left = data_size; + cairo_surface_t *surface = + cairo_image_surface_create_from_png_stream(read_png_stream, png_stream); + free(png_stream); + if (cairo_surface_status(surface) == CAIRO_STATUS_SUCCESS) { + return surface; + } else { + cairo_surface_destroy(surface); + return NULL; + } +} + +static int update_item_properties(struct swaybar_menu_item *item, + sd_bus_message *msg) { + sd_bus_message_enter_container(msg, 'a', "{sv}"); + while (!sd_bus_message_at_end(msg, 0)) { + sd_bus_message_enter_container(msg, 'e', "sv"); + char *key, *log_value; + sd_bus_message_read_basic(msg, 's', &key); + if (strcmp(key, "type") == 0) { + char *type; + sd_bus_message_read(msg, "v", "s", &type); + item->is_separator = strcmp(type, "separator") == 0; + log_value = type; + } else if (strcmp(key, "label") == 0) { + char *label; + sd_bus_message_read(msg, "v", "s", &label); + item->label = realloc(item->label, strlen(label) + 1); + if (!item->label) { + return -ENOMEM; + } + int i = 0; + for (char *c = label; *c; ++c) { + if (*c == '_' && !*++c) { + break; + } + item->label[i++] = *c; + } + item->label[i] = '\0'; + log_value = label; + } else if (strcmp(key, "enabled") == 0) { + int enabled; + sd_bus_message_read(msg, "v", "b", &enabled); + item->enabled = enabled; + log_value = item->enabled ? "true" : "false"; + } else if (strcmp(key, "visible") == 0) { + int visible; + sd_bus_message_read(msg, "v", "b", &visible); + item->visible = visible; + log_value = item->visible ? "true" : "false"; + } else if (strcmp(key, "icon-name") == 0) { + sd_bus_message_read(msg, "v", "s", &item->icon_name); + item->icon_name = strdup(item->icon_name); + log_value = item->icon_name; + } else if (strcmp(key, "icon-data") == 0) { + const void *data; + size_t data_size; + sd_bus_message_enter_container(msg, 'v', "ay"); + sd_bus_message_read_array(msg, 'y', &data, &data_size); + sd_bus_message_exit_container(msg); + item->icon_data = read_png(data, data_size); + log_value = item->icon_data ? "" : ""; + } else if (strcmp(key, "toggle-type") == 0) { + char *toggle_type; + sd_bus_message_read(msg, "v", "s", &toggle_type); + if (strcmp(toggle_type, "checkmark") == 0) { + item->toggle_type = MENU_CHECKMARK; + } else if (strcmp(toggle_type, "radio") == 0) { + item->toggle_type = MENU_RADIO; + } + log_value = toggle_type; + } else if (strcmp(key, "toggle-state") == 0) { + sd_bus_message_read(msg, "v", "i", &item->toggle_state); + log_value = item->toggle_state == 0 ? "off" : + item->toggle_state == 1 ? "on" : "indeterminate"; + } else if (strcmp(key, "children-display") == 0) { + char *children_display; + sd_bus_message_read(msg, "v", "s", &children_display); + if (strcmp(children_display, "submenu") == 0) { + item->children = create_list(); + if (!item->children) { + return -ENOMEM; + } + } + log_value = children_display; + } else { + // Ignored: shortcut, disposition + sd_bus_message_skip(msg, "v"); + log_value = ""; + } + sd_bus_message_exit_container(msg); + sway_log(SWAY_DEBUG, "%s%s %d %s = '%s'", item->sni->service, + item->sni->menu_path, item->id, key, log_value); + } + return sd_bus_message_exit_container(msg); +} + +static int get_layout_callback(sd_bus_message *msg, void *data, + sd_bus_error *error) { + struct swaybar_sni_slot *slot = data; + struct swaybar_sni *sni = slot->sni; + wl_list_remove(&slot->link); + free(slot); + + if (sd_bus_message_is_method_error(msg, NULL)) { + sway_log(SWAY_ERROR, "%s%s failed to get layout: %s", sni->service, + sni->menu_path, sd_bus_message_get_error(msg)->message); + return sd_bus_message_get_errno(msg); + } + + sd_bus_message_skip(msg, "u"); // menu revision + int ret = 0; + struct swaybar_menu_item *parent = NULL; + while (!sd_bus_message_at_end(msg, 1)) { + sd_bus_message_enter_container(msg, 'r', "ia{sv}av"); + + struct swaybar_menu_item *item = calloc(1, sizeof(struct swaybar_menu_item)); + if (!item) { + ret = -ENOMEM; + break; + } + + item->sni = sni; + item->parent = parent; + + // default properties + item->enabled = true; + item->visible = true; + item->toggle_state = -1; + + sd_bus_message_read_basic(msg, 'i', &item->id); + ret = update_item_properties(item, msg); + if (ret < 0) { + break; + } + if (parent) { + list_add(parent->children, item); + } else if (sni->menu) { + struct swaybar_menu_item **menu_ptr = menu_find_item(&sni->menu, + item->id); + destroy_menu(*menu_ptr); + *menu_ptr = item; + } else { + sni->menu = item; + } + + sd_bus_message_enter_container(msg, 'a', "v"); + parent = item; + while (parent && sd_bus_message_at_end(msg, 0)) { + parent = parent->parent; + sd_bus_message_exit_container(msg); + sd_bus_message_exit_container(msg); + sd_bus_message_exit_container(msg); + } + if (parent && parent->children) { + sd_bus_message_enter_container(msg, 'v', "(ia{sv}av)"); + } + } + + struct swaybar_popup *popup = sni->tray->popup; + if (ret < 0) { + sway_log(SWAY_ERROR, "%s%s failed to read menu layout: %s", + sni->service, sni->menu_path, strerror(-ret)); + destroy_menu(sni->menu); + sni->menu = NULL; + } else if (popup->sni == sni) { + if (popup->popup_surface) { + close_popup(popup); // TODO enhancement: redraw instead of closing + } else { + open_popup_id(sni, 0); + } + } + return ret; +} + +static void update_menu(struct swaybar_sni *sni, int id) { + struct swaybar_sni_slot *slot = calloc(1, sizeof(struct swaybar_sni_slot)); + slot->sni = sni; + + int ret = sd_bus_call_method_async(sni->tray->bus, &slot->slot, + sni->service, sni->menu_path, menu_interface, "GetLayout", + get_layout_callback, slot, "iias", id, -1, NULL); + if (ret >= 0) { + wl_list_insert(&sni->slots, &slot->link); + } else { + sway_log(SWAY_ERROR, "%s%s failed to get layout: %s", + sni->service, sni->menu_path, strerror(-ret)); + free(slot); + } +} + +static int handle_layout_updated(sd_bus_message *msg, void *data, + sd_bus_error *error) { + struct swaybar_sni *sni = data; + sway_log(SWAY_DEBUG, "%s%s layout updated", sni->service, sni->menu_path); + + int id; + sd_bus_message_read(msg, "ui", NULL, &id); + update_menu(sni, id); + return 0; +} + +static int handle_item_properties_updated(sd_bus_message *msg, void *data, + sd_bus_error *error) { + struct swaybar_sni *sni = data; + sway_log(SWAY_DEBUG, "%s%s item properties updated", sni->service, sni->menu_path); + + // update properties + sd_bus_message_enter_container(msg, 'a', "(ia{sv})"); + while (!sd_bus_message_at_end(msg, 0)) { + sd_bus_message_enter_container(msg, 'r', "ia{sv}"); + int id; + sd_bus_message_read_basic(msg, 'i', &id); + update_item_properties(*menu_find_item(&sni->menu, id), msg); + } + + // removed properties + sd_bus_message_enter_container(msg, 'a', "(ias)"); + while (!sd_bus_message_at_end(msg, 0)) { + sd_bus_message_enter_container(msg, 'r', "ia{sv}"); + int id; + sd_bus_message_read_basic(msg, 'i', &id); + struct swaybar_menu_item *item = *menu_find_item(&sni->menu, id); + + char **keys; + sd_bus_message_read_strv(msg, &keys); + if (keys) { + for (char **key = keys; *key; ++key) { + if (strcmp(*key, "type") == 0) { + item->is_separator = false; + } else if (strcmp(*key, "label") == 0) { + free(item->label); + item->label = NULL; + } else if (strcmp(*key, "enabled") == 0) { + item->enabled = true; + } else if (strcmp(*key, "visible") == 0) { + item->visible = true; + } else if (strcmp(*key, "children-display") == 0) { + for (int i = 0; i < item->children->length; ++i) { + destroy_menu(item->children->items[i]); + } + list_free(item->children); + } + } + } + } + + struct swaybar_popup *popup = sni->tray->popup; + if (popup->sni == sni) { + close_popup(popup); // TODO enhancement: redraw instead of closing + } + + return 0; +} + +static int handle_item_activation_requested(sd_bus_message *msg, void *data, + sd_bus_error *error) { + return 0; // TODO +} + +static void sni_menu_match_signal_async(struct swaybar_sni *sni, char *signal, + sd_bus_message_handler_t callback) { + struct swaybar_sni_slot *slot = calloc(1, sizeof(struct swaybar_sni_slot)); + int ret = sd_bus_match_signal_async(sni->tray->bus, &slot->slot, sni->service, + sni->menu_path, menu_interface, signal, callback, NULL, sni); + if (ret >= 0) { + wl_list_insert(&sni->slots, &slot->link); + } else { + sway_log(SWAY_ERROR, "%s%s failed to subscribe to signal %s: %s", + sni->service, sni->menu_path, signal, strerror(-ret)); + free(slot); + } +} + +static int get_icon_theme_path_callback(sd_bus_message *msg, void *data, + sd_bus_error *error) { + struct swaybar_sni_slot *slot = data; + struct swaybar_sni *sni = slot->sni; + wl_list_remove(&slot->link); + free(slot); + + int ret; + if (!sd_bus_message_is_method_error(msg, NULL)) { + ret = sd_bus_message_enter_container(msg, 'v', NULL); + if (ret >= 0) { + ret = sd_bus_message_read_strv(msg, &sni->menu_icon_theme_paths); + } + } else { + ret = -sd_bus_message_get_errno(msg); + } + + if (ret < 0) { + sway_log(SWAY_ERROR, "%s%s failed to read IconThemePath: %s", + sni->service, sni->menu_path, strerror(-ret)); + } + return ret; +} + +static void setup_menu(struct swaybar_sni *sni) { + struct swaybar_sni_slot *slot = calloc(1, sizeof(struct swaybar_sni_slot)); + slot->sni = sni; + int ret = sd_bus_call_method_async(sni->tray->bus, &slot->slot, sni->service, + sni->path, "org.freedesktop.DBus.Properties", "Get", + get_icon_theme_path_callback, slot, "ss", sni->interface, "IconThemePath"); + if (ret >= 0) { + wl_list_insert(&sni->slots, &slot->link); + } else { + sway_log(SWAY_ERROR, "%s%s failed to get IconThemePath: %s", + sni->service, sni->menu_path, strerror(-ret)); + free(slot); + } + + sni_menu_match_signal_async(sni, "ItemPropertiesUpdated", handle_item_properties_updated); + sni_menu_match_signal_async(sni, "LayoutUpdated", handle_layout_updated); + sni_menu_match_signal_async(sni, "ItemActivationRequested", handle_item_activation_requested); + + update_menu(sni, 0); +} + +/* POPUP */ + +static void destroy_popup_surface(struct swaybar_popup_surface *popup_surface) { + if (!popup_surface) { + return; + } + + destroy_popup_surface(popup_surface->child); + list_free_items_and_destroy(popup_surface->hotspots); + xdg_popup_destroy(popup_surface->xdg_popup); + wl_surface_destroy(popup_surface->surface); + destroy_buffer(&popup_surface->buffers[0]); + destroy_buffer(&popup_surface->buffers[1]); + + int id = popup_surface->item->id; + struct swaybar_sni *sni = popup_surface->item->sni; + sd_bus_call_method_async(sni->tray->bus, NULL, sni->service, sni->menu_path, + menu_interface, "Event", NULL, NULL, "isvu", id, "closed", "y", 0, time(NULL)); + sway_log(SWAY_DEBUG, "%s%s closed id %d", sni->service, sni->menu_path, id); + + free(popup_surface); +} + +static void close_popup(struct swaybar_popup *popup) { + if (!popup) { + return; + } + + destroy_popup_surface(popup->popup_surface); + popup->popup_surface = NULL; + popup->sni = NULL; +} + +void destroy_popup(struct swaybar_popup *popup) { + if (!popup) { + return; + } + + close_popup(popup); + popup->tray->popup = NULL; + xdg_wm_base_destroy(popup->wm_base); + free(popup); +} + +static void handle_global(void *data, struct wl_registry *registry, + uint32_t name, const char *interface, uint32_t version) { + struct swaybar_popup *popup = data; + if (strcmp(interface, xdg_wm_base_interface.name) == 0) { + popup->wm_base = wl_registry_bind(registry, name, &xdg_wm_base_interface, 1); + } +} + +static void handle_global_remove(void *data, struct wl_registry *registry, + uint32_t name) { + // intentionally left blank +} + +static const struct wl_registry_listener registry_listener = { + .global = handle_global, + .global_remove = handle_global_remove, +}; + +static struct swaybar_popup *create_popup(struct swaybar_tray *tray) { + struct swaybar_popup *popup = calloc(1, sizeof(struct swaybar_popup)); + if (!popup) { + return NULL; + } + + struct wl_registry *registry = wl_display_get_registry(tray->bar->display); + wl_registry_add_listener(registry, ®istry_listener, popup); + wl_display_roundtrip(tray->bar->display); + popup->tray = tray; + return popup; +} + +static void xdg_surface_handle_configure(void *data, + struct xdg_surface *xdg_surface, uint32_t serial) { + xdg_surface_ack_configure(xdg_surface, serial); +} + +static const struct xdg_surface_listener xdg_surface_listener = { + .configure = xdg_surface_handle_configure, +}; + +static void xdg_popup_configure(void *data, struct xdg_popup *xdg_popup, + int32_t x, int32_t y, int32_t width, int32_t height) { + // intentionally left blank +} + +static void xdg_popup_done(void *data, struct xdg_popup *xdg_popup) { + struct swaybar_popup *popup = data; + close_popup(popup); +} + +static const struct xdg_popup_listener xdg_popup_listener = { + .configure = xdg_popup_configure, + .popup_done = xdg_popup_done +}; + +static void show_popup_id(struct swaybar_sni *sni, int id) { + sway_log(SWAY_DEBUG, "%s%s showing popup for id %d", sni->service, sni->menu_path, id); + + cairo_surface_t *recorder = + cairo_recording_surface_create(CAIRO_CONTENT_COLOR_ALPHA, NULL); + cairo_t *cairo = cairo_create(recorder); + + list_t *hotspots = create_list(); + struct swaybar_popup_surface *popup_surface = + calloc(1, sizeof(struct swaybar_popup_surface)); + if (!(hotspots && popup_surface)) { + goto error; + } + + struct swaybar_tray *tray = sni->tray; + struct swaybar_popup *popup = tray->popup; + struct swaybar_output *output = popup->output; + + struct swaybar *bar = tray->bar; + struct swaybar_config *config = bar->config; + int padding = config->tray_padding * output->scale; + + struct swaybar_menu_item *root = *menu_find_item(&sni->menu, id); + list_t *items = root->children; + int height = 0; + for (int i = 0; i < items->length; ++i) { + struct swaybar_menu_item *item = items->items[i]; + + if (!item->visible) { + continue; + } + + if (item->is_separator) { + ++height; // drawn later, after the width is known + } else if (item->label) { + cairo_move_to(cairo, 0, height + padding); + + // draw label + if (item->enabled) { + cairo_set_source_u32(cairo, config->colors.focused_statusline); + } else { + uint32_t c = config->colors.focused_statusline; + uint32_t disabled_color = c - ((c & 0xFF) >> 1); + cairo_set_source_u32(cairo, disabled_color); + } + pango_printf(cairo, config->font, output->scale, false, "%s", item->label); + + // draw icon or menu indicator if needed + int text_height; + get_text_size(cairo, config->font, NULL, &text_height, NULL, + output->scale, false, "%s", item->label); + int size = 16; + int x = -2 * padding - size; + int y = height + padding + (text_height - size + 1) / 2; + cairo_set_source_u32(cairo, config->colors.focused_statusline); + if (item->icon_name) { + list_t *icon_search_paths = create_list(); + list_cat(icon_search_paths, tray->basedirs); + if (sni->menu_icon_theme_paths) { + for (char **path = sni->menu_icon_theme_paths; *path; ++path) { + list_add(icon_search_paths, *path); + } + } + if (sni->icon_theme_path) { + list_add(icon_search_paths, sni->icon_theme_path); + } + int min_size, max_size; + char *icon_path = find_icon(tray->themes, icon_search_paths, + item->icon_name, size, config->icon_theme, + &min_size, &max_size); + list_free(icon_search_paths); + + if (icon_path) { + cairo_surface_t *icon = load_background_image(icon_path); + free(icon_path); + cairo_surface_t *icon_scaled = + cairo_image_surface_scale(icon, size, size); + cairo_surface_destroy(icon); + + cairo_set_source_surface(cairo, icon_scaled, x, y); + cairo_rectangle(cairo, x, y, size, size); + cairo_fill(cairo); + cairo_surface_destroy(icon_scaled); + } + } else if (item->icon_data) { + cairo_surface_t *icon = + cairo_image_surface_scale(item->icon_data, size, size); + cairo_set_source_surface(cairo, icon, x, y); + cairo_rectangle(cairo, x, y, size, size); + cairo_fill(cairo); + cairo_surface_destroy(icon); + } else if (item->toggle_type == MENU_CHECKMARK) { + cairo_rectangle(cairo, x, y, size, size); + cairo_fill(cairo); + cairo_set_operator(cairo, CAIRO_OPERATOR_CLEAR); + if (item->toggle_state == 1) { // tick + cairo_move_to(cairo, x + size*3/4, y + size*5/16); + cairo_line_to(cairo, x + size*3/8, y + size*11/16); + cairo_line_to(cairo, x + size/4, y + size*9/16); + cairo_stroke(cairo); + } else if (item->toggle_state != 0) { // horizontal line + cairo_rectangle(cairo, x + size/4, y + size/2 - 1, size/2, 2); + cairo_fill(cairo); + } + cairo_set_operator(cairo, CAIRO_OPERATOR_OVER); + } else if (item->toggle_type == MENU_RADIO) { + cairo_arc(cairo, x + size/2, y + size/2, size/2, 0, 7); + cairo_fill(cairo); + if (item->toggle_state == 1) { + cairo_set_operator(cairo, CAIRO_OPERATOR_CLEAR); + cairo_arc(cairo, x + size/2, y + size/2, size/4, 0, 7); + cairo_fill(cairo); + cairo_set_operator(cairo, CAIRO_OPERATOR_OVER); + } + } else if (item->children) { // arrowhead + cairo_move_to(cairo, x + size/4, y + size/2); + cairo_line_to(cairo, x + size*3/4, y + size/4); + cairo_line_to(cairo, x + size*3/4, y + size*3/4); + cairo_fill(cairo); + } + + height += text_height + 2 * padding; + } else { + continue; + } + + struct swaybar_popup_hotspot *hotspot = + malloc(sizeof(struct swaybar_popup_hotspot)); + hotspot->y = height; + hotspot->item = item; + list_add(hotspots, hotspot); + } + + if (height == 0) { + goto error; + } + + // draw separators + double ox, w; + cairo_recording_surface_ink_extents(recorder, &ox, NULL, &w, NULL); + ox -= 2 * padding; + int width = w + 4 * padding; + + cairo_set_line_width(cairo, 1); + cairo_set_source_u32(cairo, config->colors.focused_separator); + for (int i = 0; i < hotspots->length; ++i) { + struct swaybar_popup_hotspot *hotspot = hotspots->items[i]; + if (hotspot->item->is_separator) { + cairo_move_to(cairo, ox, hotspot->y - 1/2); + cairo_line_to(cairo, ox + width, hotspot->y - 1/2); + cairo_stroke(cairo); + } + } + + // draw popup surface + popup_surface->current_buffer = get_next_buffer(tray->bar->shm, + popup_surface->buffers, width, height); + if (!popup_surface->current_buffer) { + goto error; + } + cairo_t *shm = popup_surface->current_buffer->cairo; + + cairo_set_operator(shm, CAIRO_OPERATOR_SOURCE); + cairo_set_source_u32(shm, config->colors.focused_background); + cairo_paint(shm); + + cairo_set_operator(shm, CAIRO_OPERATOR_OVER); + cairo_set_source_surface(shm, recorder, -ox, 0); + cairo_paint(shm); + + // configure & position popup surface + struct wl_surface *surface = wl_compositor_create_surface(bar->compositor); + struct xdg_surface *xdg_surface = + xdg_wm_base_get_xdg_surface(popup->wm_base, surface); + struct xdg_positioner *positioner = xdg_wm_base_create_positioner(popup->wm_base); + + int x = popup->x; + int y = popup->y; + xdg_positioner_set_anchor_rect(positioner, x, y, 1, 1); + xdg_positioner_set_offset(positioner, 0, 0); + xdg_positioner_set_size(positioner, width / output->scale, height / output->scale); + if (config->position & ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP) { // top bar + xdg_positioner_set_anchor(positioner, XDG_POSITIONER_ANCHOR_BOTTOM_LEFT); + xdg_positioner_set_gravity(positioner, XDG_POSITIONER_GRAVITY_BOTTOM_LEFT); + } else { + xdg_positioner_set_anchor(positioner, XDG_POSITIONER_ANCHOR_TOP_LEFT); + xdg_positioner_set_gravity(positioner, XDG_POSITIONER_GRAVITY_TOP_LEFT); + } + + struct xdg_popup *xdg_popup; + if (!popup->popup_surface) { // top-level popup + xdg_popup = xdg_surface_get_popup(xdg_surface, NULL, positioner); + zwlr_layer_surface_v1_get_popup(output->layer_surface, xdg_popup); + popup->popup_surface = popup_surface; + } else { // nested popup + struct swaybar_popup_surface *parent = popup->popup_surface; + while (parent->child) { + parent = parent->child; + } + xdg_popup = xdg_surface_get_popup(xdg_surface, parent->xdg_surface, positioner); + parent->child = popup_surface; + } + xdg_popup_grab(xdg_popup, popup->seat, popup->serial); + xdg_popup_add_listener(xdg_popup, &xdg_popup_listener, popup); + xdg_surface_add_listener(xdg_surface, &xdg_surface_listener, NULL); + wl_surface_commit(surface); + + wl_display_roundtrip(bar->display); + xdg_positioner_destroy(positioner); + + wl_surface_set_buffer_scale(surface, output->scale); + wl_surface_attach(surface, popup_surface->current_buffer->buffer, 0, 0); + wl_surface_damage(surface, 0, 0, width, height); + wl_surface_commit(surface); + + popup_surface->item = root; + popup_surface->hotspots = hotspots; + popup_surface->xdg_popup = xdg_popup; + popup_surface->xdg_surface = xdg_surface; + popup_surface->surface = surface; + + sd_bus_call_method_async(sni->tray->bus, NULL, sni->service, sni->menu_path, + menu_interface, "Event", NULL, NULL, "isvu", id, "opened", "y", 0, time(NULL)); + sway_log(SWAY_DEBUG, "%s%s opened id %d", sni->service, sni->menu_path, id); + +cleanup: + cairo_surface_destroy(recorder); + cairo_destroy(cairo); + return; +error: + list_free_items_and_destroy(hotspots); + free(popup_surface); + goto cleanup; +} + +static int about_to_show_callback(sd_bus_message *msg, void *data, + sd_bus_error *error) { + struct swaybar_sni_slot *slot = data; + struct swaybar_sni *sni = slot->sni; + int id = slot->menu_id; + wl_list_remove(&slot->link); + free(slot); + + int need_update; + sd_bus_message_read_basic(msg, 'b', &need_update); + if (need_update) { + update_menu(sni, id); + } else { + show_popup_id(sni, id); + } + return 0; +} + +static void open_popup_id(struct swaybar_sni *sni, int id) { + struct swaybar_sni_slot *slot = calloc(1, sizeof(struct swaybar_sni_slot)); + slot->sni = sni; + slot->menu_id = id; + + int ret = sd_bus_call_method_async(sni->tray->bus, &slot->slot, + sni->service, sni->menu_path, menu_interface, + "AboutToShow", about_to_show_callback, slot, "i", id); + if (ret >= 0) { + wl_list_insert(&sni->slots, &slot->link); + } else { + sway_log(SWAY_ERROR, "%s%s failed to send AboutToShow signal: %s", + sni->service, sni->menu_path, strerror(-ret)); + free(slot); + } +} + +void open_popup(struct swaybar_sni *sni, struct swaybar_output *output, + struct wl_seat *seat, uint32_t serial, int x, int y) { + sway_log(SWAY_DEBUG, "%s%s opening popup", sni->service, sni->menu_path); + + struct swaybar_tray *tray = sni->tray; + struct swaybar_popup *popup = tray->popup; + if (!popup) { + popup = tray->popup = create_popup(tray); + if (!popup) { + return; + } + } + + if (!sway_assert(!popup->popup_surface, "popup already open")) { + return; + } + + popup->sni = sni; + popup->output = output; + popup->seat = seat; + popup->serial = serial; + popup->x = x; + popup->y = y; + + if (sni->menu) { + open_popup_id(sni, 0); + } else { + setup_menu(sni); + } +} + +// input hooks + +bool popup_pointer_enter(void *data, struct wl_pointer *wl_pointer, + uint32_t serial, struct wl_surface *surface, + wl_fixed_t surface_x, wl_fixed_t surface_y) { + struct swaybar_seat *seat = data; + struct swaybar_tray *tray = seat->bar->tray; + if (!(tray && tray->popup)) { + return false; + } + + struct swaybar_popup *popup = tray->popup; + struct swaybar_popup_surface *popup_surface = popup->popup_surface; + while (popup_surface) { + if (popup_surface->surface == surface) { + struct swaybar_pointer *pointer = &seat->pointer; + pointer->current = popup->output; + pointer->serial = serial; + update_cursor(seat); + + popup->pointer_focus = popup_surface; + return true; + } + popup_surface = popup_surface->child; + } + return false; +} + +bool popup_pointer_leave(void *data, struct wl_pointer *wl_pointer, + uint32_t serial, struct wl_surface *surface) { + struct swaybar_seat *seat = data; + struct swaybar_tray *tray = seat->bar->tray; + if (!(tray && tray->popup)) { + return false; + } + + tray->popup->pointer_focus = NULL; + tray->popup->last_hover = NULL; + return false; +} + +bool popup_pointer_motion(void *data, struct wl_pointer *wl_pointer, + uint32_t time_, wl_fixed_t surface_x, wl_fixed_t surface_y) { + struct swaybar_seat *seat = data; + struct swaybar_tray *tray = seat->bar->tray; + if (!(tray && tray->popup && tray->popup->pointer_focus)) { + return false; + } + + struct swaybar_popup *popup = tray->popup; + double y = seat->pointer.y * popup->output->scale; + struct swaybar_popup_hotspot **hotspots_start = + (struct swaybar_popup_hotspot **)popup->pointer_focus->hotspots->items; + struct swaybar_popup_hotspot **hotspot_ptr = hotspots_start; + int step = 1; + if (popup->last_hover) { // calculate whether pointer went up or down + hotspot_ptr = popup->last_hover; + step = y < (*hotspot_ptr)->y ? -1 : 1; + hotspot_ptr += step; + } + + for (; hotspot_ptr >= hotspots_start; hotspot_ptr += step) { + if ((step == 1) == (y < (*hotspot_ptr)->y)) { + break; + } + } + + hotspot_ptr += step == -1; + struct swaybar_menu_item *item = (*hotspot_ptr)->item; + if (hotspot_ptr != popup->last_hover && item->enabled && !item->is_separator) { + struct swaybar_sni *sni = item->sni; + sd_bus_call_method_async(tray->bus, NULL, sni->service, + sni->menu_path, menu_interface, "Event", NULL, NULL, "isvu", + item->id, "hovered", "y", 0, time(NULL)); + sway_log(SWAY_DEBUG, "%s%s hovered id %d", sni->service, sni->menu_path, item->id); + } + popup->last_hover = hotspot_ptr; + + return true; +} + +bool popup_pointer_button(void *data, struct wl_pointer *wl_pointer, + uint32_t serial, uint32_t time_, uint32_t button, uint32_t state) { + struct swaybar_seat *seat = data; + struct swaybar_tray *tray = seat->bar->tray; + if (!(tray && tray->popup)) { + return false; + } + + struct swaybar_popup *popup = tray->popup; + float y = seat->pointer.y * popup->output->scale; + if (state != WL_POINTER_BUTTON_STATE_PRESSED) { + // intentionally left blank + } else if (!popup->pointer_focus) { + close_popup(popup); + } else if (button == BTN_LEFT) { + list_t *hotspots = popup->pointer_focus->hotspots; + for (int i = 0; i < hotspots->length; ++i) { + struct swaybar_popup_hotspot *hotspot = hotspots->items[i]; + if (y < hotspot->y) { + struct swaybar_menu_item *item = hotspot->item; + + if (!item->enabled || item->is_separator) { + break; + } + + struct swaybar_sni *sni = popup->sni; + if (item->children) { + destroy_popup_surface(popup->pointer_focus->child); + popup->pointer_focus->child = NULL; + popup->serial = serial; + popup->x = 0; + if (tray->bar->config->position & ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP) { // top bar + popup->y = i ? ((struct swaybar_popup_hotspot *)hotspots->items[i-1])->y : 0; + } else { + popup->y = hotspot->y; + } + open_popup_id(sni, item->id); + } else { + sd_bus_call_method_async(tray->bus, NULL, sni->service, + sni->menu_path, menu_interface, "Event", NULL, NULL, + "isvu", item->id, "clicked", "y", 0, time(NULL)); + sway_log(SWAY_DEBUG, "%s%s popup clicked id %d", + sni->service, sni->menu_path, item->id); + close_popup(popup); + } + break; + } + } + } + return popup->pointer_focus; +} + +bool popup_pointer_axis(void *data, struct wl_pointer *wl_pointer, + uint32_t time, uint32_t axis, wl_fixed_t value) { + struct swaybar_seat *seat = data; + struct swaybar_tray *tray = seat->bar->tray; + if (!(tray && tray->popup)) { + return false; + } + return tray->popup->pointer_focus; +} diff --git a/swaybar/tray/tray.c b/swaybar/tray/tray.c index 5fe6f9c31..1f28d2fe3 100644 --- a/swaybar/tray/tray.c +++ b/swaybar/tray/tray.c @@ -7,6 +7,7 @@ #include "swaybar/tray/icon.h" #include "swaybar/tray/host.h" #include "swaybar/tray/item.h" +#include "swaybar/tray/menu.h" #include "swaybar/tray/tray.h" #include "swaybar/tray/watcher.h" #include "list.h" @@ -76,6 +77,7 @@ void destroy_tray(struct swaybar_tray *tray) { if (!tray) { return; } + destroy_popup(tray->popup); finish_host(&tray->host_xdg); finish_host(&tray->host_kde); for (int i = 0; i < tray->items->length; ++i) {