diff options
Diffstat (limited to 'swaybar/tray/item.c')
-rw-r--r-- | swaybar/tray/item.c | 472 |
1 files changed, 472 insertions, 0 deletions
diff --git a/swaybar/tray/item.c b/swaybar/tray/item.c new file mode 100644 index 00000000..0833dcb9 --- /dev/null +++ b/swaybar/tray/item.c | |||
@@ -0,0 +1,472 @@ | |||
1 | #define _POSIX_C_SOURCE 200809L | ||
2 | #include <cairo.h> | ||
3 | #include <stdbool.h> | ||
4 | #include <stdlib.h> | ||
5 | #include <string.h> | ||
6 | #include "swaybar/bar.h" | ||
7 | #include "swaybar/config.h" | ||
8 | #include "swaybar/input.h" | ||
9 | #include "swaybar/tray/host.h" | ||
10 | #include "swaybar/tray/icon.h" | ||
11 | #include "swaybar/tray/item.h" | ||
12 | #include "swaybar/tray/tray.h" | ||
13 | #include "background-image.h" | ||
14 | #include "cairo.h" | ||
15 | #include "list.h" | ||
16 | #include "log.h" | ||
17 | #include "wlr-layer-shell-unstable-v1-client-protocol.h" | ||
18 | |||
19 | // TODO menu | ||
20 | |||
21 | static bool sni_ready(struct swaybar_sni *sni) { | ||
22 | return sni->status && (sni->status[0] == 'N' ? // NeedsAttention | ||
23 | sni->attention_icon_name || sni->attention_icon_pixmap : | ||
24 | sni->icon_name || sni->icon_pixmap); | ||
25 | } | ||
26 | |||
27 | static void set_sni_dirty(struct swaybar_sni *sni) { | ||
28 | if (sni_ready(sni)) { | ||
29 | sni->min_size = sni->max_size = 0; // invalidate previous icon | ||
30 | set_bar_dirty(sni->tray->bar); | ||
31 | } | ||
32 | } | ||
33 | |||
34 | static int read_pixmap(sd_bus_message *msg, struct swaybar_sni *sni, | ||
35 | const char *prop, list_t **dest) { | ||
36 | int ret = sd_bus_message_enter_container(msg, 'a', "(iiay)"); | ||
37 | if (ret < 0) { | ||
38 | wlr_log(WLR_ERROR, "%s %s: %s", sni->watcher_id, prop, strerror(-ret)); | ||
39 | return ret; | ||
40 | } | ||
41 | |||
42 | if (sd_bus_message_at_end(msg, 0)) { | ||
43 | wlr_log(WLR_DEBUG, "%s %s no. of icons = 0", sni->watcher_id, prop); | ||
44 | return ret; | ||
45 | } | ||
46 | |||
47 | list_t *pixmaps = create_list(); | ||
48 | if (!pixmaps) { | ||
49 | return -12; // -ENOMEM | ||
50 | } | ||
51 | |||
52 | while (!sd_bus_message_at_end(msg, 0)) { | ||
53 | ret = sd_bus_message_enter_container(msg, 'r', "iiay"); | ||
54 | if (ret < 0) { | ||
55 | wlr_log(WLR_ERROR, "%s %s: %s", sni->watcher_id, prop, strerror(-ret)); | ||
56 | goto error; | ||
57 | } | ||
58 | |||
59 | int size; | ||
60 | ret = sd_bus_message_read(msg, "ii", NULL, &size); | ||
61 | if (ret < 0) { | ||
62 | wlr_log(WLR_ERROR, "%s %s: %s", sni->watcher_id, prop, strerror(-ret)); | ||
63 | goto error; | ||
64 | } | ||
65 | |||
66 | const void *pixels; | ||
67 | size_t npixels; | ||
68 | ret = sd_bus_message_read_array(msg, 'y', &pixels, &npixels); | ||
69 | if (ret < 0) { | ||
70 | wlr_log(WLR_ERROR, "%s %s: %s", sni->watcher_id, prop, strerror(-ret)); | ||
71 | goto error; | ||
72 | } | ||
73 | |||
74 | struct swaybar_pixmap *pixmap = | ||
75 | malloc(sizeof(struct swaybar_pixmap) + npixels); | ||
76 | pixmap->size = size; | ||
77 | memcpy(pixmap->pixels, pixels, npixels); | ||
78 | list_add(pixmaps, pixmap); | ||
79 | |||
80 | sd_bus_message_exit_container(msg); | ||
81 | } | ||
82 | list_free_items_and_destroy(*dest); | ||
83 | *dest = pixmaps; | ||
84 | wlr_log(WLR_DEBUG, "%s %s no. of icons = %d", sni->watcher_id, prop, | ||
85 | pixmaps->length); | ||
86 | |||
87 | return ret; | ||
88 | error: | ||
89 | list_free_items_and_destroy(pixmaps); | ||
90 | return ret; | ||
91 | } | ||
92 | |||
93 | struct get_property_data { | ||
94 | struct swaybar_sni *sni; | ||
95 | const char *prop; | ||
96 | const char *type; | ||
97 | void *dest; | ||
98 | }; | ||
99 | |||
100 | static int get_property_callback(sd_bus_message *msg, void *data, | ||
101 | sd_bus_error *error) { | ||
102 | struct get_property_data *d = data; | ||
103 | struct swaybar_sni *sni = d->sni; | ||
104 | const char *prop = d->prop; | ||
105 | const char *type = d->type; | ||
106 | void *dest = d->dest; | ||
107 | |||
108 | int ret; | ||
109 | if (sd_bus_message_is_method_error(msg, NULL)) { | ||
110 | wlr_log(WLR_ERROR, "%s %s: %s", sni->watcher_id, prop, | ||
111 | sd_bus_message_get_error(msg)->message); | ||
112 | ret = sd_bus_message_get_errno(msg); | ||
113 | goto cleanup; | ||
114 | } | ||
115 | |||
116 | ret = sd_bus_message_enter_container(msg, 'v', type); | ||
117 | if (ret < 0) { | ||
118 | wlr_log(WLR_ERROR, "%s %s: %s", sni->watcher_id, prop, strerror(-ret)); | ||
119 | goto cleanup; | ||
120 | } | ||
121 | |||
122 | if (!type) { | ||
123 | ret = read_pixmap(msg, sni, prop, dest); | ||
124 | if (ret < 0) { | ||
125 | goto cleanup; | ||
126 | } | ||
127 | } else { | ||
128 | if (*type == 's' || *type == 'o') { | ||
129 | free(*(char **)dest); | ||
130 | } | ||
131 | |||
132 | ret = sd_bus_message_read(msg, type, dest); | ||
133 | if (ret < 0) { | ||
134 | wlr_log(WLR_ERROR, "%s %s: %s", sni->watcher_id, prop, strerror(-ret)); | ||
135 | goto cleanup; | ||
136 | } | ||
137 | |||
138 | if (*type == 's' || *type == 'o') { | ||
139 | char **str = dest; | ||
140 | *str = strdup(*str); | ||
141 | wlr_log(WLR_DEBUG, "%s %s = '%s'", sni->watcher_id, prop, *str); | ||
142 | } else if (*type == 'b') { | ||
143 | wlr_log(WLR_DEBUG, "%s %s = %s", sni->watcher_id, prop, | ||
144 | *(bool *)dest ? "true" : "false"); | ||
145 | } | ||
146 | } | ||
147 | |||
148 | if (strcmp(prop, "Status") == 0 || (sni->status && (sni->status[0] == 'N' ? | ||
149 | prop[0] == 'A' : strncmp(prop, "Icon", 4) == 0))) { | ||
150 | set_sni_dirty(sni); | ||
151 | } | ||
152 | cleanup: | ||
153 | free(data); | ||
154 | return ret; | ||
155 | } | ||
156 | |||
157 | static void sni_get_property_async(struct swaybar_sni *sni, const char *prop, | ||
158 | const char *type, void *dest) { | ||
159 | struct get_property_data *data = malloc(sizeof(struct get_property_data)); | ||
160 | data->sni = sni; | ||
161 | data->prop = prop; | ||
162 | data->type = type; | ||
163 | data->dest = dest; | ||
164 | int ret = sd_bus_call_method_async(sni->tray->bus, NULL, sni->service, | ||
165 | sni->path, "org.freedesktop.DBus.Properties", "Get", | ||
166 | get_property_callback, data, "ss", sni->interface, prop); | ||
167 | if (ret < 0) { | ||
168 | wlr_log(WLR_ERROR, "%s %s: %s", sni->watcher_id, prop, strerror(-ret)); | ||
169 | } | ||
170 | } | ||
171 | |||
172 | /* | ||
173 | * There is a quirk in sd-bus that in some systems, it is unable to get the | ||
174 | * well-known names on the bus, so it cannot identify if an incoming signal, | ||
175 | * which uses the sender's unique name, actually matches the callback's matching | ||
176 | * sender if the callback uses a well-known name, in which case it just calls | ||
177 | * the callback and hopes for the best, resulting in false positives. In the | ||
178 | * case of NewIcon & NewAttentionIcon, this doesn't affect anything, but it | ||
179 | * means that for NewStatus, if the SNI does not definitely match the sender, | ||
180 | * then the safe thing to do is to query the status independently. | ||
181 | * This function returns 1 if the SNI definitely matches the signal sender, | ||
182 | * which is returned by the calling function to indicate that signal matching | ||
183 | * can stop since it has already found the required callback, otherwise, it | ||
184 | * returns 0, which allows matching to continue. | ||
185 | */ | ||
186 | static int sni_check_msg_sender(struct swaybar_sni *sni, sd_bus_message *msg, | ||
187 | const char *signal) { | ||
188 | bool has_well_known_names = | ||
189 | sd_bus_creds_get_mask(sd_bus_message_get_creds(msg)) & SD_BUS_CREDS_WELL_KNOWN_NAMES; | ||
190 | if (sni->service[0] == ':' || has_well_known_names) { | ||
191 | wlr_log(WLR_DEBUG, "%s has new %s", sni->watcher_id, signal); | ||
192 | return 1; | ||
193 | } else { | ||
194 | wlr_log(WLR_DEBUG, "%s may have new %s", sni->watcher_id, signal); | ||
195 | return 0; | ||
196 | } | ||
197 | } | ||
198 | |||
199 | static int handle_new_icon(sd_bus_message *msg, void *data, sd_bus_error *error) { | ||
200 | struct swaybar_sni *sni = data; | ||
201 | sni_get_property_async(sni, "IconName", "s", &sni->icon_name); | ||
202 | sni_get_property_async(sni, "IconPixmap", NULL, &sni->icon_pixmap); | ||
203 | return sni_check_msg_sender(sni, msg, "icon"); | ||
204 | } | ||
205 | |||
206 | static int handle_new_attention_icon(sd_bus_message *msg, void *data, | ||
207 | sd_bus_error *error) { | ||
208 | struct swaybar_sni *sni = data; | ||
209 | sni_get_property_async(sni, "AttentionIconName", "s", &sni->attention_icon_name); | ||
210 | sni_get_property_async(sni, "AttentionIconPixmap", NULL, &sni->attention_icon_pixmap); | ||
211 | return sni_check_msg_sender(sni, msg, "attention icon"); | ||
212 | } | ||
213 | |||
214 | static int handle_new_status(sd_bus_message *msg, void *data, sd_bus_error *error) { | ||
215 | struct swaybar_sni *sni = data; | ||
216 | int ret = sni_check_msg_sender(sni, msg, "status"); | ||
217 | if (ret == 1) { | ||
218 | char *status; | ||
219 | int r = sd_bus_message_read(msg, "s", &status); | ||
220 | if (r < 0) { | ||
221 | wlr_log(WLR_ERROR, "%s new status error: %s", sni->watcher_id, strerror(-ret)); | ||
222 | ret = r; | ||
223 | } else { | ||
224 | free(sni->status); | ||
225 | sni->status = strdup(status); | ||
226 | wlr_log(WLR_DEBUG, "%s has new status = '%s'", sni->watcher_id, status); | ||
227 | set_sni_dirty(sni); | ||
228 | } | ||
229 | } else { | ||
230 | sni_get_property_async(sni, "Status", "s", &sni->status); | ||
231 | } | ||
232 | |||
233 | return ret; | ||
234 | } | ||
235 | |||
236 | static void sni_match_signal(struct swaybar_sni *sni, sd_bus_slot **slot, | ||
237 | char *signal, sd_bus_message_handler_t callback) { | ||
238 | int ret = sd_bus_match_signal(sni->tray->bus, slot, sni->service, sni->path, | ||
239 | sni->interface, signal, callback, sni); | ||
240 | if (ret < 0) { | ||
241 | wlr_log(WLR_ERROR, "Failed to subscribe to signal %s: %s", signal, | ||
242 | strerror(-ret)); | ||
243 | } | ||
244 | } | ||
245 | |||
246 | struct swaybar_sni *create_sni(char *id, struct swaybar_tray *tray) { | ||
247 | struct swaybar_sni *sni = calloc(1, sizeof(struct swaybar_sni)); | ||
248 | if (!sni) { | ||
249 | return NULL; | ||
250 | } | ||
251 | sni->tray = tray; | ||
252 | sni->watcher_id = strdup(id); | ||
253 | char *path_ptr = strchr(id, '/'); | ||
254 | if (!path_ptr) { | ||
255 | sni->service = strdup(id); | ||
256 | sni->path = strdup("/StatusNotifierItem"); | ||
257 | sni->interface = "org.freedesktop.StatusNotifierItem"; | ||
258 | } else { | ||
259 | sni->service = strndup(id, path_ptr - id); | ||
260 | sni->path = strdup(path_ptr); | ||
261 | sni->interface = "org.kde.StatusNotifierItem"; | ||
262 | sni_get_property_async(sni, "IconThemePath", "s", &sni->icon_theme_path); | ||
263 | } | ||
264 | |||
265 | // Ignored: Category, Id, Title, WindowId, OverlayIconName, | ||
266 | // OverlayIconPixmap, AttentionMovieName, ToolTip | ||
267 | sni_get_property_async(sni, "Status", "s", &sni->status); | ||
268 | sni_get_property_async(sni, "IconName", "s", &sni->icon_name); | ||
269 | sni_get_property_async(sni, "IconPixmap", NULL, &sni->icon_pixmap); | ||
270 | sni_get_property_async(sni, "AttentionIconName", "s", &sni->attention_icon_name); | ||
271 | sni_get_property_async(sni, "AttentionIconPixmap", NULL, &sni->attention_icon_pixmap); | ||
272 | sni_get_property_async(sni, "ItemIsMenu", "b", &sni->item_is_menu); | ||
273 | sni_get_property_async(sni, "Menu", "o", &sni->menu); | ||
274 | |||
275 | sni_match_signal(sni, &sni->new_icon_slot, "NewIcon", handle_new_icon); | ||
276 | sni_match_signal(sni, &sni->new_attention_icon_slot, "NewAttentionIcon", | ||
277 | handle_new_attention_icon); | ||
278 | sni_match_signal(sni, &sni->new_status_slot, "NewStatus", handle_new_status); | ||
279 | |||
280 | return sni; | ||
281 | } | ||
282 | |||
283 | void destroy_sni(struct swaybar_sni *sni) { | ||
284 | if (!sni) { | ||
285 | return; | ||
286 | } | ||
287 | |||
288 | sd_bus_slot_unref(sni->new_icon_slot); | ||
289 | sd_bus_slot_unref(sni->new_attention_icon_slot); | ||
290 | sd_bus_slot_unref(sni->new_status_slot); | ||
291 | |||
292 | free(sni->watcher_id); | ||
293 | free(sni->service); | ||
294 | free(sni->path); | ||
295 | free(sni->status); | ||
296 | free(sni->icon_name); | ||
297 | free(sni->icon_pixmap); | ||
298 | free(sni->attention_icon_name); | ||
299 | free(sni->menu); | ||
300 | free(sni); | ||
301 | } | ||
302 | |||
303 | static void handle_click(struct swaybar_sni *sni, int x, int y, | ||
304 | enum x11_button button, int delta) { | ||
305 | const char *method = sni->tray->bar->config->tray_bindings[button]; | ||
306 | if (!method) { | ||
307 | static const char *default_bindings[10] = { | ||
308 | "nop", | ||
309 | "Activate", | ||
310 | "SecondaryActivate", | ||
311 | "ContextMenu", | ||
312 | "ScrollUp", | ||
313 | "ScrollDown", | ||
314 | "ScrollLeft", | ||
315 | "ScrollRight", | ||
316 | "nop", | ||
317 | "nop" | ||
318 | }; | ||
319 | method = default_bindings[button]; | ||
320 | } | ||
321 | if (strcmp(method, "nop") == 0) { | ||
322 | return; | ||
323 | } | ||
324 | if (sni->item_is_menu && strcmp(method, "Activate") == 0) { | ||
325 | method = "ContextMenu"; | ||
326 | } | ||
327 | |||
328 | if (strncmp(method, "Scroll", strlen("Scroll")) == 0) { | ||
329 | char dir = method[strlen("Scroll")]; | ||
330 | char *orientation = (dir = 'U' || dir == 'D') ? "vertical" : "horizontal"; | ||
331 | int sign = (dir == 'U' || dir == 'L') ? -1 : 1; | ||
332 | |||
333 | sd_bus_call_method_async(sni->tray->bus, NULL, sni->service, sni->path, | ||
334 | sni->interface, "Scroll", NULL, NULL, "is", delta*sign, orientation); | ||
335 | } else { | ||
336 | sd_bus_call_method_async(sni->tray->bus, NULL, sni->service, sni->path, | ||
337 | sni->interface, method, NULL, NULL, "ii", x, y); | ||
338 | } | ||
339 | } | ||
340 | |||
341 | static int cmp_sni_id(const void *item, const void *cmp_to) { | ||
342 | const struct swaybar_sni *sni = item; | ||
343 | return strcmp(sni->watcher_id, cmp_to); | ||
344 | } | ||
345 | |||
346 | static enum hotspot_event_handling icon_hotspot_callback( | ||
347 | struct swaybar_output *output, struct swaybar_hotspot *hotspot, | ||
348 | int x, int y, enum x11_button button, void *data) { | ||
349 | wlr_log(WLR_DEBUG, "Clicked on %s", (char *)data); | ||
350 | |||
351 | struct swaybar_tray *tray = output->bar->tray; | ||
352 | int idx = list_seq_find(tray->items, cmp_sni_id, data); | ||
353 | |||
354 | if (idx != -1) { | ||
355 | struct swaybar_sni *sni = tray->items->items[idx]; | ||
356 | // guess global position since wayland doesn't expose it | ||
357 | struct swaybar_config *config = tray->bar->config; | ||
358 | int global_x = output->output_x + config->gaps.left + x; | ||
359 | bool top_bar = config->position & ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP; | ||
360 | int global_y = output->output_y + (top_bar ? config->gaps.top + y: | ||
361 | (int) output->output_height - config->gaps.bottom - y); | ||
362 | |||
363 | wlr_log(WLR_DEBUG, "Guessing click position at (%d, %d)", global_x, global_y); | ||
364 | handle_click(sni, global_x, global_y, button, 1); // TODO get delta from event | ||
365 | return HOTSPOT_IGNORE; | ||
366 | } else { | ||
367 | wlr_log(WLR_DEBUG, "but it doesn't exist"); | ||
368 | } | ||
369 | |||
370 | return HOTSPOT_PROCESS; | ||
371 | } | ||
372 | |||
373 | uint32_t render_sni(cairo_t *cairo, struct swaybar_output *output, double *x, | ||
374 | struct swaybar_sni *sni) { | ||
375 | uint32_t height = output->height * output->scale; | ||
376 | int padding = output->bar->config->tray_padding; | ||
377 | int ideal_size = height - 2*padding; | ||
378 | if ((ideal_size < sni->min_size || ideal_size > sni->max_size) && sni_ready(sni)) { | ||
379 | bool icon_found = false; | ||
380 | char *icon_name = sni->status[0] == 'N' ? | ||
381 | sni->attention_icon_name : sni->icon_name; | ||
382 | if (icon_name) { | ||
383 | char *icon_path = find_icon(sni->tray->themes, sni->tray->basedirs, | ||
384 | icon_name, ideal_size, output->bar->config->icon_theme, | ||
385 | &sni->min_size, &sni->max_size); | ||
386 | if (!icon_path && sni->icon_theme_path) { | ||
387 | icon_path = find_icon_in_dir(icon_name, sni->icon_theme_path, | ||
388 | &sni->min_size, &sni->max_size); | ||
389 | } | ||
390 | if (icon_path) { | ||
391 | cairo_surface_destroy(sni->icon); | ||
392 | sni->icon = load_background_image(icon_path); | ||
393 | free(icon_path); | ||
394 | icon_found = true; | ||
395 | } | ||
396 | } | ||
397 | if (!icon_found) { | ||
398 | list_t *pixmaps = sni->status[0] == 'N' ? | ||
399 | sni->attention_icon_pixmap : sni->icon_pixmap; | ||
400 | if (pixmaps) { | ||
401 | int idx = -1; | ||
402 | unsigned smallest_error = -1; // UINT_MAX | ||
403 | for (int i = 0; i < pixmaps->length; ++i) { | ||
404 | struct swaybar_pixmap *pixmap = pixmaps->items[i]; | ||
405 | unsigned error = (ideal_size - pixmap->size) * | ||
406 | (ideal_size < pixmap->size ? -1 : 1); | ||
407 | if (error < smallest_error) { | ||
408 | smallest_error = error; | ||
409 | idx = i; | ||
410 | } | ||
411 | } | ||
412 | struct swaybar_pixmap *pixmap = pixmaps->items[idx]; | ||
413 | cairo_surface_destroy(sni->icon); | ||
414 | sni->icon = cairo_image_surface_create_for_data(pixmap->pixels, | ||
415 | CAIRO_FORMAT_ARGB32, pixmap->size, pixmap->size, | ||
416 | cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, pixmap->size)); | ||
417 | } | ||
418 | } | ||
419 | } | ||
420 | |||
421 | int icon_size; | ||
422 | cairo_surface_t *icon; | ||
423 | if (sni->icon) { | ||
424 | int actual_size = cairo_image_surface_get_height(sni->icon); | ||
425 | icon_size = actual_size < ideal_size ? | ||
426 | actual_size*(ideal_size/actual_size) : ideal_size; | ||
427 | icon = cairo_image_surface_scale(sni->icon, icon_size, icon_size); | ||
428 | } else { // draw a :( | ||
429 | icon_size = ideal_size*0.8; | ||
430 | icon = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, icon_size, icon_size); | ||
431 | cairo_t *cairo_icon = cairo_create(icon); | ||
432 | cairo_set_source_u32(cairo_icon, 0xFF0000FF); | ||
433 | cairo_translate(cairo_icon, icon_size/2, icon_size/2); | ||
434 | cairo_scale(cairo_icon, icon_size/2, icon_size/2); | ||
435 | cairo_arc(cairo_icon, 0, 0, 1, 0, 7); | ||
436 | cairo_fill(cairo_icon); | ||
437 | cairo_set_operator(cairo_icon, CAIRO_OPERATOR_CLEAR); | ||
438 | cairo_arc(cairo_icon, 0.35, -0.3, 0.1, 0, 7); | ||
439 | cairo_fill(cairo_icon); | ||
440 | cairo_arc(cairo_icon, -0.35, -0.3, 0.1, 0, 7); | ||
441 | cairo_fill(cairo_icon); | ||
442 | cairo_arc(cairo_icon, 0, 0.75, 0.5, 3.71238898038469, 5.71238898038469); | ||
443 | cairo_set_line_width(cairo_icon, 0.1); | ||
444 | cairo_stroke(cairo_icon); | ||
445 | cairo_destroy(cairo_icon); | ||
446 | } | ||
447 | |||
448 | int padded_size = icon_size + 2*padding; | ||
449 | *x -= padded_size; | ||
450 | int y = floor((height - padded_size) / 2.0); | ||
451 | |||
452 | cairo_operator_t op = cairo_get_operator(cairo); | ||
453 | cairo_set_operator(cairo, CAIRO_OPERATOR_OVER); | ||
454 | cairo_set_source_surface(cairo, icon, *x + padding, y + padding); | ||
455 | cairo_rectangle(cairo, *x, y, padded_size, padded_size); | ||
456 | cairo_fill(cairo); | ||
457 | cairo_set_operator(cairo, op); | ||
458 | |||
459 | cairo_surface_destroy(icon); | ||
460 | |||
461 | struct swaybar_hotspot *hotspot = calloc(1, sizeof(struct swaybar_hotspot)); | ||
462 | hotspot->x = *x; | ||
463 | hotspot->y = 0; | ||
464 | hotspot->width = height; | ||
465 | hotspot->height = height; | ||
466 | hotspot->callback = icon_hotspot_callback; | ||
467 | hotspot->destroy = free; | ||
468 | hotspot->data = strdup(sni->watcher_id); | ||
469 | wl_list_insert(&output->hotspots, &hotspot->link); | ||
470 | |||
471 | return output->height; | ||
472 | } | ||