1 /*  bubblegen.c -- Generate various sorts of bubbles.
2  *  Copyright (C) 2008-2020  Nick Gasson
3  *
4  *  This program is free software: you can redistribute it and/or modify
5  *  it under the terms of the GNU General Public License as published by
6  *  the Free Software Foundation, either version 3 of the License, or
7  *  (at your option) any later version.
8  *
9  *  This program is distributed in the hope that it will be useful,
10  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
11  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  *  GNU General Public License for more details.
13  *
14  *  You should have received a copy of the GNU General Public License
15  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 #ifdef HAVE_CONFIG_H
19 #include "config.h"
20 #endif
21 
22 #include <stdio.h>
23 #include <stdlib.h>
24 #include <stdbool.h>
25 #include <string.h>
26 #include <math.h>
27 
28 #include <gtk/gtk.h>
29 #include <gdk-pixbuf/gdk-pixbuf.h>
30 
31 #include "floating_shape.h"
32 #include "display_cow.h"
33 #include "settings.h"
34 #include "i18n.h"
35 
36 #define LEFT_BUF       5   // Amount of pixels to leave after cow's tail
37 #define TIP_WIDTH      20  // Length of the triangle bit on the speech bubble
38 #define THINK_WIDTH    80 // Spaces for thinking circles
39 #define CORNER_RADIUS  30  // Radius of corners on the speech bubble
40 #define CORNER_DIAM    CORNER_RADIUS*2
41 #define BUBBLE_BORDER  5   // Pixels to leave free around edge of bubble
42 #define MIN_TIP_HEIGHT 15
43 
44 // These next ones control the size and position of the "thinking circles"
45 // (or whatever you call them)
46 #define BIG_KIRCLE_X      38
47 #define BIG_KIRCLE_Y      70
48 #define BIG_KIRCLE_DIAM   35
49 #define BIG_KIRCLE_RADIUS (BIG_KIRCLE_DIAM / 2)
50 
51 #define SMALL_KIRCLE_X      5
52 #define SMALL_KIRCLE_Y      40
53 #define SMALL_KIRCLE_DIAM   20
54 #define SMALL_KIRCLE_RADIUS (SMALL_KIRCLE_DIAM / 2)
55 
56 // Min distance from top of the big kircle to the top of the bubble
57 #define KIRCLE_TOP_MIN  10
58 
59 typedef struct {
60    int width, height;
61    cairo_surface_t *surface;
62    cairo_t *cr;
63 } bubble_t;
64 
65 typedef enum { NORMAL, THOUGHT } bubble_style_t;
66 
bubble_corner_arcs(bubble_t * b,bubble_style_t style,int corners[4][2])67 static void bubble_corner_arcs(bubble_t *b, bubble_style_t style,
68                                int corners[4][2])
69 {
70    // Space between cow and bubble
71    int middle = (style == NORMAL ? TIP_WIDTH : THINK_WIDTH);
72 
73    if (get_bool_option("left")) {
74       corners[0][0] = BUBBLE_BORDER + CORNER_RADIUS;
75       corners[0][1] = BUBBLE_BORDER + CORNER_RADIUS;
76 
77       corners[3][0] = BUBBLE_BORDER + CORNER_RADIUS;
78       corners[3][1] = b->height - CORNER_DIAM + CORNER_RADIUS;
79 
80       corners[2][0] = b->width - CORNER_DIAM - BUBBLE_BORDER - middle + CORNER_RADIUS;
81       corners[2][1] = b->height - CORNER_DIAM + CORNER_RADIUS;
82 
83       corners[1][0] = b->width - CORNER_DIAM - BUBBLE_BORDER - middle + CORNER_RADIUS;
84       corners[1][1] = BUBBLE_BORDER + CORNER_RADIUS;
85    }
86    else {
87       corners[0][0] = middle + BUBBLE_BORDER + CORNER_RADIUS;
88       corners[0][1] = BUBBLE_BORDER + CORNER_RADIUS;
89 
90       corners[3][0] = middle + BUBBLE_BORDER + CORNER_RADIUS;
91       corners[3][1] = b->height - CORNER_DIAM + CORNER_RADIUS;
92 
93       corners[2][0] = b->width - CORNER_DIAM - BUBBLE_BORDER + CORNER_RADIUS;
94       corners[2][1] = b->height - CORNER_DIAM + CORNER_RADIUS;
95 
96       corners[1][0] = b->width - CORNER_DIAM - BUBBLE_BORDER + CORNER_RADIUS;
97       corners[1][1] = BUBBLE_BORDER + CORNER_RADIUS;
98    }
99 }
100 
bubble_init_cairo(bubble_t * b,cairo_t * cr,bubble_style_t style)101 static void bubble_init_cairo(bubble_t *b, cairo_t *cr, bubble_style_t style)
102 {
103    GdkPoint tip_points[5];
104    bool right = !get_bool_option("left");
105 
106    cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.0);
107    cairo_rectangle(cr, 0, 0, b->width, b->height);
108    cairo_fill(cr);
109 
110    b->width -= BUBBLE_BORDER;
111    b->height -= BUBBLE_BORDER;
112 
113    // Space between cow and bubble
114    int middle = style == NORMAL ? TIP_WIDTH : THINK_WIDTH;
115 
116    // Draw the white corners
117    int corners[4][2];
118    bubble_corner_arcs(b, style, corners);
119 
120    cairo_set_line_width(cr, 4.0);
121    cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
122    cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
123 
124    cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
125 
126    for (int i = 0; i < 4; i++) {
127       cairo_move_to(cr, corners[i][0], corners[i][1]);
128       cairo_arc(cr, corners[i][0], corners[i][1],
129                 CORNER_RADIUS,
130                 M_PI + i * (M_PI / 2.0),
131                 M_PI + (i+1) * (M_PI / 2.0));
132       cairo_close_path(cr);
133       cairo_fill(cr);
134    }
135 
136    // Fill in the middle of the bubble
137    cairo_rectangle(cr,
138                    CORNER_RADIUS + (right ? middle : 0) + BUBBLE_BORDER,
139                    BUBBLE_BORDER,
140                    b->width - middle - BUBBLE_BORDER - CORNER_DIAM,
141                    b->height - BUBBLE_BORDER);
142    cairo_rectangle(cr,
143                    (right ? middle : 0) + BUBBLE_BORDER,
144                    BUBBLE_BORDER + CORNER_RADIUS,
145                    b->width - middle - BUBBLE_BORDER*2,
146                    b->height - BUBBLE_BORDER - CORNER_DIAM);
147    cairo_fill(cr);
148 
149    if (style == NORMAL) {
150       // The points on the tip part
151       int tip_compute_offset = (b->height - BUBBLE_BORDER - CORNER_DIAM)/3;
152       int tip_offset[3] = { tip_compute_offset, tip_compute_offset, tip_compute_offset };
153       if (tip_compute_offset < MIN_TIP_HEIGHT) {
154          int new_offset = (b->height - BUBBLE_BORDER - CORNER_DIAM - MIN_TIP_HEIGHT)/2;
155          tip_offset[0] = new_offset;
156          tip_offset[1] = MIN_TIP_HEIGHT;
157          tip_offset[2] = new_offset;
158       }
159 
160       if (right) {
161          tip_points[0].x = middle + BUBBLE_BORDER;
162          tip_points[0].y = BUBBLE_BORDER + CORNER_RADIUS;
163          tip_points[1].x = middle + BUBBLE_BORDER;
164          tip_points[1].y = BUBBLE_BORDER + CORNER_RADIUS + tip_offset[0];
165          tip_points[2].x = BUBBLE_BORDER;
166          tip_points[2].y = BUBBLE_BORDER + CORNER_RADIUS + tip_offset[0] + tip_offset[1]/2;
167          tip_points[3].x = middle + BUBBLE_BORDER;
168          tip_points[3].y = BUBBLE_BORDER + CORNER_RADIUS + tip_offset[0] + tip_offset[1];
169          tip_points[4].x = middle + BUBBLE_BORDER;
170          tip_points[4].y = b->height - CORNER_RADIUS;
171       }
172       else {
173          tip_points[0].x = b->width - middle - BUBBLE_BORDER;
174          tip_points[0].y = BUBBLE_BORDER + CORNER_RADIUS;
175          tip_points[1].x = b->width - middle - BUBBLE_BORDER;
176          tip_points[1].y = BUBBLE_BORDER + CORNER_RADIUS + tip_offset[0];
177          tip_points[2].x = b->width - BUBBLE_BORDER;
178          tip_points[2].y = BUBBLE_BORDER + CORNER_RADIUS + tip_offset[0] + tip_offset[1]/2;
179          tip_points[3].x = b->width - middle - BUBBLE_BORDER;
180          tip_points[3].y = BUBBLE_BORDER + CORNER_RADIUS + tip_offset[0] + tip_offset[1];
181          tip_points[4].x = b->width - middle - BUBBLE_BORDER;
182          tip_points[4].y = b->height - CORNER_RADIUS;
183       }
184 
185       cairo_move_to(cr, tip_points[0].x, tip_points[0].y);
186       for (int i = 1; i < 5; i++) {
187          cairo_line_to(cr, tip_points[i].x, tip_points[i].y);
188       }
189 
190       cairo_close_path(cr);
191       cairo_fill(cr);
192    }
193    else {
194       // Incrementally move the top kircle down so it's within the
195       // bubble's border
196       int big_y = BIG_KIRCLE_Y;
197       int small_y = SMALL_KIRCLE_Y;
198 
199       while (big_y + KIRCLE_TOP_MIN > b->height/2) {
200          big_y /= 2;
201          small_y /= 2;
202       }
203 
204       // Draw two think kircles
205       cairo_arc(cr,
206                 (right ? BIG_KIRCLE_X + BIG_KIRCLE_RADIUS
207                  : b->width - BIG_KIRCLE_X - BIG_KIRCLE_RADIUS),
208                 b->height/2 - big_y + BIG_KIRCLE_RADIUS, BIG_KIRCLE_RADIUS,
209                 0, 2.0 * M_PI);
210 
211       cairo_arc(cr,
212                 (right ? SMALL_KIRCLE_X + SMALL_KIRCLE_RADIUS
213                  : b->width - SMALL_KIRCLE_X - SMALL_KIRCLE_RADIUS),
214                 b->height/2 - small_y + SMALL_KIRCLE_RADIUS, SMALL_KIRCLE_RADIUS,
215                 0, 2.0 * M_PI);
216 
217       cairo_fill(cr);
218 
219       cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
220 
221       cairo_arc(cr,
222                 (right ? BIG_KIRCLE_X + BIG_KIRCLE_RADIUS
223                  : b->width - BIG_KIRCLE_X - BIG_KIRCLE_RADIUS),
224                 b->height/2 - big_y + BIG_KIRCLE_RADIUS, BIG_KIRCLE_RADIUS,
225                 0, 2.0 * M_PI);
226 
227       cairo_stroke(cr);
228 
229       cairo_arc(cr,
230                 (right ? SMALL_KIRCLE_X + SMALL_KIRCLE_RADIUS
231                  : b->width - SMALL_KIRCLE_X - SMALL_KIRCLE_RADIUS),
232                 b->height/2 - small_y + SMALL_KIRCLE_RADIUS, SMALL_KIRCLE_RADIUS,
233                 0, 2.0 * M_PI);
234 
235       cairo_stroke(cr);
236    }
237 
238    // Draw the black rounded corners
239    cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
240 
241    // Top left
242    cairo_arc(cr,
243              (right ? middle : 0) + BUBBLE_BORDER + CORNER_RADIUS,
244              BUBBLE_BORDER + CORNER_RADIUS,
245              CORNER_RADIUS,
246              M_PI, M_PI + (M_PI / 2.0));
247 
248    // Top right
249    cairo_arc(cr,
250              b->width - (!right ? middle : 0) - CORNER_RADIUS - BUBBLE_BORDER,
251              BUBBLE_BORDER + CORNER_RADIUS,
252              CORNER_RADIUS,
253              M_PI + (M_PI / 2.0), 2 * M_PI);
254 
255    if (style == NORMAL && !right) {
256       cairo_move_to(cr, tip_points[0].x, tip_points[0].y);
257       for (int i = 1; i < 5; i++) {
258          cairo_line_to(cr, tip_points[i].x, tip_points[i].y);
259       }
260    }
261 
262    // Bottom left
263    cairo_arc(cr,
264              b->width - (!right ? middle : 0) - CORNER_RADIUS - BUBBLE_BORDER,
265              b->height - CORNER_RADIUS,
266              CORNER_RADIUS,
267              0.0, (M_PI / 2.0));
268 
269    cairo_arc(cr,
270              (right ? middle : 0) + BUBBLE_BORDER + CORNER_RADIUS,
271              b->height - CORNER_RADIUS,
272              CORNER_RADIUS,
273              M_PI / 2.0, M_PI);
274 
275    if (style == NORMAL && right) {
276       cairo_move_to(cr, tip_points[0].x, tip_points[0].y);
277       for (int i = 1; i < 5; i++) {
278          cairo_line_to(cr, tip_points[i].x, tip_points[i].y);
279       }
280    }
281    else {
282       cairo_line_to(cr, (right ? middle : 0) + BUBBLE_BORDER,
283                     BUBBLE_BORDER + CORNER_RADIUS);
284    }
285 
286    cairo_stroke(cr);
287 }
288 
bubble_init(bubble_t * b,bubble_style_t style)289 static void bubble_init(bubble_t *b, bubble_style_t style)
290 {
291    b->surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32,
292                                            b->width, b->height);
293    g_assert(b->surface);
294 
295    b->cr = cairo_create(b->surface);
296 
297    bubble_init_cairo(b, b->cr, style);
298 }
299 
bubble_size_from_content(bubble_t * b,bubble_style_t style,int c_width,int c_height)300 static void bubble_size_from_content(bubble_t *b, bubble_style_t style,
301                                      int c_width, int c_height)
302 {
303    int middle = style == NORMAL ? TIP_WIDTH : THINK_WIDTH;
304    b->width = 2*BUBBLE_BORDER + CORNER_DIAM + middle + c_width;
305    b->height = BUBBLE_BORDER + CORNER_DIAM + c_height;
306 }
307 
bubble_tidy(bubble_t * b)308 static GdkPixbuf *bubble_tidy(bubble_t *b)
309 {
310    GdkPixbuf *pixbuf =
311       gdk_pixbuf_get_from_surface(b->surface, 0, 0, b->width + BUBBLE_BORDER, b->height + BUBBLE_BORDER);
312 
313    cairo_surface_destroy(b->surface);
314    return pixbuf;
315 }
316 
bubble_content_left(bubble_style_t style)317 static int bubble_content_left(bubble_style_t style)
318 {
319    if (get_bool_option("left")) {
320       return BUBBLE_BORDER + CORNER_RADIUS;
321    }
322    else {
323       const int middle = style == NORMAL ? TIP_WIDTH : THINK_WIDTH;
324       return BUBBLE_BORDER + middle + CORNER_RADIUS;
325    }
326 }
327 
bubble_content_top()328 static int bubble_content_top()
329 {
330    return CORNER_RADIUS;
331 }
332 
make_dream_bubble(const char * file,int * p_width,int * p_height)333 GdkPixbuf *make_dream_bubble(const char *file, int *p_width, int *p_height)
334 {
335    bubble_t bubble;
336    GError *error = NULL;
337    GdkPixbuf *image = gdk_pixbuf_new_from_file(file, &error);
338 
339    if (NULL == image) {
340       fprintf(stderr, "Error: failed to load %s\n", file);
341       exit(1);
342    }
343 
344    bubble_size_from_content(&bubble, THOUGHT, gdk_pixbuf_get_width(image),
345                             gdk_pixbuf_get_height(image));
346    *p_width = bubble.width;
347    *p_height = bubble.height;
348 
349    bubble_init(&bubble, THOUGHT);
350 
351    gdk_cairo_set_source_pixbuf(bubble.cr, image,
352                                bubble_content_left(THOUGHT),
353                                bubble_content_top());
354    cairo_paint(bubble.cr);
355 
356    cairo_destroy(bubble.cr);
357    g_object_unref(image);
358 
359    return bubble_tidy(&bubble);
360 }
361 
make_text_bubble(char * text,int * p_width,int * p_height,int max_width,cowmode_t mode)362 GdkPixbuf *make_text_bubble(char *text, int *p_width, int *p_height,
363                             int max_width, cowmode_t mode)
364 {
365    bubble_t bubble;
366    int text_width, text_height;
367 
368    // Work out the size of the bubble from the text
369    PangoContext *pango_context = gdk_pango_context_get();
370    PangoLayout *layout = pango_layout_new(pango_context);
371    PangoFontDescription *font =
372       pango_font_description_from_string(get_string_option("font"));
373    PangoAttrList *pango_attrs = NULL;
374 
375    // Adjust max width to account for bubble edges
376    max_width -= LEFT_BUF;
377    max_width -= TIP_WIDTH;
378    max_width -= 2 * BUBBLE_BORDER;
379    max_width -= CORNER_DIAM;
380 
381    if (get_bool_option("wrap")) {
382       pango_layout_set_width(layout, max_width * PANGO_SCALE);
383       pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR);
384    }
385 
386    char *stripped;
387    if (!pango_parse_markup(text, -1, 0, &pango_attrs,
388          &stripped, NULL, NULL)) {
389 
390       // This isn't fatal as the the text may contain angled brackets, etc.
391       stripped = text;
392    }
393    else {
394       pango_layout_set_attributes(layout, pango_attrs);
395    }
396 
397    pango_layout_set_font_description(layout, font);
398    pango_layout_set_text(layout, stripped, -1);
399    pango_layout_get_pixel_size(layout, &text_width, &text_height);
400 
401    bubble_style_t style = mode == COWMODE_NORMAL ? NORMAL : THOUGHT;
402 
403    bubble_size_from_content(&bubble, style, text_width, text_height);
404    *p_width = bubble.width;
405    *p_height = bubble.height;
406 
407    bubble_init(&bubble, style);
408 
409    // Render the text
410    cairo_move_to(bubble.cr, bubble_content_left(style), bubble_content_top());
411    pango_cairo_show_layout(bubble.cr, layout);
412 
413    cairo_destroy(bubble.cr);
414 
415    // Make sure to free the Pango objects
416    g_object_unref(pango_context);
417    g_object_unref(layout);
418    pango_font_description_free(font);
419    if (NULL != pango_attrs)
420       pango_attr_list_unref(pango_attrs);
421 
422    return bubble_tidy(&bubble);
423 }
424