version 2.8
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.org/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(uint16_t 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  int16_t word; /* Length of the word in pixels. */
24  int16_t space; /* Length of the whitespace in pixels. */
25  uint16_t 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;
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  c = mf_getchar(text);
46  }
47 
48  prev = *text;
49  while (c && is_wrap_space(c))
50  {
51  result->chars++;
52 
53  if (c == ' ')
54  result->space += mf_character_width(font, c);
55  else if (c == '-')
56  result->space += mf_character_width(font, '-');
57  else if (c == '\t')
58  result->space += mf_character_width(font, 'm') * MF_TABSIZE;
59  else if (c == '\n')
60  break;
61 
62  prev = *text;
63  c = mf_getchar(text);
64  }
65 
66  /* The last loop reads the first character of next word, put it back. */
67  if (c)
68  *text = prev;
69 
70  return (c == '\0' || c == '\n');
71 }
72 
73 /* Represents the rendered length for a single line. */
74 struct linelen_s
75 {
76  mf_str start; /* Start of the text for line. */
77  uint16_t chars; /* Total number of characters on the line. */
78  int16_t width; /* Total length of all words + whitespace on the line in pixels. */
79  bool linebreak; /* True if line ends in a linebreak */
80  struct wordlen_s last_word; /* Last word on the line. */
81  struct wordlen_s last_word_2; /* Second to last word on the line. */
82 };
83 
84 /* Append word onto the line if it fits. If it would overflow, don't add and
85  * return false. */
86 static bool append_word(const struct mf_font_s *font, int16_t width,
87  struct linelen_s *current, mf_str *text)
88 {
89  mf_str tmp = *text;
90  struct wordlen_s wordlen;
91  bool linebreak;
92 
93  linebreak = get_wordlen(font, &tmp, &wordlen);
94 
95  if (current->width + wordlen.word <= width)
96  {
97  *text = tmp;
98  current->last_word_2 = current->last_word;
99  current->last_word = wordlen;
100  current->linebreak = linebreak;
101  current->chars += wordlen.chars;
102  current->width += wordlen.word + wordlen.space;
103  return true;
104  }
105  else
106  {
107  return false;
108  }
109 }
110 
111 /* Append a character to the line if it fits. */
112 static bool append_char(const struct mf_font_s *font, int16_t width,
113  struct linelen_s *current, mf_str *text)
114 {
115  mf_str tmp = *text;
116  mf_char c;
117  uint16_t w;
118 
119  c = mf_getchar(&tmp);
120  w = mf_character_width(font, c);
121 
122  if (current->width + w <= width)
123  {
124  *text = tmp;
125  current->chars++;
126  current->width += w;
127  return true;
128  }
129  else
130  {
131  return false;
132  }
133 }
134 
135 static int32_t sq16(int16_t x) { return (int32_t)x * x; }
136 
137 /* Try to balance the lines by potentially moving one word from the previous
138  * line to the the current one. */
139 static void tune_lines(struct linelen_s *current, struct linelen_s *previous,
140  int16_t max_width)
141 {
142  int16_t curw1, prevw1;
143  int16_t curw2, prevw2;
144  int32_t delta1, delta2;
145 
146  /* If the lines are rendered as is */
147  curw1 = current->width - current->last_word.space;
148  prevw1 = previous->width - previous->last_word.space;
149  delta1 = sq16(max_width - prevw1) + sq16(max_width - curw1);
150 
151  /* If the last word is moved */
152  curw2 = current->width + previous->last_word.word;
153  prevw2 = previous->width - previous->last_word.word
154  - previous->last_word.space
155  - previous->last_word_2.space;
156  delta2 = sq16(max_width - prevw2) + sq16(max_width - curw2);
157 
158  if (delta1 > delta2 && curw2 <= max_width)
159  {
160  /* Do the change. */
161  uint16_t chars;
162 
163  chars = previous->last_word.chars;
164  previous->chars -= chars;
165  current->chars += chars;
166  previous->width -= previous->last_word.word + previous->last_word.space;
167  current->width += previous->last_word.word + previous->last_word.space;
168  previous->last_word = previous->last_word_2;
169 
170  while (chars--) mf_rewind(&current->start);
171  }
172 }
173 
174 void mf_wordwrap(const struct mf_font_s *font, int16_t width,
175  mf_str text, mf_line_callback_t callback, void *state)
176 {
177  struct linelen_s current = { 0 };
178  struct linelen_s previous = { 0 };
179  bool full;
180 
181  current.start = text;
182 
183  while (*text)
184  {
185  full = !append_word(font, width, &current, &text);
186 
187  if (full || current.linebreak)
188  {
189  if (!current.chars)
190  {
191  /* We have a very long word. We must just cut it off at some
192  * point. */
193  while (append_char(font, width, &current, &text));
194  }
195 
196  if (previous.chars)
197  {
198  /* Tune the length and dispatch the previous line. */
199  if (!previous.linebreak && !current.linebreak)
200  tune_lines(&current, &previous, width);
201 
202  if (!callback(previous.start, previous.chars, state))
203  return;
204  }
205 
206  previous = current;
207  current.start = text;
208  current.chars = 0;
209  current.width = 0;
210  current.linebreak = false;
211  current.last_word.word = 0;
212  current.last_word.space = 0;
213  current.last_word.chars = 0;
214  }
215  }
216 
217  /* Dispatch the last lines. */
218  if (previous.chars)
219  {
220  if (!callback(previous.start, previous.chars, state))
221  return;
222  }
223 
224  if (current.chars)
225  callback(current.start, current.chars, state);
226 }
227 
228 #else
229 
230 void mf_wordwrap(const struct mf_font_s *font, int16_t width,
231  mf_str text, mf_line_callback_t callback, void *state)
232 {
233  mf_str linestart;
234 
235  /* Current line width and character count */
236  int16_t lw_cur = 0, cc_cur = 0;
237 
238  /* Previous wrap point */
239  int16_t cc_prev;
240  mf_str ls_prev;
241 
242  linestart = text;
243 
244  while (*text)
245  {
246  cc_prev = 0;
247  ls_prev = text;
248 
249  while (*text)
250  {
251  mf_char c;
252  int16_t new_width;
253  mf_str tmp;
254 
255  tmp = text;
256  c = mf_getchar(&text);
257  new_width = lw_cur + mf_character_width(font, c);
258 
259  if (c == '\n')
260  {
261  cc_prev = cc_cur + 1;
262  ls_prev = text;
263  break;
264  }
265 
266  if (new_width > width)
267  {
268  text = tmp;
269  break;
270  }
271 
272  cc_cur++;
273  lw_cur = new_width;
274 
275  if (is_wrap_space(c))
276  {
277  cc_prev = cc_cur;
278  ls_prev = text;
279  }
280  }
281 
282  /* Handle unbreakable words */
283  if (cc_prev == 0)
284  {
285  cc_prev = cc_cur;
286  ls_prev = text;
287  }
288 
289  if (!callback(linestart, cc_prev, state))
290  return;
291 
292  linestart = ls_prev;
293  text = linestart;
294  lw_cur = 0;
295  cc_cur = 0;
296  }
297 }
298 
299 #endif
300 
301 #endif //MF_NO_COMPILE