dunst/src/draw.c
Nikita Zlobin 1e36a6b4ac Support arbitrary hex color formats (16/32bits per component)
It's unlikely that string_to_color() is faster, but at least is more
flexible. Not sure if 8-digit per channel format is used.
2020-05-23 14:14:50 +05:00

738 lines
25 KiB
C

#include "draw.h"
#include <assert.h>
#include <cairo.h>
#include <math.h>
#include <pango/pango-attributes.h>
#include <pango/pangocairo.h>
#include <pango/pango-font.h>
#include <pango/pango-layout.h>
#include <pango/pango-types.h>
#include <stdlib.h>
#include <inttypes.h>
#include <errno.h>
#include "dunst.h"
#include "icon.h"
#include "log.h"
#include "markup.h"
#include "notification.h"
#include "queues.h"
#include "x11/x.h"
struct colored_layout {
PangoLayout *l;
struct color fg;
struct color bg;
struct color frame;
char *text;
PangoAttrList *attr;
cairo_surface_t *icon;
const struct notification *n;
};
struct window_x11 *win;
PangoFontDescription *pango_fdesc;
#define UINT_MAX_N(bits) (1 << (bits-1) | (( 1 << (bits-1) ) - 1))
void draw_setup(void)
{
x_setup();
win = x_win_create();
pango_fdesc = pango_font_description_from_string(settings.font);
}
static struct color hex_to_color(uintmax_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)
{
uintmax_t val;
unsigned clen;
{
int cn;
/* accept 3 or 4 equal components */ {
char *end;
val = strtoumax(str+1, &end, 16);
if (errno == ERANGE || (end[0] != '\0' && end[1] != '\0'))
goto err;
const int len = (end - (str+1));
if (len % 3 == 0) cn = 3;
else if (len % 4 == 0) cn = 4;
else goto err;
clen = len / cn;
}
/* component length must be 2^n */ {
unsigned mask = 1;
while(mask < clen) mask <<= 1;
if (mask != clen) goto err;
}
/* turn 3-component to opaque 4-components */ {
const unsigned csize = clen * 4;
if (cn == 3)
val = (val << csize) | UINT_MAX_N(csize);
}
}
return hex_to_color(val, clen);
/* return black on error */
err:
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 struct dimensions calculate_dimensions(GSList *layouts)
{
struct dimensions dim = { 0 };
struct screen_info *scr = 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) + settings.h_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);
}
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)
{
struct screen_info *screen = get_active_screen();
PangoContext *context = pango_cairo_create_context(c);
pango_cairo_context_set_resolution(context, screen_dpi_get(screen));
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->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) + settings.h_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 = MAX(settings.notification_height, n->displayed_height + settings.padding * 2);
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;
pango_layout_get_pixel_size(cl->l, NULL, &h);
if (cl->icon)
h_icon = cairo_image_surface_get_height(cl->icon);
return MAX(h, h_icon);
}
/* 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)
{
// 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;
if (settings.frame_width > 0) {
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_OVER);
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_text;
pango_layout_get_pixel_size(cl->l, NULL, &h_text);
int text_x = settings.h_padding,
text_y = settings.padding + h / 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 + 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) + 2 * settings.h_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/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 + settings.padding - image_height;
if (image_y < settings.padding || image_y > h)
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);
}
}
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)
{
struct screen_info *scr = 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(x_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);
x_display_surface(image_surface, win, &dim);
cairo_surface_destroy(image_surface);
g_slist_free_full(layouts, free_colored_layout);
}
void draw_deinit(void)
{
x_win_destroy(win);
x_free();
}
/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */