narrative_pos_trail_to_offset: Consider end of run as within run
[colloquium.git] / libstorycode / narrative_render_cairo.c
1 /*
2  * narrative_render_cairo.c
3  *
4  * Copyright © 2013-2019 Thomas White <taw@bitwiz.org.uk>
5  *
6  * This file is part of Colloquium.
7  *
8  * Colloquium is free software: you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation, either version 3 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
20  *
21  */
22
23
24 #ifdef HAVE_CONFIG_H
25 #include <config.h>
26 #endif
27
28 #include <cairo.h>
29 #include <cairo-pdf.h>
30 #include <pango/pangocairo.h>
31 #include <assert.h>
32 #include <string.h>
33 #include <stdlib.h>
34 #include <math.h>
35
36 #include <libintl.h>
37 #define _(x) gettext(x)
38
39 #include "slide.h"
40 #include "narrative.h"
41 #include "stylesheet.h"
42 #include "imagestore.h"
43 #include "slide_render_cairo.h"
44 #include "narrative_render_cairo.h"
45 #include "render_cairo_common.h"
46
47 #include "narrative_priv.h"
48
49 const double dummy_h_val = 1024.0;
50
51
52 static double lcalc(struct length l, double pd)
53 {
54         if ( l.unit == LENGTH_UNIT ) {
55                 return l.len;
56         } else {
57                 return l.len * pd;
58         }
59 }
60
61
62 static PangoAlignment to_pangoalignment(enum alignment align)
63 {
64         switch ( align ) {
65                 case ALIGN_LEFT : return PANGO_ALIGN_LEFT;
66                 case ALIGN_RIGHT : return PANGO_ALIGN_RIGHT;
67                 case ALIGN_CENTER : return PANGO_ALIGN_CENTER;
68                 default: return PANGO_ALIGN_LEFT;
69         }
70 }
71
72
73 static void wrap_text(struct narrative_item *item, PangoContext *pc,
74                       Stylesheet *ss, const char *stn, double w,
75                       size_t sel_start, size_t sel_end)
76 {
77         PangoAlignment palignment;
78         PangoRectangle rect;
79         const char *font;
80         PangoFontDescription *fontdesc;
81         enum alignment align;
82         double wrap_w;
83         PangoAttrList *attrs;
84         PangoAttribute *attr;
85         struct colour fgcol;
86         guint16 r, g, b;
87
88         font = stylesheet_get_font(ss, stn, &fgcol, &align);
89         if ( font == NULL ) return;
90         fontdesc = pango_font_description_from_string(font);
91
92         if ( item->align == ALIGN_INHERIT ) {
93                 /* Use value from stylesheet */
94                 palignment = to_pangoalignment(align);
95         } else {
96                 /* Use item-specific value */
97                 palignment = to_pangoalignment(item->align);
98         }
99
100         /* Calculate width of actual text */
101         wrap_w = w - item->space_l - item->space_r;
102
103         /* Set foreground colour */
104         attrs = pango_attr_list_new();
105         r = fgcol.rgba[0] * 65535;
106         g = fgcol.rgba[1] * 65535;
107         b = fgcol.rgba[2] * 65535;
108         attr = pango_attr_foreground_new(r, g, b);
109         pango_attr_list_insert(attrs, attr);
110
111         /* Add attributes for selected text */
112         if ( sel_start > 0 || sel_end > 0 ) {
113                 PangoAttribute *attr;
114                 attr = pango_attr_background_new(42919, 58853, 65535);
115                 attr->start_index = sel_start;
116                 attr->end_index = sel_end;
117                 pango_attr_list_insert(attrs, attr);
118         }
119
120         if ( item->layout == NULL ) {
121                 item->layout = pango_layout_new(pc);
122         }
123         pango_layout_set_width(item->layout, pango_units_from_double(wrap_w));
124         pango_layout_set_alignment(item->layout, palignment);
125         pango_layout_set_font_description(item->layout, fontdesc);
126         pango_layout_set_attributes(item->layout, attrs);
127
128         if ( runs_to_pangolayout(item->layout, item->runs, item->n_runs) ) return;
129
130         pango_layout_get_extents(item->layout, NULL, &rect);
131         item->obj_w = pango_units_to_double(rect.width);
132         item->obj_h = pango_units_to_double(rect.height);
133
134         pango_attr_list_unref(attrs);
135 }
136
137
138 /* Render a thumbnail of the slide */
139 static cairo_surface_t *render_thumbnail(Slide *s, Stylesheet *ss, ImageStore *is,
140                                          int w, int h)
141 {
142         cairo_surface_t *surf;
143         cairo_surface_t *full_surf;
144         cairo_t *cr;
145         double logical_w, logical_h;
146         PangoContext *pc;
147         const int rh = 1024; /* "reasonably big" height for slide */
148         int rw;
149         struct slide_pos sel;
150
151         slide_get_logical_size(s, ss, &logical_w, &logical_h);
152         rw = rh*(logical_w/logical_h);
153
154         sel.para = 0;  sel.pos = 0;  sel.trail = 0;
155
156         /* Render at a reasonably big size.  Rendering to a small surface makes
157          * rounding of text positions (due to font hinting) cause significant
158          * differences between the thumbnail and "normal" rendering. */
159         full_surf = cairo_image_surface_create(CAIRO_FORMAT_RGB24, rw, rh);
160         cr = cairo_create(full_surf);
161         cairo_scale(cr, (double)rw/logical_w, (double)rh/logical_h);
162         pc = pango_cairo_create_context(cr);
163         slide_render_cairo(s, cr, is, ss, 0, pango_language_get_default(), pc,
164                            NULL, sel, sel);
165         g_object_unref(pc);
166         cairo_destroy(cr);
167
168         /* Scale down to the actual size of the thumbnail */
169         surf = cairo_image_surface_create(CAIRO_FORMAT_RGB24, w, h);
170         cr = cairo_create(surf);
171         cairo_scale(cr, (double)w/rw, (double)h/rh);
172         cairo_set_source_surface(cr, full_surf, 0.0, 0.0);
173         cairo_paint(cr);
174         cairo_destroy(cr);
175
176         cairo_surface_destroy(full_surf);
177         return surf;
178 }
179
180
181 static void wrap_slide(struct narrative_item *item, Stylesheet *ss, ImageStore *is, int sel_block)
182 {
183         double w, h;
184
185         slide_get_logical_size(item->slide, ss, &w, &h);
186         item->obj_h = 320.0;  /* Actual height of thumbnail */
187         item->obj_w = rint(item->obj_h*w/h);
188
189         if ( item->slide_thumbnail != NULL ) {
190                 cairo_surface_destroy(item->slide_thumbnail);
191         }
192         item->slide_thumbnail = render_thumbnail(item->slide, ss, is,
193                                                  item->obj_w, item->obj_h);
194         item->selected = sel_block;
195 }
196
197
198 size_t narrative_pos_trail_to_offset(Narrative *n, int i, int offs, int trail)
199 {
200         int run;
201         struct narrative_item *item = &n->items[i];
202         size_t pos;
203
204         if ( !narrative_item_is_text(n, i) ) return offs;
205
206         pos = 0;
207         for ( run=0; run<item->n_runs; run++ ) {
208                 pos += strlen(item->runs[run].text);
209                 if ( pos >= offs ) {
210                         glong char_offs;
211                         char *ptr;
212                         char_offs = g_utf8_pointer_to_offset(item->runs[run].text,
213                                                              item->runs[run].text+offs);
214                         char_offs += trail;
215                         ptr = g_utf8_offset_to_pointer(item->runs[run].text, char_offs);
216                         return ptr - item->runs[run].text;
217                 }
218         }
219
220         fprintf(stderr, "narrative_pos_trail_to_offset: Outside last run\n");
221         return 0;
222 }
223
224
225 static int positions_equal(struct edit_pos a, struct edit_pos b)
226 {
227         if ( a.para != b.para ) return 0;
228         if ( a.pos != b.pos ) return 0;
229         if ( a.trail != b.trail ) return 0;
230         return 1;
231 }
232
233
234 static void sort_positions(struct edit_pos *a, struct edit_pos *b)
235 {
236         if ( a->para > b->para ) {
237                 size_t tpos;
238                 int tpara, ttrail;
239                 tpara = b->para;   tpos = b->pos;  ttrail = b->trail;
240                 b->para = a->para;  b->pos = a->pos;  b->trail = a->trail;
241                 a->para = tpara;    a->pos = tpos;    a->trail = ttrail;
242         }
243
244         if ( (a->para == b->para) && (a->pos > b->pos) )
245         {
246                 size_t tpos = b->pos;
247                 int ttrail = b->trail;
248                 b->pos = a->pos;  b->trail = a->trail;
249                 a->pos = tpos;    a->trail = ttrail;
250         }
251 }
252
253
254 static void wrap_marker(struct narrative_item *item, PangoContext *pc, double w,
255                         int sel_block)
256 {
257         item->obj_w = w - item->space_l - item->space_r;
258         item->obj_h = 20.0;
259         item->selected = sel_block;
260 }
261
262
263 int narrative_wrap_range(Narrative *n, Stylesheet *stylesheet, PangoLanguage *lang,
264                          PangoContext *pc, double w, ImageStore *is,
265                          int min, int max,
266                          struct edit_pos sel_start, struct edit_pos sel_end)
267 {
268         int i;
269         struct length pad[4];
270         int sel_s, sel_e;
271         const char *stn;
272         struct length paraspace[4];
273
274         if ( stylesheet_get_padding(stylesheet, "NARRATIVE", pad) ) return 1;
275         n->space_l = lcalc(pad[0], w);
276         n->space_r = lcalc(pad[1], w);
277         n->space_t = lcalc(pad[2], dummy_h_val);
278         n->space_b = lcalc(pad[3], dummy_h_val);
279
280         n->w = w;
281         w -= n->space_l + n->space_r;
282
283         sort_positions(&sel_start, &sel_end);
284         if ( min < 0 ) min = 0;
285         if ( max >= n->n_items ) max = n->n_items-1;
286
287         if ( !positions_equal(sel_start, sel_end) ) {
288                 sel_s = narrative_pos_trail_to_offset(n, sel_start.para,
289                                                       sel_start.pos, sel_start.trail);
290                 sel_e = narrative_pos_trail_to_offset(n, sel_end.para,
291                                                       sel_end.pos, sel_end.trail);
292         } else {
293                 sel_s = 0;
294                 sel_e = 0;
295         }
296
297         for ( i=min; i<=max; i++ ) {
298
299                 size_t srt, end;
300                 int sel_block = 0;
301
302                 if ( i >= sel_start.para && i <= sel_end.para ) {
303                         if ( i == sel_start.para ) {
304                                 srt = sel_s;
305                         } else {
306                                 srt = 0;
307                         }
308                         if ( i == sel_end.para ) {
309                                 end = sel_e;
310                         } else {
311                                 end = G_MAXUINT;
312                         }
313                         if ( i > sel_start.para && i < sel_end.para ) {
314                                 end = G_MAXUINT;
315                         }
316                         sel_block = 1;
317                 } else {
318                         srt = 0;
319                         end = 0;
320                 }
321
322                 switch ( n->items[i].type ) {
323
324                         case NARRATIVE_ITEM_TEXT :
325                         stn = "NARRATIVE.TEXT";
326                         break;
327
328                         case NARRATIVE_ITEM_BP :
329                         stn = "NARRATIVE.BP";
330                         break;
331
332                         case NARRATIVE_ITEM_PRESTITLE :
333                         stn = "NARRATIVE.PRESTITLE";
334                         break;
335
336                         case NARRATIVE_ITEM_SLIDE :
337                         stn = "NARRATIVE.SLIDE";
338                         break;
339
340                         case NARRATIVE_ITEM_EOP :
341                         stn = "NARRATIVE.EOP";
342                         break;
343
344                         default :
345                         stn = NULL;
346                         break;
347                 }
348
349                 if ( stylesheet_get_paraspace(stylesheet, stn, paraspace) == 0 ) {
350                         n->items[i].space_l = lcalc(paraspace[0], w);
351                         n->items[i].space_r = lcalc(paraspace[1], w);
352                         n->items[i].space_t = lcalc(paraspace[2], dummy_h_val);
353                         n->items[i].space_b = lcalc(paraspace[3], dummy_h_val);
354                 }
355
356                 switch ( n->items[i].type ) {
357
358                         case NARRATIVE_ITEM_TEXT :
359                         case NARRATIVE_ITEM_BP :
360                         case NARRATIVE_ITEM_PRESTITLE :
361                         wrap_text(&n->items[i], pc, stylesheet,
362                                   stn, w, srt, end);
363                         break;
364
365                         case NARRATIVE_ITEM_SLIDE :
366                         wrap_slide(&n->items[i], stylesheet, is, sel_block);
367                         break;
368
369                         case NARRATIVE_ITEM_EOP :
370                         wrap_marker(&n->items[i], pc, w, sel_block);
371                         break;
372                 }
373         }
374
375         return 0;
376 }
377
378
379 double narrative_item_get_height(Narrative *n, int i)
380 {
381         return n->items[i].obj_h + n->items[i].space_t + n->items[i].space_b;
382 }
383
384
385 double narrative_get_height(Narrative *n)
386 {
387         int i;
388         double total = 0.0;
389         for ( i=0; i<n->n_items; i++ ) {
390                 total += narrative_item_get_height(n, i);
391         }
392         return total + n->space_t + n->space_b;
393 }
394
395
396 static void draw_slide(struct narrative_item *item, cairo_t *cr)
397 {
398         double x, y;
399
400         cairo_save(cr);
401         cairo_translate(cr, item->space_l, item->space_t);
402
403         x = 0.0;  y = 0.0;
404         cairo_user_to_device(cr, &x, &y);
405         x = rint(x);  y = rint(y);
406         cairo_device_to_user(cr, &x, &y);
407
408         if ( item->selected ) {
409                 cairo_rectangle(cr, x-5.0, y-5.0, item->obj_w+10.0, item->obj_h+10.0);
410                 cairo_set_source_rgb(cr, 0.655, 0.899, 1.0);
411                 cairo_fill(cr);
412         }
413
414         cairo_rectangle(cr, x, y, item->obj_w, item->obj_h);
415         cairo_set_source_surface(cr, item->slide_thumbnail, 0.0, 0.0);
416         cairo_fill(cr);
417
418         cairo_rectangle(cr, x+0.5, y+0.5, item->obj_w, item->obj_h);
419         cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
420         cairo_set_line_width(cr, 1.0);
421         cairo_stroke(cr);
422
423         cairo_restore(cr);
424 }
425
426
427 static void draw_marker(struct narrative_item *item, cairo_t *cr)
428 {
429         double x, y;
430         PangoLayout *layout;
431         PangoFontDescription *fontdesc;
432
433         cairo_save(cr);
434         cairo_translate(cr, item->space_l, item->space_t);
435
436         x = 0.0;  y = 0.0;
437         cairo_user_to_device(cr, &x, &y);
438         x = rint(x);  y = rint(y);
439         cairo_device_to_user(cr, &x, &y);
440
441         if ( item->selected ) {
442                 cairo_rectangle(cr, x-5.0, y-5.0, item->obj_w+10.0, item->obj_h+10.0);
443                 cairo_set_source_rgb(cr, 0.655, 0.899, 1.0);
444                 cairo_fill(cr);
445         }
446
447         cairo_rectangle(cr, x, y, item->obj_w, item->obj_h);
448         cairo_set_source_rgb(cr, 0.8, 0.0, 0.0);
449         cairo_fill(cr);
450
451         layout = pango_cairo_create_layout(cr);
452         pango_layout_set_text(layout, _("End of presentation"), -1);
453
454         fontdesc = pango_font_description_new();
455         pango_font_description_set_family_static(fontdesc, "Sans");
456         pango_font_description_set_style(fontdesc, PANGO_STYLE_ITALIC);
457         pango_font_description_set_absolute_size(fontdesc, 0.9*item->obj_h*PANGO_SCALE);
458         pango_layout_set_font_description(layout, fontdesc);
459         pango_layout_set_alignment(layout, PANGO_ALIGN_CENTER);
460         pango_layout_set_width(layout, item->obj_w*PANGO_SCALE);
461
462         cairo_move_to(cr, 0.0, 0.0);
463         cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
464         pango_cairo_update_layout(cr, layout);
465         pango_cairo_show_layout(cr, layout);
466         cairo_fill(cr);
467
468         g_object_unref(layout);
469         pango_font_description_free(fontdesc);
470
471         cairo_restore(cr);
472 }
473
474
475 static void draw_text(struct narrative_item *item, cairo_t *cr)
476 {
477         if ( item->layout == NULL ) return;
478
479         cairo_save(cr);
480         cairo_translate(cr, item->space_l, item->space_t);
481
482         //if ( (hpos + cur_h > min_y) && (hpos < max_y) ) {
483                 cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 1.0);
484                 pango_cairo_update_layout(cr, item->layout);
485                 pango_cairo_show_layout(cr, item->layout);
486                 cairo_fill(cr);
487         //} /* else paragraph is not visible */
488
489         cairo_restore(cr);
490 }
491
492
493 int narrative_render_item_cairo(Narrative*n, cairo_t *cr, int i)
494 {
495         switch ( n->items[i].type ) {
496
497                 case NARRATIVE_ITEM_TEXT :
498                 case NARRATIVE_ITEM_PRESTITLE :
499                 draw_text(&n->items[i], cr);
500                 break;
501
502                 case NARRATIVE_ITEM_BP :
503                 draw_text(&n->items[i], cr);
504                 break;
505
506                 case NARRATIVE_ITEM_SLIDE :
507                 draw_slide(&n->items[i], cr);
508                 break;
509
510                 case NARRATIVE_ITEM_EOP :
511                 draw_marker(&n->items[i], cr);
512                 break;
513
514         }
515
516         return 0;
517 }
518
519
520 /* NB You must first call narrative_wrap() */
521 int narrative_render_cairo(Narrative *n, cairo_t *cr, Stylesheet *stylesheet)
522 {
523         int i, r;
524         enum gradient bg;
525         struct colour bgcol;
526         struct colour bgcol2;
527         cairo_pattern_t *patt = NULL;
528
529         r = stylesheet_get_background(stylesheet, "NARRATIVE", &bg, &bgcol, &bgcol2);
530         if ( r ) return 1;
531
532         /* Overall background */
533         cairo_rectangle(cr, 0.0, 0.0, n->w, narrative_get_height(n));
534         switch ( bg ) {
535
536                 case GRAD_NONE:
537                 cairo_set_source_rgb(cr, bgcol.rgba[0], bgcol.rgba[1], bgcol.rgba[2]);
538                 break;
539
540                 case GRAD_VERT:
541                 patt = cairo_pattern_create_linear(0.0, 0.0, 0.0, narrative_get_height(n));
542                 cairo_pattern_add_color_stop_rgb(patt, 0.0,
543                                                  bgcol.rgba[0], bgcol.rgba[1], bgcol.rgba[2]);
544                 cairo_pattern_add_color_stop_rgb(patt, 1.0,
545                                                  bgcol2.rgba[0], bgcol2.rgba[1], bgcol2.rgba[2]);
546                 cairo_set_source(cr, patt);
547                 break;
548
549                 case GRAD_HORIZ:
550                 patt = cairo_pattern_create_linear(0.0, 0.0, n->w, 0.0);
551                 cairo_pattern_add_color_stop_rgb(patt, 0.0,
552                                                  bgcol.rgba[0], bgcol.rgba[1], bgcol.rgba[2]);
553                 cairo_pattern_add_color_stop_rgb(patt, 1.0,
554                                                  bgcol2.rgba[0], bgcol2.rgba[1], bgcol2.rgba[2]);
555                 cairo_set_source(cr, patt);
556                 break;
557
558         }
559         cairo_fill(cr);
560
561         cairo_save(cr);
562         cairo_translate(cr, n->space_l, n->space_t);
563
564         for ( i=0; i<n->n_items; i++ ) {
565                 narrative_render_item_cairo(n, cr, i);
566                 cairo_translate(cr, 0.0, narrative_item_get_height(n, i));
567         }
568
569         cairo_restore(cr);
570
571         return 0;
572 }