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