µGFX  2.9
version 2.9
mf_wordwrap.c
1 /*
2  * This file is subject to the terms of the GFX License. If a copy of
3  * the license was not distributed with this file, you can obtain one at:
4  *
5  * http://ugfx.io/license.html
6  */
7 
8 #include "mf_wordwrap.h"
9 
10 #ifndef MF_NO_COMPILE
11 
12 /* Returns true if the line can be broken at this character. */
13 static bool is_wrap_space(gU16 c)
14 {
15  return c == ' ' || c == '\n' || c == '\t' || c == '\r' || c == '-';
16 }
17 
18 #if MF_USE_ADVANCED_WORDWRAP
19 
20 /* Represents a single word and the whitespace after it. */
21 struct wordlen_s
22 {
23  gI16 word; /* Length of the word in pixels. */
24  gI16 space; /* Length of the whitespace in pixels. */
25  gU16 chars; /* Number of characters in word + space, combined. */
26 };
27 
28 /* Take the next word from the string and compute its width.
29  * Returns true if the word ends in a linebreak. */
30 static bool get_wordlen(const struct mf_font_s *font, mf_str *text,
31  struct wordlen_s *result)
32 {
33  mf_char c;
34  mf_str prev = *text;
35 
36  result->word = 0;
37  result->space = 0;
38  result->chars = 0;
39 
40  c = mf_getchar(text);
41  while (c && !is_wrap_space(c))
42  {
43  result->chars++;
44  result->word += mf_character_width(font, c);
45 
46  prev = *text;
47  c = mf_getchar(text);
48  }
49 
50  while (c && is_wrap_space(c))
51  {
52  result->chars++;
53 
54  if (c == ' ' || c == '-')
55  result->space += mf_character_width(font, c);
56  else if (c == '\t')
57  result->space += mf_character_width(font, 'm') * MF_TABSIZE;
58  else if (c == '\n') {
59  /* Special case for newlines, skip the character then break. */
60  prev = *text;
61  break;
62  }
63 
64  prev = *text;
65  c = mf_getchar(text);
66  }
67 
68  /* The last loop reads the first character of next word, put it back. */
69  if (c)
70  *text = prev;
71 
72  return (c == '\0' || c == '\n');
73 }
74 
75 /* Represents the rendered length for a single line. */
76 struct linelen_s
77 {
78  mf_str start; /* Start of the text for line. */
79  gU16 chars; /* Total number of characters on the line. */
80  gI16 width; /* Total length of all words + whitespace on the line in pixels. */
81  bool linebreak; /* True if line ends in a linebreak */
82  struct wordlen_s last_word; /* Last word on the line. */
83  struct wordlen_s last_word_2; /* Second to last word on the line. */
84 };
85 
86 /* Append word onto the line if it fits. If it would overflow, don't add and
87  * return false. */
88 static bool append_word(const struct mf_font_s *font, gI16 width,
89  struct linelen_s *current, mf_str *text)
90 {
91  mf_str tmp = *text;
92  struct wordlen_s wordlen;
93  bool linebreak;
94 
95  linebreak = get_wordlen(font, &tmp, &wordlen);
96 
97  if (current->width + wordlen.word <= width)
98  {
99  *text = tmp;
100  current->last_word_2 = current->last_word;
101  current->last_word = wordlen;
102  current->linebreak = linebreak;
103  current->chars += wordlen.chars;
104  current->width += wordlen.word + wordlen.space;
105  return true;
106  }
107  else
108  {
109  return false;
110  }
111 }
112 
113 /* Append a character to the line if it fits. */
114 static bool append_char(const struct mf_font_s *font, gI16 width,
115  struct linelen_s *current, mf_str *text)
116 {
117  mf_str tmp = *text;
118  mf_char c;
119  gU16 w;
120 
121  c = mf_getchar(&tmp);
122  w = mf_character_width(font, c);
123 
124  if (current->width + w <= width)
125  {
126  *text = tmp;
127  current->chars++;
128  current->width += w;
129  return true;
130  }
131  else
132  {
133  return false;
134  }
135 }
136 
137 static gI32 sq16(gI16 x) { return (gI32)x * x; }
138 
139 /* Try to balance the lines by potentially moving one word from the previous
140  * line to the the current one. */
141 static void tune_lines(struct linelen_s *current, struct linelen_s *previous,
142  gI16 max_width)
143 {
144  gI16 curw1, prevw1;
145  gI16 curw2, prevw2;
146  gI32 delta1, delta2;
147 
148  /* If the lines are rendered as is */
149  curw1 = current->width - current->last_word.space;
150  prevw1 = previous->width - previous->last_word.space;
151  delta1 = sq16(max_width - prevw1) + sq16(max_width - curw1);
152 
153  /* If the last word is moved */
154  curw2 = current->width + previous->last_word.word;
155  prevw2 = previous->width - previous->last_word.word
156  - previous->last_word.space
157  - previous->last_word_2.space;
158  delta2 = sq16(max_width - prevw2) + sq16(max_width - curw2);
159 
160  if (delta1 > delta2 && curw2 <= max_width)
161  {
162  /* Do the change. */
163  gU16 chars;
164 
165  chars = previous->last_word.chars;
166  previous->chars -= chars;
167  current->chars += chars;
168  previous->width -= previous->last_word.word + previous->last_word.space;
169  current->width += previous->last_word.word + previous->last_word.space;
170  previous->last_word = previous->last_word_2;
171 
172  while (chars--) mf_rewind(&current->start);
173  }
174 }
175 
176 void mf_wordwrap(const struct mf_font_s *font, gI16 width,
177  mf_str text, mf_line_callback_t callback, void *state)
178 {
179  struct linelen_s current = { 0 };
180  struct linelen_s previous = { 0 };
181  bool full;
182  gU32 giveUp = 2048; /* Do while-loop a maximum of x times */
183 
184  current.start = text;
185 
186  while (*text)
187  {
188  full = !append_word(font, width, &current, &text);
189 
190  if (full || current.linebreak)
191  {
192  if (!current.chars)
193  {
194  /* We have a very long word. We must just cut it off at some
195  * point. */
196  while (append_char(font, width, &current, &text));
197  }
198 
199  if (previous.chars)
200  {
201  /* Tune the length and dispatch the previous line. */
202  if (!previous.linebreak && !current.linebreak)
203  tune_lines(&current, &previous, width);
204 
205  if (!callback(previous.start, previous.chars, state))
206  return;
207  }
208 
209  previous = current;
210  current.start = text;
211  current.chars = 0;
212  current.width = 0;
213  current.linebreak = false;
214  current.last_word.word = 0;
215  current.last_word.space = 0;
216  current.last_word.chars = 0;
217  }
218 
219  giveUp--;
220  if (giveUp == 0)
221  {
222  break;
223  }
224  }
225 
226  /* Dispatch the last lines. */
227  if (previous.chars)
228  {
229  if (!callback(previous.start, previous.chars, state))
230  return;
231  }
232 
233  if (current.chars)
234  callback(current.start, current.chars, state);
235 }
236 
237 #else
238 
239 void mf_wordwrap(const struct mf_font_s *font, gI16 width,
240  mf_str text, mf_line_callback_t callback, void *state)
241 {
242  mf_str linestart;
243 
244  /* Current line width and character count */
245  gI16 lw_cur = 0, cc_cur = 0;
246 
247  /* Previous wrap point */
248  gI16 cc_prev;
249  mf_str ls_prev;
250 
251  linestart = text;
252 
253  while (*text)
254  {
255  cc_prev = 0;
256  ls_prev = text;
257 
258  while (*text)
259  {
260  mf_char c;
261  gI16 new_width;
262  mf_str tmp;
263 
264  tmp = text;
265  c = mf_getchar(&text);
266  new_width = lw_cur + mf_character_width(font, c);
267 
268  if (c == '\n')
269  {
270  cc_prev = cc_cur + 1;
271  ls_prev = text;
272  break;
273  }
274 
275  if (new_width > width)
276  {
277  text = tmp;
278  break;
279  }
280 
281  cc_cur++;
282  lw_cur = new_width;
283 
284  if (is_wrap_space(c))
285  {
286  cc_prev = cc_cur;
287  ls_prev = text;
288  }
289  }
290 
291  /* Handle unbreakable words */
292  if (cc_prev == 0)
293  {
294  cc_prev = cc_cur;
295  ls_prev = text;
296  }
297 
298  if (!callback(linestart, cc_prev, state))
299  return;
300 
301  linestart = ls_prev;
302  text = linestart;
303  lw_cur = 0;
304  cc_cur = 0;
305  }
306 }
307 
308 #endif
309 
310 #endif //MF_NO_COMPILE