1 #ifndef _BM_CAIRO_H_
2 #define _BM_CAIRO_H_
3 
4 #include "internal.h"
5 #include <string.h>
6 #include <assert.h>
7 #include <math.h>
8 #include <cairo.h>
9 #include <pango/pangocairo.h>
10 
11 struct cairo {
12     cairo_t *cr;
13     cairo_surface_t *surface;
14     PangoContext *pango;
15     int scale;
16 };
17 
18 struct cairo_color {
19     float r, g, b, a;
20 };
21 
22 struct cairo_paint {
23     struct cairo_color fg;
24     struct cairo_color bg;
25     const char *font;
26     int32_t baseline;
27     uint32_t cursor;
28     uint32_t cursor_height;
29     bool draw_cursor;
30 
31     struct box {
32         int32_t lx, rx; // left/right offset (pos.x - lx, box.w + rx)
33         int32_t ty, by; // top/bottom offset (pos.y - ty, box.h + by)
34         int32_t w, h; // 0 for text width/height
35     } box;
36 
37     struct pos {
38         int32_t x, y;
39     } pos;
40 };
41 
42 struct cairo_result {
43     uint32_t x_advance;
44     uint32_t height;
45     uint32_t baseline;
46 };
47 
48 struct cairo_paint_result {
49     uint32_t displayed;
50     uint32_t height;
51 };
52 
53 static size_t blen = 0;
54 static char *buffer = NULL;
55 
56 static inline bool
bm_cairo_create_for_surface(struct cairo * cairo,cairo_surface_t * surface)57 bm_cairo_create_for_surface(struct cairo *cairo, cairo_surface_t *surface)
58 {
59     assert(cairo && surface);
60     if (!(cairo->cr = cairo_create(surface)))
61         goto fail;
62 
63     if (!(cairo->pango = pango_cairo_create_context(cairo->cr)))
64         goto fail;
65 
66     cairo->surface = surface;
67     assert(cairo->scale > 0);
68     cairo_surface_set_device_scale(surface, cairo->scale, cairo->scale);
69     return true;
70 
71 fail:
72     if (cairo->cr)
73       cairo_destroy(cairo->cr);
74     return false;
75 }
76 
77 static inline void
bm_cairo_destroy(struct cairo * cairo)78 bm_cairo_destroy(struct cairo *cairo)
79 {
80     if (cairo->cr)
81         cairo_destroy(cairo->cr);
82     if (cairo->surface)
83         cairo_surface_destroy(cairo->surface);
84 }
85 
86 static inline PangoLayout*
bm_pango_get_layout(struct cairo * cairo,struct cairo_paint * paint,const char * buffer)87 bm_pango_get_layout(struct cairo *cairo, struct cairo_paint *paint, const char *buffer)
88 {
89     PangoLayout *layout = pango_cairo_create_layout(cairo->cr);
90     pango_layout_set_text(layout, buffer, -1);
91     PangoFontDescription *desc = pango_font_description_from_string(paint->font);
92     pango_layout_set_font_description(layout, desc);
93     pango_layout_set_single_paragraph_mode(layout, 1);
94     pango_font_description_free(desc);
95     return layout;
96 }
97 
98 BM_LOG_ATTR(4, 5) static inline bool
bm_pango_get_text_extents(struct cairo * cairo,struct cairo_paint * paint,struct cairo_result * result,const char * fmt,...)99 bm_pango_get_text_extents(struct cairo *cairo, struct cairo_paint *paint, struct cairo_result *result, const char *fmt, ...)
100 {
101     assert(cairo && paint && result && fmt);
102     memset(result, 0, sizeof(struct cairo_result));
103 
104     va_list args;
105     va_start(args, fmt);
106     bool ret = bm_vrprintf(&buffer, &blen, fmt, args);
107     va_end(args);
108 
109     if (!ret)
110         return false;
111 
112     PangoRectangle rect;
113     PangoLayout *layout = bm_pango_get_layout(cairo, paint, buffer);
114     pango_layout_get_pixel_extents(layout, NULL, &rect);
115     int baseline = pango_layout_get_baseline(layout) / PANGO_SCALE;
116     g_object_unref(layout);
117 
118     result->x_advance = rect.x + rect.width;
119     result->height = rect.height;
120     result->baseline = baseline;
121     return true;
122 }
123 
124 static inline bool
bm_cairo_draw_line_str(struct cairo * cairo,struct cairo_paint * paint,struct cairo_result * result,const char * buffer)125 bm_cairo_draw_line_str(struct cairo *cairo, struct cairo_paint *paint, struct cairo_result *result, const char *buffer)
126 {
127     PangoLayout *layout = bm_pango_get_layout(cairo, paint, buffer);
128     pango_cairo_update_layout(cairo->cr, layout);
129 
130     int width, height;
131     pango_layout_get_pixel_size(layout, &width, &height);
132     height = paint->box.h > 0 ? paint->box.h : height;
133     int base = pango_layout_get_baseline(layout) / PANGO_SCALE;
134 
135     uint32_t line_height = height + paint->box.by + paint->box.ty;
136     cairo_set_source_rgba(cairo->cr, paint->bg.r, paint->bg.b, paint->bg.g, paint->bg.a);
137     cairo_rectangle(cairo->cr,
138             paint->pos.x - paint->box.lx, paint->pos.y - paint->box.ty,
139             (paint->box.w > 0 ? paint->box.w : width) + paint->box.rx + paint->box.lx,
140             line_height);
141     cairo_fill(cairo->cr);
142 
143     cairo_set_source_rgba(cairo->cr, paint->fg.r, paint->fg.b, paint->fg.g, paint->fg.a);
144     cairo_move_to(cairo->cr, paint->box.lx + paint->pos.x, paint->pos.y - base + paint->baseline);
145     pango_cairo_show_layout(cairo->cr, layout);
146 
147     if (paint->draw_cursor) {
148         PangoRectangle rect;
149         pango_layout_index_to_pos(layout, paint->cursor, &rect);
150 
151         if (!rect.width) {
152             struct cairo_result result = {0};
153             bm_pango_get_text_extents(cairo, paint, &result, "#");
154             rect.width = result.x_advance * PANGO_SCALE;
155         }
156 
157         uint32_t cursor_height = fmin(paint->cursor_height == 0 ? line_height : paint->cursor_height, line_height);
158         cairo_set_source_rgba(cairo->cr, paint->fg.r, paint->fg.b, paint->fg.g, paint->fg.a);
159         cairo_rectangle(cairo->cr,
160                 paint->pos.x + paint->box.lx + rect.x / PANGO_SCALE, paint->pos.y - paint->box.ty + ((line_height - cursor_height) / 2),
161                 rect.width / PANGO_SCALE, cursor_height);
162         cairo_fill(cairo->cr);
163 
164         cairo_rectangle(cairo->cr,
165                 paint->pos.x + paint->box.lx + rect.x / PANGO_SCALE, paint->pos.y - paint->box.ty,
166                 rect.width / PANGO_SCALE, line_height);
167         cairo_clip(cairo->cr);
168 
169         cairo_set_source_rgba(cairo->cr, paint->bg.r, paint->bg.b, paint->bg.g, paint->bg.a);
170         cairo_move_to(cairo->cr, paint->box.lx + paint->pos.x, paint->pos.y - base + paint->baseline);
171         pango_cairo_show_layout(cairo->cr, layout);
172         cairo_reset_clip(cairo->cr);
173     }
174 
175     g_object_unref(layout);
176 
177     result->x_advance = width + paint->box.rx;
178     result->height = line_height;
179 
180     cairo_identity_matrix(cairo->cr);
181     return true;
182 }
183 
184 BM_LOG_ATTR(4, 5) static inline bool
bm_cairo_draw_line(struct cairo * cairo,struct cairo_paint * paint,struct cairo_result * result,const char * fmt,...)185 bm_cairo_draw_line(struct cairo *cairo, struct cairo_paint *paint, struct cairo_result *result, const char *fmt, ...)
186 {
187     assert(cairo && paint && result && fmt);
188     memset(result, 0, sizeof(struct cairo_result));
189 
190     va_list args;
191     va_start(args, fmt);
192     bool ret = bm_vrprintf(&buffer, &blen, fmt, args);
193     va_end(args);
194 
195     if (!ret)
196         return false;
197 
198     return bm_cairo_draw_line_str(cairo, paint, result, buffer);
199 }
200 
201 static inline void
bm_cairo_color_from_menu_color(const struct bm_menu * menu,enum bm_color color,struct cairo_color * c)202 bm_cairo_color_from_menu_color(const struct bm_menu *menu, enum bm_color color, struct cairo_color *c)
203 {
204     assert(menu);
205     c->r = (float)menu->colors[color].r / 255.0f;
206     c->g = (float)menu->colors[color].g / 255.0f;
207     c->b = (float)menu->colors[color].b / 255.0f;
208     c->a = (float)menu->colors[color].a / 255.0f;
209 }
210 
211 static inline void
bm_cairo_paint(struct cairo * cairo,uint32_t width,uint32_t max_height,const struct bm_menu * menu,struct cairo_paint_result * out_result)212 bm_cairo_paint(struct cairo *cairo, uint32_t width, uint32_t max_height, const struct bm_menu *menu, struct cairo_paint_result *out_result)
213 {
214     assert(cairo && menu && out_result);
215 
216     max_height /= cairo->scale;
217 
218     memset(out_result, 0, sizeof(struct cairo_paint_result));
219     out_result->displayed = 1;
220 
221     struct cairo_paint paint = {0};
222     paint.font = menu->font.name;
223 
224     struct cairo_result result = {0};
225     int ascii_height;
226     bm_pango_get_text_extents(cairo, &paint, &result, "!\"#$%%&'()*+,-./0123456789:;<=>?@ABCD"
227                               "EFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~");
228     ascii_height = result.height;
229     paint.baseline = result.baseline;
230 
231     uint32_t height = fmin(fmax(menu->line_height, ascii_height), max_height);
232     uint32_t vpadding = (height - ascii_height)/2;
233 
234     cairo_set_source_rgba(cairo->cr, 0, 0, 0, 0);
235     cairo_rectangle(cairo->cr, 0, 0, width, height);
236 
237     cairo_save(cairo->cr);
238     cairo_set_operator(cairo->cr, CAIRO_OPERATOR_CLEAR);
239     cairo_paint(cairo->cr);
240     cairo_restore(cairo->cr);
241 
242     memset(&result, 0, sizeof(result));
243     uint32_t title_x = 0;
244     if (menu->title) {
245         bm_cairo_color_from_menu_color(menu, BM_COLOR_TITLE_FG, &paint.fg);
246         bm_cairo_color_from_menu_color(menu, BM_COLOR_TITLE_BG, &paint.bg);
247         paint.pos = (struct pos){ result.x_advance, vpadding };
248         paint.box = (struct box){ 4, 8, vpadding, -vpadding, 0, height };
249         bm_cairo_draw_line(cairo, &paint, &result, "%s", menu->title);
250         title_x = result.x_advance;
251     }
252 
253     bm_cairo_color_from_menu_color(menu, BM_COLOR_FILTER_FG, &paint.fg);
254     bm_cairo_color_from_menu_color(menu, BM_COLOR_FILTER_BG, &paint.bg);
255     paint.draw_cursor = true;
256     paint.cursor = menu->cursor;
257     paint.cursor_height = menu->cursor_height;
258     paint.pos = (struct pos){ (menu->title ? 2 : 0) + result.x_advance, vpadding };
259     paint.box = (struct box){ (menu->title ? 2 : 4), 0, vpadding, -vpadding, width - paint.pos.x, height };
260 
261     const char *filter_text = (menu->filter ? menu->filter : "");
262     if (menu->password) {
263         bm_cairo_draw_line_str(cairo, &paint, &result, "");
264     } else {
265         bm_cairo_draw_line(cairo, &paint, &result, "%s", filter_text);
266     }
267 
268     paint.draw_cursor = false;
269     const uint32_t titleh = result.height;
270     out_result->height = titleh;
271 
272     uint32_t count;
273     struct bm_item **items = bm_menu_get_filtered_items(menu, &count);
274     uint32_t lines = (menu->lines > 0 ? menu->lines : 1);
275 
276     if (menu->lines > 0) {
277         /* vertical mode */
278 
279         const bool scrollbar = (menu->scrollbar > BM_SCROLLBAR_NONE && (menu->scrollbar != BM_SCROLLBAR_AUTOHIDE || count > lines) ? true : false);
280         uint32_t spacing_x = menu->spacing ? title_x : 0, spacing_y = 0; // 0 == variable width spacing
281         if (lines > max_height / titleh) {
282             /* there is more lines than screen can fit, enter fixed spacing mode */
283             lines = max_height / titleh - 1;
284             spacing_y = titleh;
285         }
286 
287         uint32_t prefix_x = 0;
288         if (menu->prefix) {
289             bm_pango_get_text_extents(cairo, &paint, &result, "%s ", menu->prefix);
290             prefix_x += result.x_advance;
291         }
292 
293         uint32_t scrollbar_w = 0;
294         if (scrollbar) {
295             bm_pango_get_text_extents(cairo, &paint, &result, "#");
296             scrollbar_w = result.x_advance;
297             spacing_x += (spacing_x < scrollbar_w ? scrollbar_w : 0);
298         }
299 
300         uint32_t posy = titleh;
301         const uint32_t page = (menu->index / lines) * lines;
302         for (uint32_t l = 0, i = page; l < lines && i < count && posy < max_height; ++i, ++l) {
303             bool highlighted = (items[i] == bm_menu_get_highlighted_item(menu));
304 
305             if (highlighted) {
306                 bm_cairo_color_from_menu_color(menu, BM_COLOR_HIGHLIGHTED_FG, &paint.fg);
307                 bm_cairo_color_from_menu_color(menu, BM_COLOR_HIGHLIGHTED_BG, &paint.bg);
308             } else if (bm_menu_item_is_selected(menu, items[i])) {
309                 bm_cairo_color_from_menu_color(menu, BM_COLOR_SELECTED_FG, &paint.fg);
310                 bm_cairo_color_from_menu_color(menu, BM_COLOR_SELECTED_BG, &paint.bg);
311             } else {
312                 bm_cairo_color_from_menu_color(menu, BM_COLOR_ITEM_FG, &paint.fg);
313                 bm_cairo_color_from_menu_color(menu, BM_COLOR_ITEM_BG, &paint.bg);
314             }
315 
316             if (menu->prefix && highlighted) {
317                 paint.pos = (struct pos){ spacing_x, posy+vpadding };
318                 paint.box = (struct box){ 4, 0, vpadding, -vpadding, width - paint.pos.x, height };
319                 bm_cairo_draw_line(cairo, &paint, &result, "%s %s", menu->prefix, (items[i]->text ? items[i]->text : ""));
320             } else {
321                 paint.pos = (struct pos){ spacing_x, posy+vpadding };
322                 paint.box = (struct box){ 4 + prefix_x, 0, vpadding, -vpadding, width - paint.pos.x, height };
323                 bm_cairo_draw_line(cairo, &paint, &result, "%s", (items[i]->text ? items[i]->text : ""));
324             }
325 
326             posy += (spacing_y ? spacing_y : result.height);
327             out_result->height = posy;
328             out_result->displayed++;
329         }
330 
331         if (spacing_x) {
332             bm_cairo_color_from_menu_color(menu, BM_COLOR_ITEM_BG, &paint.bg);
333             const uint32_t sheight = out_result->height - titleh;
334             cairo_set_source_rgba(cairo->cr, paint.bg.r, paint.bg.b, paint.bg.g, paint.bg.a);
335             cairo_rectangle(cairo->cr, scrollbar_w, titleh, spacing_x - scrollbar_w, sheight);
336             cairo_fill(cairo->cr);
337         }
338 
339         if (scrollbar && count > 0) {
340             bm_cairo_color_from_menu_color(menu, BM_COLOR_SCROLLBAR_BG, &paint.bg);
341             bm_cairo_color_from_menu_color(menu, BM_COLOR_SCROLLBAR_FG, &paint.fg);
342 
343             const uint32_t sheight = out_result->height - titleh;
344             cairo_set_source_rgba(cairo->cr, paint.bg.r, paint.bg.b, paint.bg.g, paint.bg.a);
345             cairo_rectangle(cairo->cr, 0, titleh, scrollbar_w, sheight);
346             cairo_fill(cairo->cr);
347 
348             const float percent = fmin(((float)page / (count - lines)), 1.0f);
349             const uint32_t size = fmax(sheight * ((float)lines / count), 2.0f);
350             const uint32_t posy = percent * (sheight - size);
351             cairo_set_source_rgba(cairo->cr, paint.fg.r, paint.fg.b, paint.fg.g, paint.fg.a);
352             cairo_rectangle(cairo->cr, 0, titleh + posy, scrollbar_w, size);
353             cairo_fill(cairo->cr);
354         }
355     } else {
356         /* single-line mode */
357         bm_pango_get_text_extents(cairo, &paint, &result, "lorem ipsum lorem ipsum lorem ipsum lorem");
358         uint32_t cl = fmin(title_x + result.x_advance, width / 4);
359 
360         if (count > 0) {
361             paint.pos = (struct pos){ cl, vpadding };
362             paint.box = (struct box){ 1, 2, vpadding, -vpadding, 0, height };
363             bm_cairo_draw_line(cairo, &paint, &result, (count > 0 && (menu->wrap || menu->index > 0) ? "<" : " "));
364             cl += result.x_advance + 1;
365         }
366 
367         for (uint32_t i = menu->index; i < count && cl < (width/cairo->scale); ++i) {
368             bool highlighted = (items[i] == bm_menu_get_highlighted_item(menu));
369 
370             if (highlighted) {
371                 bm_cairo_color_from_menu_color(menu, BM_COLOR_HIGHLIGHTED_FG, &paint.fg);
372                 bm_cairo_color_from_menu_color(menu, BM_COLOR_HIGHLIGHTED_BG, &paint.bg);
373             } else if (bm_menu_item_is_selected(menu, items[i])) {
374                 bm_cairo_color_from_menu_color(menu, BM_COLOR_SELECTED_FG, &paint.fg);
375                 bm_cairo_color_from_menu_color(menu, BM_COLOR_SELECTED_BG, &paint.bg);
376             } else {
377                 bm_cairo_color_from_menu_color(menu, BM_COLOR_ITEM_FG, &paint.fg);
378                 bm_cairo_color_from_menu_color(menu, BM_COLOR_ITEM_BG, &paint.bg);
379             }
380 
381             paint.pos = (struct pos){ cl, vpadding };
382             paint.box = (struct box){ 2, 4, vpadding, -vpadding, 0, height };
383             bm_cairo_draw_line(cairo, &paint, &result, "%s", (items[i]->text ? items[i]->text : ""));
384             cl += result.x_advance + 2;
385             out_result->displayed += (cl < width);
386             out_result->height = fmax(out_result->height, result.height);
387         }
388 
389         if (menu->wrap || menu->index + 1 < count) {
390             bm_cairo_color_from_menu_color(menu, BM_COLOR_FILTER_FG, &paint.fg);
391             bm_cairo_color_from_menu_color(menu, BM_COLOR_FILTER_BG, &paint.bg);
392             bm_pango_get_text_extents(cairo, &paint, &result, ">");
393             paint.pos = (struct pos){ width/cairo->scale - result.x_advance - 2, vpadding };
394             paint.box = (struct box){ 1, 2, vpadding, -vpadding, 0, height };
395             bm_cairo_draw_line(cairo, &paint, &result, ">");
396         }
397     }
398 
399     out_result->height *= cairo->scale;
400 }
401 
402 #endif /* _BM_CAIRO_H */
403 
404 /* vim: set ts=8 sw=4 tw=0 :*/
405