#include "draw.h" #include #include #include #include #include #include #include #include #include #include #include "dunst.h" #include "icon.h" #include "log.h" #include "markup.h" #include "notification.h" #include "queues.h" #include "output.h" #include "settings.h" struct color { double r; double g; double b; double a; }; struct colored_layout { PangoLayout *l; struct color fg; struct color bg; struct color highlight; struct color frame; char *text; PangoAttrList *attr; cairo_surface_t *icon; const struct notification *n; }; const struct output *output; window win; PangoFontDescription *pango_fdesc; #define UINT_MAX_N(bits) ((1 << bits) - 1) void draw_setup(void) { const struct output *out = output_create(settings.force_xwayland); output = out; out->init(); win = out->win_create(); pango_fdesc = pango_font_description_from_string(settings.font); } static struct color hex_to_color(uint32_t hexValue, int dpc) { const int bpc = 4 * dpc; const unsigned single_max = UINT_MAX_N(bpc); struct color ret; ret.r = ((hexValue >> 3 * bpc) & single_max) / (double)single_max; ret.g = ((hexValue >> 2 * bpc) & single_max) / (double)single_max; ret.b = ((hexValue >> 1 * bpc) & single_max) / (double)single_max; ret.a = ((hexValue) & single_max) / (double)single_max; return ret; } static struct color string_to_color(const char *str) { char *end; uint_fast32_t val = strtoul(str+1, &end, 16); if (end[0] != '\0' && end[1] != '\0') { LOG_W("Invalid color string: '%s'", str); } switch (end - (str+1)) { case 3: return hex_to_color((val << 4) | 0xF, 1); case 6: return hex_to_color((val << 8) | 0xFF, 2); case 4: return hex_to_color(val, 1); case 8: return hex_to_color(val, 2); } /* return black on error */ LOG_W("Invalid color string: '%s'", str); return hex_to_color(0xF, 1); } static inline double color_apply_delta(double base, double delta) { base += delta; if (base > 1) base = 1; if (base < 0) base = 0; return base; } static struct color calculate_foreground_color(struct color bg) { double c_delta = 0.1; struct color fg = bg; /* do we need to darken or brighten the colors? */ bool darken = (bg.r + bg.g + bg.b) / 3 > 0.5; int signedness = darken ? -1 : 1; fg.r = color_apply_delta(fg.r, c_delta * signedness); fg.g = color_apply_delta(fg.g, c_delta * signedness); fg.b = color_apply_delta(fg.b, c_delta * signedness); return fg; } static struct color layout_get_sepcolor(struct colored_layout *cl, struct colored_layout *cl_next) { switch (settings.sep_color.type) { case SEP_FRAME: if (cl_next->n->urgency > cl->n->urgency) return cl_next->frame; else return cl->frame; case SEP_CUSTOM: return string_to_color(settings.sep_color.sep_color); case SEP_FOREGROUND: return cl->fg; case SEP_AUTO: return calculate_foreground_color(cl->bg); default: LOG_E("Invalid %s enum value in %s:%d", "sep_color", __FILE__, __LINE__); break; } } static void layout_setup_pango(PangoLayout *layout, int width) { pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR); pango_layout_set_width(layout, width * PANGO_SCALE); pango_layout_set_font_description(layout, pango_fdesc); pango_layout_set_spacing(layout, settings.line_height * PANGO_SCALE); PangoAlignment align; switch (settings.align) { case ALIGN_LEFT: default: align = PANGO_ALIGN_LEFT; break; case ALIGN_CENTER: align = PANGO_ALIGN_CENTER; break; case ALIGN_RIGHT: align = PANGO_ALIGN_RIGHT; break; } pango_layout_set_alignment(layout, align); } static void free_colored_layout(void *data) { struct colored_layout *cl = data; g_object_unref(cl->l); pango_attr_list_unref(cl->attr); g_free(cl->text); if (cl->icon) cairo_surface_destroy(cl->icon); g_free(cl); } static bool have_dynamic_width(void) { return (settings.geometry.width_set && settings.geometry.w == 0); } static int get_text_icon_padding() { if (settings.text_icon_padding) { return settings.text_icon_padding; } else { return settings.h_padding; } } static bool have_progress_bar(const struct notification *n) { return (n->progress >= 0 && settings.progress_bar == true); } static struct dimensions calculate_dimensions(GSList *layouts) { struct dimensions dim = { 0 }; const struct screen_info *scr = output->get_active_screen(); if (have_dynamic_width()) { /* dynamic width */ dim.w = 0; } else if (settings.geometry.width_set) { /* fixed width */ if (settings.geometry.negative_width) { dim.w = scr->w - settings.geometry.w; } else { dim.w = settings.geometry.w; } } else { /* across the screen */ dim.w = scr->w; } dim.h += 2 * settings.frame_width; dim.h += (g_slist_length(layouts) - 1) * settings.separator_height; dim.corner_radius = settings.corner_radius; int text_width = 0, total_width = 0; for (GSList *iter = layouts; iter; iter = iter->next) { struct colored_layout *cl = iter->data; int w=0,h=0; pango_layout_get_pixel_size(cl->l, &w, &h); if (cl->icon) { h = MAX(cairo_image_surface_get_height(cl->icon), h); w += cairo_image_surface_get_width(cl->icon) + settings.h_padding; } h = MAX(settings.notification_height, h + settings.padding * 2); dim.h += h; text_width = MAX(w, text_width); if (have_dynamic_width() || settings.shrink) { /* dynamic width */ total_width = MAX(text_width + 2 * settings.h_padding, total_width); /* subtract height from the unwrapped text */ dim.h -= h; if (total_width > scr->w) { /* set width to screen width */ dim.w = scr->w - settings.geometry.x * 2; } else if (have_dynamic_width() || (total_width < settings.geometry.w && settings.shrink)) { /* set width to text width */ dim.w = total_width + 2 * settings.frame_width; } /* re-setup the layout */ w = dim.w; w -= 2 * settings.h_padding; w -= 2 * settings.frame_width; if (cl->icon) { w -= cairo_image_surface_get_width(cl->icon) + get_text_icon_padding(); } layout_setup_pango(cl->l, w); /* re-read information */ pango_layout_get_pixel_size(cl->l, &w, &h); if (cl->icon) { h = MAX(cairo_image_surface_get_height(cl->icon), h); w += cairo_image_surface_get_width(cl->icon) + settings.h_padding; } h = MAX(settings.notification_height, h + settings.padding * 2); dim.h += h; text_width = MAX(w, text_width); } if (have_progress_bar(cl->n)){ dim.h += settings.progress_bar_height + settings.padding; dim.w = MAX(dim.w, settings.progress_bar_min_width); } dim.corner_radius = MIN(dim.corner_radius, h/2); } if (dim.w <= 0) { dim.w = text_width + 2 * settings.h_padding; dim.w += 2 * settings.frame_width; } return dim; } static PangoLayout *layout_create(cairo_t *c) { const struct screen_info *screen = output->get_active_screen(); PangoContext *context = pango_cairo_create_context(c); pango_cairo_context_set_resolution(context, screen->dpi); PangoLayout *layout = pango_layout_new(context); g_object_unref(context); return layout; } static struct colored_layout *layout_init_shared(cairo_t *c, const struct notification *n) { struct colored_layout *cl = g_malloc(sizeof(struct colored_layout)); cl->l = layout_create(c); if (!settings.word_wrap) { PangoEllipsizeMode ellipsize; switch (settings.ellipsize) { case ELLIPSE_START: ellipsize = PANGO_ELLIPSIZE_START; break; case ELLIPSE_MIDDLE: ellipsize = PANGO_ELLIPSIZE_MIDDLE; break; case ELLIPSE_END: ellipsize = PANGO_ELLIPSIZE_END; break; default: LOG_E("Invalid %s enum value in %s:%d", "ellipsize", __FILE__, __LINE__); break; } pango_layout_set_ellipsize(cl->l, ellipsize); } if (settings.icon_position != ICON_OFF && n->icon) { cl->icon = gdk_pixbuf_to_cairo_surface(n->icon); } else { cl->icon = NULL; } if (cl->icon && cairo_surface_status(cl->icon) != CAIRO_STATUS_SUCCESS) { cairo_surface_destroy(cl->icon); cl->icon = NULL; } cl->fg = string_to_color(n->colors.fg); cl->bg = string_to_color(n->colors.bg); cl->highlight = string_to_color(n->colors.highlight); cl->frame = string_to_color(n->colors.frame); cl->n = n; struct dimensions dim = calculate_dimensions(NULL); int width = dim.w; if (have_dynamic_width()) { layout_setup_pango(cl->l, -1); } else { width -= 2 * settings.h_padding; width -= 2 * settings.frame_width; if (cl->icon) { width -= cairo_image_surface_get_width(cl->icon) + get_text_icon_padding(); } layout_setup_pango(cl->l, width); } return cl; } static struct colored_layout *layout_derive_xmore(cairo_t *c, const struct notification *n, int qlen) { struct colored_layout *cl = layout_init_shared(c, n); cl->text = g_strdup_printf("(%d more)", qlen); cl->attr = NULL; pango_layout_set_text(cl->l, cl->text, -1); return cl; } static struct colored_layout *layout_from_notification(cairo_t *c, struct notification *n) { struct colored_layout *cl = layout_init_shared(c, n); /* markup */ GError *err = NULL; pango_parse_markup(n->text_to_render, -1, 0, &(cl->attr), &(cl->text), NULL, &err); if (!err) { pango_layout_set_text(cl->l, cl->text, -1); pango_layout_set_attributes(cl->l, cl->attr); } else { /* remove markup and display plain message instead */ n->text_to_render = markup_strip(n->text_to_render); cl->text = NULL; cl->attr = NULL; pango_layout_set_text(cl->l, n->text_to_render, -1); if (n->first_render) { LOG_W("Unable to parse markup: %s", err->message); } g_error_free(err); } pango_layout_get_pixel_size(cl->l, NULL, &(n->displayed_height)); if (cl->icon) n->displayed_height = MAX(cairo_image_surface_get_height(cl->icon), n->displayed_height); n->displayed_height = n->displayed_height + settings.padding * 2; // progress bar if (have_progress_bar(n)) n->displayed_height += settings.progress_bar_height + settings.padding; n->displayed_height = MAX(settings.notification_height, n->displayed_height); n->first_render = false; return cl; } static GSList *create_layouts(cairo_t *c) { GSList *layouts = NULL; int qlen = queues_length_waiting(); bool xmore_is_needed = qlen > 0 && settings.indicate_hidden; for (const GList *iter = queues_get_displayed(); iter; iter = iter->next) { struct notification *n = iter->data; notification_update_text_to_render(n); if (!iter->next && xmore_is_needed && settings.geometry.h == 1) { char *new_ttr = g_strdup_printf("%s (%d more)", n->text_to_render, qlen); g_free(n->text_to_render); n->text_to_render = new_ttr; } layouts = g_slist_append(layouts, layout_from_notification(c, n)); } if (xmore_is_needed && settings.geometry.h != 1) { /* append xmore message as new message */ layouts = g_slist_append(layouts, layout_derive_xmore(c, queues_get_head_waiting(), qlen)); } return layouts; } static int layout_get_height(struct colored_layout *cl) { int h; int h_icon = 0; int h_progress_bar = 0; pango_layout_get_pixel_size(cl->l, NULL, &h); if (cl->icon) h_icon = cairo_image_surface_get_height(cl->icon); if (have_progress_bar(cl->n)){ h_progress_bar = settings.progress_bar_height + settings.padding; } int res = MAX(h, h_icon) + h_progress_bar; return res; } /* Attempt to make internal radius more organic. * Simple r-w is not enough for too small r/w ratio. * simplifications: r/2 == r - w + w*w / (r * 2) with (w == r) * r, w - corner radius & frame width, * h - box height */ static int frame_internal_radius (int r, int w, int h) { if (r == 0 || h + (w - r) * 2 == 0) return 0; // Integer precision scaler, using 1/4 of int size const int s = 2 << (8 * sizeof(int) / 4); int r1, r2, ret; h *= s; r *= s; w *= s; r1 = r - w + w * w / (r * 2); // w < r r2 = r * h / (h + (w - r) * 2); // w >= r ret = (r > w) ? r1 : (r / 2 < r2) ? r / 2 : r2; return ret / s; } /** * Create a path on the given cairo context to draw the background of a notification. * The top corners will get rounded by `corner_radius`, if `first` is set. * Respectably the same for `last` with the bottom corners. */ void draw_rounded_rect(cairo_t *c, int x, int y, int width, int height, int corner_radius, bool first, bool last) { const float degrees = M_PI / 180.0; cairo_new_sub_path(c); if (last) { // bottom right cairo_arc(c, x + width - corner_radius, y + height - corner_radius, corner_radius, degrees * 0, degrees * 90); // bottom left cairo_arc(c, x + corner_radius, y + height - corner_radius, corner_radius, degrees * 90, degrees * 180); } else { cairo_line_to(c, x + width, y + height); cairo_line_to(c, x, y + height); } if (first) { // top left cairo_arc(c, x + corner_radius, y + corner_radius, corner_radius, degrees * 180, degrees * 270); // top right cairo_arc(c, x + width - corner_radius, y + corner_radius, corner_radius, degrees * 270, degrees * 360); } else { cairo_line_to(c, x, y); cairo_line_to(c, x + width, y); } cairo_close_path(c); } static cairo_surface_t *render_background(cairo_surface_t *srf, struct colored_layout *cl, struct colored_layout *cl_next, int y, int width, int height, int corner_radius, bool first, bool last, int *ret_width) { int x = 0; int radius_int = corner_radius; cairo_t *c = cairo_create(srf); /* stroke area doesn't intersect with main area */ cairo_set_fill_rule(c, CAIRO_FILL_RULE_EVEN_ODD); /* for correct combination of adjacent areas */ cairo_set_operator(c, CAIRO_OPERATOR_ADD); if (first) height += settings.frame_width; if (last) height += settings.frame_width; else height += settings.separator_height; draw_rounded_rect(c, x, y, width, height, corner_radius, first, last); /* adding frame */ x += settings.frame_width; if (first) { y += settings.frame_width; height -= settings.frame_width; } width -= 2 * settings.frame_width; if (last) height -= settings.frame_width; else height -= settings.separator_height; radius_int = frame_internal_radius(corner_radius, settings.frame_width, height); draw_rounded_rect(c, x, y, width, height, radius_int, first, last); cairo_set_source_rgba(c, cl->frame.r, cl->frame.g, cl->frame.b, cl->frame.a); cairo_fill(c); draw_rounded_rect(c, x, y, width, height, radius_int, first, last); cairo_set_source_rgba(c, cl->bg.r, cl->bg.g, cl->bg.b, cl->bg.a); cairo_fill(c); cairo_set_operator(c, CAIRO_OPERATOR_SOURCE); if ( settings.sep_color.type != SEP_FRAME && settings.separator_height > 0 && !last) { struct color sep_color = layout_get_sepcolor(cl, cl_next); cairo_set_source_rgba(c, sep_color.r, sep_color.g, sep_color.b, sep_color.a); cairo_rectangle(c, settings.frame_width, y + height, width, settings.separator_height); cairo_fill(c); } cairo_destroy(c); if (ret_width) *ret_width = width; return cairo_surface_create_for_rectangle(srf, x, y, width, height); } static void render_content(cairo_t *c, struct colored_layout *cl, int width) { const int h = layout_get_height(cl); int h_without_progress_bar = h; if (have_progress_bar(cl->n)){ h_without_progress_bar -= settings.progress_bar_height + settings.padding; } int h_text; pango_layout_get_pixel_size(cl->l, NULL, &h_text); int text_x = settings.h_padding, text_y = settings.padding + h_without_progress_bar / 2 - h_text / 2; // text positioning if (cl->icon) { // vertical alignment if (settings.vertical_alignment == VERTICAL_TOP) { text_y = settings.padding; } else if (settings.vertical_alignment == VERTICAL_BOTTOM) { text_y = h_without_progress_bar + settings.padding - h_text; if (text_y < 0) text_y = settings.padding; } // else VERTICAL_CENTER // icon position if (settings.icon_position == ICON_LEFT) { text_x = cairo_image_surface_get_width(cl->icon) + settings.h_padding + get_text_icon_padding(); } // else ICON_RIGHT } cairo_move_to(c, text_x, text_y); cairo_set_source_rgba(c, cl->fg.r, cl->fg.g, cl->fg.b, cl->fg.a); pango_cairo_update_layout(c, cl->l); pango_cairo_show_layout(c, cl->l); // icon positioning if (cl->icon) { unsigned int image_width = cairo_image_surface_get_width(cl->icon), image_height = cairo_image_surface_get_height(cl->icon), image_x = width - settings.h_padding - image_width, image_y = settings.padding + h_without_progress_bar/2 - image_height/2; // vertical alignment if (settings.vertical_alignment == VERTICAL_TOP) { image_y = settings.padding; } else if (settings.vertical_alignment == VERTICAL_BOTTOM) { image_y = h_without_progress_bar + settings.padding - image_height; if (image_y < settings.padding || image_y > h_without_progress_bar) image_y = settings.padding; } // else VERTICAL_CENTER // icon position if (settings.icon_position == ICON_LEFT) { image_x = settings.h_padding; } // else ICON_RIGHT cairo_set_source_surface(c, cl->icon, image_x, image_y); cairo_rectangle(c, image_x, image_y, image_width, image_height); cairo_fill(c); } // progress bar positioning if (have_progress_bar(cl->n)){ int progress = MIN(cl->n->progress, 100); unsigned int frame_width = settings.progress_bar_frame_width, progress_width = MIN(width - 2 * settings.h_padding, settings.progress_bar_max_width), progress_height = settings.progress_bar_height - frame_width, frame_x = settings.h_padding, frame_y = settings.padding + h - settings.progress_bar_height, progress_width_without_frame = progress_width - 2 * frame_width, progress_width_1 = progress_width_without_frame * progress / 100, progress_width_2 = progress_width_without_frame - progress_width_1, x_bar_1 = frame_x + frame_width, x_bar_2 = x_bar_1 + progress_width_1; double half_frame_width = frame_width / 2.0; // draw progress bar // Note: the bar could be drawn a bit smaller, because the frame is drawn on top // left side cairo_set_source_rgba(c, cl->highlight.r, cl->highlight.g, cl->highlight.b, cl->highlight.a); cairo_rectangle(c, x_bar_1, frame_y, progress_width_1, progress_height); cairo_fill(c); // right side cairo_set_source_rgba(c, cl->bg.r, cl->bg.g, cl->bg.b, cl->bg.a); cairo_rectangle(c, x_bar_2, frame_y, progress_width_2, progress_height); cairo_fill(c); // border cairo_set_source_rgba(c, cl->frame.r, cl->frame.g, cl->frame.b, cl->frame.a); cairo_rectangle(c, frame_x + half_frame_width, frame_y + half_frame_width, progress_width - frame_width, progress_height); cairo_set_line_width(c, frame_width); cairo_stroke(c); } } static struct dimensions layout_render(cairo_surface_t *srf, struct colored_layout *cl, struct colored_layout *cl_next, struct dimensions dim, bool first, bool last) { const int cl_h = layout_get_height(cl); int h_text = 0; pango_layout_get_pixel_size(cl->l, NULL, &h_text); int bg_width = 0; int bg_height = MAX(settings.notification_height, (2 * settings.padding) + cl_h); cairo_surface_t *content = render_background(srf, cl, cl_next, dim.y, dim.w, bg_height, dim.corner_radius, first, last, &bg_width); cairo_t *c = cairo_create(content); render_content(c, cl, bg_width); /* adding frame */ if (first) dim.y += settings.frame_width; if (!last) dim.y += settings.separator_height; if (settings.notification_height <= (2 * settings.padding) + cl_h) dim.y += cl_h + 2 * settings.padding; else dim.y += settings.notification_height; cairo_destroy(c); cairo_surface_destroy(content); return dim; } /** * Calculates the position the window should be placed at given its width and * height and stores them in \p ret_x and \p ret_y. */ static void calc_window_pos(int width, int height, int *ret_x, int *ret_y) { const struct screen_info *scr = output->get_active_screen(); if (ret_x) { if (settings.geometry.negative_x) { *ret_x = (scr->x + (scr->w - width)) + settings.geometry.x; } else { *ret_x = scr->x + settings.geometry.x; } } if (ret_y) { if (settings.geometry.negative_y) { *ret_y = scr->y + (scr->h + settings.geometry.y) - height; } else { *ret_y = scr->y + settings.geometry.y; } } } void draw(void) { assert(queues_length_displayed() > 0); GSList *layouts = create_layouts(output->win_get_context(win)); struct dimensions dim = calculate_dimensions(layouts); cairo_surface_t *image_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, dim.w, dim.h); bool first = true; for (GSList *iter = layouts; iter; iter = iter->next) { struct colored_layout *cl_this = iter->data; struct colored_layout *cl_next = iter->next ? iter->next->data : NULL; dim = layout_render(image_surface, cl_this, cl_next, dim, first, !cl_next); first = false; } calc_window_pos(dim.w, dim.h, &dim.x, &dim.y); output->display_surface(image_surface, win, &dim); cairo_surface_destroy(image_surface); g_slist_free_full(layouts, free_colored_layout); } void draw_deinit(void) { output->win_destroy(win); output->deinit(); } /* vim: set ft=c tabstop=8 shiftwidth=8 expandtab textwidth=0: */