fixed bug with broken text area content when editing envelope text
[rocksndiamonds.git] / src / libgame / text.c
1 // ============================================================================
2 // Artsoft Retro-Game Library
3 // ----------------------------------------------------------------------------
4 // (c) 1995-2014 by Artsoft Entertainment
5 //                  Holger Schemel
6 //                  info@artsoft.org
7 //                  https://www.artsoft.org/
8 // ----------------------------------------------------------------------------
9 // text.c
10 // ============================================================================
11
12 #include <stdio.h>
13 #include <stdarg.h>
14
15 #include "text.h"
16 #include "misc.h"
17
18
19 // ============================================================================
20 // font functions
21 // ============================================================================
22
23 void InitFontInfo(struct FontBitmapInfo *font_bitmap_info, int num_fonts,
24                   int (*select_font_function)(int),
25                   int (*get_font_from_token_function)(char *))
26 {
27   gfx.num_fonts = num_fonts;
28   gfx.font_bitmap_info = font_bitmap_info;
29   gfx.select_font_function = select_font_function;
30   gfx.get_font_from_token_function = get_font_from_token_function;
31 }
32
33 void FreeFontInfo(struct FontBitmapInfo *font_bitmap_info)
34 {
35   if (font_bitmap_info == NULL)
36     return;
37
38   free(font_bitmap_info);
39 }
40
41 struct FontBitmapInfo *getFontBitmapInfo(int font_nr)
42 {
43   int font_bitmap_id = gfx.select_font_function(font_nr);
44
45   return &gfx.font_bitmap_info[font_bitmap_id];
46 }
47
48 int getFontWidth(int font_nr)
49 {
50   int font_bitmap_id = gfx.select_font_function(font_nr);
51
52   return gfx.font_bitmap_info[font_bitmap_id].width;
53 }
54
55 int getFontHeight(int font_nr)
56 {
57   int font_bitmap_id = gfx.select_font_function(font_nr);
58
59   return gfx.font_bitmap_info[font_bitmap_id].height;
60 }
61
62 int getFontDrawOffsetX(int font_nr)
63 {
64   int font_bitmap_id = gfx.select_font_function(font_nr);
65
66   return gfx.font_bitmap_info[font_bitmap_id].draw_xoffset;
67 }
68
69 int getFontDrawOffsetY(int font_nr)
70 {
71   int font_bitmap_id = gfx.select_font_function(font_nr);
72
73   return gfx.font_bitmap_info[font_bitmap_id].draw_yoffset;
74 }
75
76 int getTextWidth(char *text, int font_nr)
77 {
78   return (text != NULL ? strlen(text) * getFontWidth(font_nr) : 0);
79 }
80
81 static int getFontCharPosition(int font_nr, char c)
82 {
83   int font_bitmap_id = gfx.select_font_function(font_nr);
84   struct FontBitmapInfo *font = &gfx.font_bitmap_info[font_bitmap_id];
85   boolean default_font = (font->num_chars == DEFAULT_NUM_CHARS_PER_FONT);
86   int font_pos = (unsigned char)c - 32;
87
88   // map some special characters to their ascii values in default font
89   if (default_font)
90     font_pos = MAP_FONT_ASCII(c) - 32;
91
92   // this allows dynamic special characters together with special font
93   if (font_pos < 0 || font_pos >= font->num_chars)
94     font_pos = 0;
95
96   return font_pos;
97 }
98
99 void getFontCharSource(int font_nr, char c, Bitmap **bitmap, int *x, int *y)
100 {
101   int font_bitmap_id = gfx.select_font_function(font_nr);
102   struct FontBitmapInfo *font = &gfx.font_bitmap_info[font_bitmap_id];
103   int font_pos = getFontCharPosition(font_nr, c);
104   int offset_x = (font->offset_x != 0 ? font->offset_x : font->width);
105   int offset_y = (font->offset_y != 0 ? font->offset_y : font->height);
106
107   *bitmap = font->bitmap;
108   *x = font->src_x + (font_pos % font->num_chars_per_line) * offset_x;
109   *y = font->src_y + (font_pos / font->num_chars_per_line) * offset_y;
110 }
111
112
113 // ============================================================================
114 // text string helper functions
115 // ============================================================================
116
117 int maxWordLengthInRequestString(char *text)
118 {
119   char *text_ptr;
120   int word_len = 0, max_word_len = 0;
121
122   for (text_ptr = text; *text_ptr; text_ptr++)
123   {
124     word_len = (*text_ptr != ' ' &&
125                 *text_ptr != '?' &&
126                 *text_ptr != '!' ? word_len + 1 : 0);
127
128     max_word_len = MAX(word_len, max_word_len);
129   }
130
131   return max_word_len;
132 }
133
134
135 // ============================================================================
136 // simple text drawing functions
137 // ============================================================================
138
139 static void DrawInitTextExt(char *text, int ypos, int font_nr, boolean update)
140 {
141   LimitScreenUpdates(TRUE);
142
143   UPDATE_BUSY_STATE();
144
145   if (window != NULL &&
146       gfx.draw_init_text &&
147       gfx.num_fonts > 0 &&
148       gfx.font_bitmap_info[font_nr].bitmap != NULL)
149   {
150     int x = (video.width - getTextWidth(text, font_nr)) / 2;
151     int y = ypos;
152     int width = video.width;
153     int height = getFontHeight(font_nr);
154
155     ClearRectangleOnBackground(drawto, 0, y, width, height);
156     DrawTextExt(drawto, x, y, text, font_nr, BLIT_MASKED);
157
158     if (update)
159       BlitBitmap(drawto, window, 0, 0, video.width, video.height, 0, 0);
160   }
161 }
162
163 void DrawInitText(char *text, int ypos, int font_nr)
164 {
165   DrawInitTextExt(text, ypos, font_nr, FALSE);
166 }
167
168 void DrawInitTextHead(char *text)
169 {
170   DrawInitTextExt(text, 120, FC_GREEN, FALSE);
171 }
172
173 void DrawInitTextItem(char *text)
174 {
175   DrawInitTextExt(text, 150, FC_YELLOW, TRUE);
176 }
177
178 void DrawTextF(int x, int y, int font_nr, char *format, ...)
179 {
180   char buffer[MAX_OUTPUT_LINESIZE + 1];
181   va_list ap;
182
183   va_start(ap, format);
184   vsprintf(buffer, format, ap);
185   va_end(ap);
186
187   if (strlen(buffer) > MAX_OUTPUT_LINESIZE)
188     Fail("string too long in DrawTextF() -- aborting");
189
190   DrawText(gfx.sx + x, gfx.sy + y, buffer, font_nr);
191 }
192
193 void DrawTextFCentered(int y, int font_nr, char *format, ...)
194 {
195   char buffer[MAX_OUTPUT_LINESIZE + 1];
196   va_list ap;
197
198   va_start(ap, format);
199   vsprintf(buffer, format, ap);
200   va_end(ap);
201
202   if (strlen(buffer) > MAX_OUTPUT_LINESIZE)
203     Fail("string too long in DrawTextFCentered() -- aborting");
204
205   DrawText(gfx.sx + (gfx.sxsize - getTextWidth(buffer, font_nr)) / 2,
206            gfx.sy + y, buffer, font_nr);
207 }
208
209 void DrawTextS(int x, int y, int font_nr, char *text)
210 {
211   DrawText(gfx.sx + x, gfx.sy + y, text, font_nr);
212 }
213
214 void DrawTextSCentered(int y, int font_nr, char *text)
215 {
216   DrawText(gfx.sx + (gfx.sxsize - getTextWidth(text, font_nr)) / 2,
217            gfx.sy + y, text, font_nr);
218 }
219
220 void DrawTextSAligned(int x, int y, char *text, int font_nr, int align)
221 {
222   DrawText(gfx.sx + ALIGNED_XPOS(x, getTextWidth(text, font_nr), align),
223            gfx.sy + y, text, font_nr);
224 }
225
226 void DrawTextAligned(int x, int y, char *text, int font_nr, int align)
227 {
228   DrawText(ALIGNED_XPOS(x, getTextWidth(text, font_nr), align),
229            y, text, font_nr);
230 }
231
232 void DrawText(int x, int y, char *text, int font_nr)
233 {
234   int mask_mode = BLIT_OPAQUE;
235
236   if (DrawingOnBackground(x, y))
237     mask_mode = BLIT_ON_BACKGROUND;
238
239   DrawTextExt(drawto, x, y, text, font_nr, mask_mode);
240
241   if (IN_GFX_FIELD_FULL(x, y))
242     redraw_mask |= REDRAW_FIELD;
243   else if (IN_GFX_DOOR_1(x, y))
244     redraw_mask |= REDRAW_DOOR_1;
245   else if (IN_GFX_DOOR_2(x, y))
246     redraw_mask |= REDRAW_DOOR_2;
247   else if (IN_GFX_DOOR_3(x, y))
248     redraw_mask |= REDRAW_DOOR_3;
249   else
250     redraw_mask |= REDRAW_ALL;
251 }
252
253 void DrawTextExt(DrawBuffer *dst_bitmap, int dst_x, int dst_y, char *text,
254                  int font_nr, int mask_mode)
255 {
256   struct FontBitmapInfo *font = getFontBitmapInfo(font_nr);
257   int font_width = getFontWidth(font_nr);
258   int font_height = getFontHeight(font_nr);
259   Bitmap *src_bitmap;
260   int src_x, src_y;
261   char *text_ptr = text;
262
263   if (font->bitmap == NULL)
264     return;
265
266   // skip text to be printed outside the window (left/right will be clipped)
267   if (dst_y < 0 || dst_y + font_height > video.height)
268     return;
269
270   // add offset for drawing font characters
271   dst_x += font->draw_xoffset;
272   dst_y += font->draw_yoffset;
273
274   while (*text_ptr)
275   {
276     char c = *text_ptr++;
277
278     if (c == '\n')
279       c = ' ';          // print space instead of newline
280
281     getFontCharSource(font_nr, c, &src_bitmap, &src_x, &src_y);
282
283     // clip text at the left side of the window
284     if (dst_x < 0)
285     {
286       dst_x += font_width;
287
288       continue;
289     }
290
291     // clip text at the right side of the window
292     if (dst_x + font_width > video.width)
293       break;
294
295     if (mask_mode == BLIT_INVERSE)      // special mode for text gadgets
296     {
297       // first step: draw solid colored rectangle (use "cursor" character)
298       if (strlen(text) == 1)    // only one char inverted => draw cursor
299       {
300         Bitmap *cursor_bitmap;
301         int cursor_x, cursor_y;
302
303         getFontCharSource(font_nr, FONT_ASCII_CURSOR, &cursor_bitmap,
304                           &cursor_x, &cursor_y);
305
306         BlitBitmap(cursor_bitmap, dst_bitmap, cursor_x, cursor_y,
307                    font_width, font_height, dst_x, dst_y);
308       }
309
310       // second step: draw masked inverted character
311       SDLCopyInverseMasked(src_bitmap, dst_bitmap, src_x, src_y,
312                            font_width, font_height, dst_x, dst_y);
313     }
314     else if (mask_mode == BLIT_MASKED || mask_mode == BLIT_ON_BACKGROUND)
315     {
316       if (mask_mode == BLIT_ON_BACKGROUND)
317       {
318         // clear font character background
319         ClearRectangleOnBackground(dst_bitmap, dst_x, dst_y,
320                                    font_width, font_height);
321       }
322
323       BlitBitmapMasked(src_bitmap, dst_bitmap, src_x, src_y,
324                        font_width, font_height, dst_x, dst_y);
325     }
326     else        // normal, non-masked font blitting
327     {
328       BlitBitmap(src_bitmap, dst_bitmap, src_x, src_y,
329                  font_width, font_height, dst_x, dst_y);
330     }
331
332     dst_x += font_width;
333   }
334 }
335
336
337 // ============================================================================
338 // text buffer drawing functions
339 // ============================================================================
340
341 #define MAX_LINES_FROM_FILE             1024
342
343 char *GetTextBufferFromFile(char *filename, int max_lines)
344 {
345   File *file;
346   char *buffer;
347   int num_lines = 0;
348
349   if (filename == NULL)
350     return NULL;
351
352   if (!(file = openFile(filename, MODE_READ)))
353     return NULL;
354
355   buffer = checked_calloc(1);   // start with valid, but empty text buffer
356
357   while (!checkEndOfFile(file) && num_lines < max_lines)
358   {
359     char line[MAX_LINE_LEN];
360
361     // read next line of input file
362     if (!getStringFromFile(file, line, MAX_LINE_LEN))
363       break;
364
365     buffer = checked_realloc(buffer, strlen(buffer) + strlen(line) + 1);
366
367     strcat(buffer, line);
368
369     num_lines++;
370   }
371
372   closeFile(file);
373
374   if (getTextEncoding(buffer) == TEXT_ENCODING_UTF_8)
375   {
376     char *body_latin1 = getLatin1FromUTF8(buffer);
377
378     checked_free(buffer);
379
380     buffer = body_latin1;
381   }
382
383   return buffer;
384 }
385
386 static boolean RenderLineToBuffer(char **src_buffer_ptr, char *dst_buffer,
387                                   int *dst_buffer_len, int line_length,
388                                   boolean last_line_was_empty)
389 {
390   char *text_ptr = *src_buffer_ptr;
391   char *buffer = dst_buffer;
392   int buffer_len = *dst_buffer_len;
393   boolean buffer_filled = FALSE;
394
395   while (*text_ptr)
396   {
397     char *word_ptr;
398     int word_len;
399
400     // skip leading whitespaces
401     while (*text_ptr == ' ' || *text_ptr == '\t')
402       text_ptr++;
403
404     word_ptr = text_ptr;
405     word_len = 0;
406
407     // look for end of next word
408     while (*word_ptr != ' ' && *word_ptr != '\t' && *word_ptr != '\0')
409     {
410       word_ptr++;
411       word_len++;
412     }
413
414     if (word_len == 0)
415     {
416       continue;
417     }
418     else if (*text_ptr == '\n')         // special case: force empty line
419     {
420       if (buffer_len == 0)
421         text_ptr++;
422
423       // prevent printing of multiple empty lines
424       if (buffer_len > 0 || !last_line_was_empty)
425         buffer_filled = TRUE;
426     }
427     else if (word_len < line_length - buffer_len)
428     {
429       // word fits into text buffer -- add word
430
431       if (buffer_len > 0)
432         buffer[buffer_len++] = ' ';
433
434       strncpy(&buffer[buffer_len], text_ptr, word_len);
435       buffer_len += word_len;
436       buffer[buffer_len] = '\0';
437       text_ptr += word_len;
438     }
439     else if (buffer_len > 0)
440     {
441       // not enough space left for word in text buffer -- print buffer
442
443       buffer_filled = TRUE;
444     }
445     else
446     {
447       // word does not fit at all into empty text buffer -- cut word
448
449       strncpy(buffer, text_ptr, line_length);
450       buffer[line_length] = '\0';
451       text_ptr += line_length;
452       buffer_filled = TRUE;
453     }
454
455     if (buffer_filled)
456       break;
457   }
458
459   *src_buffer_ptr = text_ptr;
460   *dst_buffer_len = buffer_len;
461
462   return buffer_filled;
463 }
464
465 static boolean getCheckedTokenValueFromString(char *string, char **token,
466                                               char **value)
467 {
468   char *ptr;
469
470   if (!getTokenValueFromString(string, token, value))
471     return FALSE;
472
473   if (**token != '.')                   // token should begin with dot
474     return FALSE;
475
476   for (ptr = *token; *ptr; ptr++)       // token should contain no whitespace
477     if (*ptr == ' ' || *ptr == '\t')
478       return FALSE;
479
480   for (ptr = *value; *ptr; ptr++)       // value should contain no whitespace
481     if (*ptr == ' ' || *ptr == '\t')
482       return FALSE;
483
484   return TRUE;
485 }
486
487 static void DrawTextBuffer_Flush(int x, int y, char *buffer, int font_nr,
488                                  int line_length, int cut_length,
489                                  int mask_mode, boolean centered,
490                                  int current_ypos)
491 {
492   int buffer_len = strlen(buffer);
493   int font_width = getFontWidth(font_nr);
494   int offset_chars = (centered ? (line_length - buffer_len) / 2 : 0);
495   int offset_xsize =
496     (centered ? font_width * (line_length - buffer_len) / 2 : 0);
497   int final_cut_length = MAX(0, cut_length - offset_chars);
498   int xx = x + offset_xsize;
499   int yy = y + current_ypos;
500
501   buffer[final_cut_length] = '\0';
502
503   if (mask_mode != -1)
504     DrawTextExt(drawto, xx, yy, buffer, font_nr, mask_mode);
505   else
506     DrawText(xx, yy, buffer, font_nr);
507 }
508
509 static int DrawTextBufferExt(int x, int y, char *text_buffer, int font_nr,
510                              int line_length, int cut_length, int max_lines,
511                              int line_spacing, int mask_mode, boolean autowrap,
512                              boolean centered, boolean parse_comments,
513                              boolean is_text_area)
514 {
515   char buffer[line_length + 1];
516   int buffer_len;
517   int font_height = getFontHeight(font_nr);
518   int line_height = font_height + line_spacing;
519   int current_line = 0;
520   int current_ypos = 0;
521   int max_ysize = max_lines * line_height;
522
523   if (text_buffer == NULL || *text_buffer == '\0')
524     return 0;
525
526   if (current_line >= max_lines)
527     return 0;
528
529   if (cut_length == -1)
530     cut_length = line_length;
531
532   buffer[0] = '\0';
533   buffer_len = 0;
534
535   while (*text_buffer && current_ypos < max_ysize)
536   {
537     char line[MAX_LINE_LEN + 1];
538     char *line_ptr;
539     boolean last_line_was_empty = TRUE;
540     int num_line_chars = MAX_LINE_LEN;
541     int i;
542
543     // copy next line from text buffer to line buffer (nearly fgets() style)
544     for (i = 0; i < num_line_chars && *text_buffer; i++)
545     {
546       if ((line[i] = *text_buffer++) == '\n')
547       {
548         // in text areas, 'line_length' sized lines cause additional empty line
549         if (i == line_length && is_text_area)
550           text_buffer--;
551
552         break;
553       }
554     }
555     line[i] = '\0';
556
557     // prevent 'num_line_chars' sized lines to cause additional empty line
558     if (i == num_line_chars && *text_buffer == '\n')
559       text_buffer++;
560
561     // skip comments (lines directly beginning with '#')
562     if (line[0] == '#' && parse_comments)
563     {
564       char *token, *value;
565
566       // try to read generic token/value pair definition after comment sign
567       if (getCheckedTokenValueFromString(line + 1, &token, &value))
568       {
569         // if found, flush the current buffer, if non-empty
570         if (buffer_len > 0 && current_ypos < max_ysize)
571         {
572           DrawTextBuffer_Flush(x, y, buffer, font_nr, line_length, cut_length,
573                                mask_mode, centered, current_ypos);
574           current_ypos += line_height;
575           current_line++;
576
577           buffer[0] = '\0';
578           buffer_len = 0;
579         }
580
581         if (strEqual(token, ".font"))
582           font_nr = gfx.get_font_from_token_function(value);
583         else if (strEqual(token, ".autowrap"))
584           autowrap = get_boolean_from_string(value);
585         else if (strEqual(token, ".centered"))
586           centered = get_boolean_from_string(value);
587         else if (strEqual(token, ".parse_comments"))
588           parse_comments = get_boolean_from_string(value);
589
590         // if font has changed, depending values need to be updated as well
591         font_height = getFontHeight(font_nr);
592         line_height = font_height + line_spacing;
593       }
594
595       continue;
596     }
597
598     // cut trailing newline and carriage return from input line
599     for (line_ptr = line; *line_ptr; line_ptr++)
600     {
601       if (*line_ptr == '\n' || *line_ptr == '\r')
602       {
603         *line_ptr = '\0';
604         break;
605       }
606     }
607
608     if (strlen(line) == 0)              // special case: force empty line
609       strcpy(line, "\n");
610
611     line_ptr = line;
612
613     while (*line_ptr && current_ypos < max_ysize)
614     {
615       boolean buffer_filled;
616
617       if (autowrap)
618       {
619         buffer_filled = RenderLineToBuffer(&line_ptr, buffer, &buffer_len,
620                                            line_length, last_line_was_empty);
621       }
622       else
623       {
624         if (strlen(line_ptr) <= line_length)
625         {
626           buffer_len = strlen(line_ptr);
627           strcpy(buffer, line_ptr);
628         }
629         else
630         {
631           buffer_len = line_length;
632           strncpy(buffer, line_ptr, line_length);
633         }
634
635         buffer[buffer_len] = '\0';
636         line_ptr += buffer_len;
637
638         buffer_filled = TRUE;
639       }
640
641       if (buffer_filled)
642       {
643         DrawTextBuffer_Flush(x, y, buffer, font_nr, line_length, cut_length,
644                              mask_mode, centered, current_ypos);
645         current_ypos += line_height;
646         current_line++;
647
648         last_line_was_empty = (buffer_len == 0);
649
650         buffer[0] = '\0';
651         buffer_len = 0;
652       }
653     }
654   }
655
656   if (buffer_len > 0 && current_ypos < max_ysize)
657   {
658     DrawTextBuffer_Flush(x, y, buffer, font_nr, line_length, cut_length,
659                          mask_mode, centered, current_ypos);
660     current_ypos += line_height;
661     current_line++;
662   }
663
664   return current_line;
665 }
666
667 int DrawTextArea(int x, int y, char *text_buffer, int font_nr,
668                  int line_length, int cut_length, int max_lines,
669                  int line_spacing, int mask_mode, boolean autowrap,
670                  boolean centered, boolean parse_comments)
671 {
672   return DrawTextBufferExt(x, y, text_buffer, font_nr,
673                            line_length, cut_length, max_lines,
674                            line_spacing, mask_mode, autowrap,
675                            centered, parse_comments, TRUE);
676 }
677
678 int DrawTextBuffer(int x, int y, char *text_buffer, int font_nr,
679                    int line_length, int cut_length, int max_lines,
680                    int line_spacing, int mask_mode, boolean autowrap,
681                    boolean centered, boolean parse_comments)
682 {
683   return DrawTextBufferExt(x, y, text_buffer, font_nr,
684                            line_length, cut_length, max_lines,
685                            line_spacing, mask_mode, autowrap,
686                            centered, parse_comments, FALSE);
687 }
688
689 int DrawTextBufferS(int x, int y, char *text_buffer, int font_nr,
690                     int line_length, int cut_length, int max_lines,
691                     int line_spacing, int mask_mode, boolean autowrap,
692                     boolean centered, boolean parse_comments)
693 {
694   return DrawTextBuffer(gfx.sx + x, gfx.sy + y, text_buffer, font_nr,
695                         line_length, cut_length, max_lines,
696                         line_spacing, mask_mode, autowrap,
697                         centered, parse_comments);
698 }
699
700 int DrawTextBufferVA(int x, int y, char *format, va_list ap, int font_nr,
701                      int line_length, int cut_length, int max_lines,
702                      int line_spacing, int mask_mode, boolean autowrap,
703                      boolean centered, boolean parse_comments)
704 {
705   char text_buffer[MAX_OUTPUT_LINESIZE];
706   int text_length = vsnprintf(text_buffer, MAX_OUTPUT_LINESIZE, format, ap);
707
708   if (text_length >= MAX_OUTPUT_LINESIZE)
709     Warn("string too long in DrawTextBufferVA() -- truncated");
710
711   int num_lines_printed = DrawTextBuffer(x, y, text_buffer, font_nr,
712                                          line_length, cut_length, max_lines,
713                                          line_spacing, mask_mode, autowrap,
714                                          centered, parse_comments);
715   return num_lines_printed;
716 }
717
718 int DrawTextFile(int x, int y, char *filename, int font_nr,
719                  int line_length, int cut_length, int max_lines,
720                  int line_spacing, int mask_mode, boolean autowrap,
721                  boolean centered, boolean parse_comments)
722 {
723   char *text_buffer = GetTextBufferFromFile(filename, MAX_LINES_FROM_FILE);
724   int num_lines_printed = DrawTextBuffer(x, y, text_buffer, font_nr,
725                                          line_length, cut_length, max_lines,
726                                          line_spacing, mask_mode, autowrap,
727                                          centered, parse_comments);
728   checked_free(text_buffer);
729
730   return num_lines_printed;
731 }