big dialogs: set_curosr2 -> set_dlg_cursor.
[elinks.git] / src / bfu / text.c
blob409b1bb3a22864f5034584338d254b8cadcf57bd
1 /* Text widget implementation. */
3 #ifdef HAVE_CONFIG_H
4 #include "config.h"
5 #endif
7 #include <ctype.h>
8 #include <stdlib.h>
9 #include <string.h>
11 #include "elinks.h"
13 #include "bfu/dialog.h"
14 #include "bfu/text.h"
15 #include "config/kbdbind.h"
16 #include "intl/gettext/libintl.h"
17 #include "terminal/draw.h"
18 #include "terminal/mouse.h"
19 #include "terminal/terminal.h"
20 #include "util/color.h"
22 /* FIXME: For UTF-8 strings we need better function than isspace. */
23 #define is_unsplitable(pos) (*(pos) && *(pos) != '\n' && !isspace(*(pos)))
25 void
26 add_dlg_text(struct dialog *dlg, unsigned char *text,
27 enum format_align align, int bottom_pad)
29 struct widget *widget = &dlg->widgets[dlg->number_of_widgets++];
31 widget->type = WIDGET_TEXT;
32 widget->text = text;
34 widget->info.text.align = align;
35 widget->info.text.is_label = !!bottom_pad;
38 /* Returns length of substring (from start of @text) before a split. */
39 #ifdef CONFIG_UTF8
40 static inline int
41 split_line(unsigned char *text, int max_width, int *cells, int utf8)
42 #else
43 static inline int
44 split_line(unsigned char *text, int max_width, int *cells)
45 #endif /* CONFIG_UTF8 */
47 unsigned char *split = text;
48 #ifdef CONFIG_UTF8
49 unsigned char *text_end = split + strlen(split);
50 #endif /* CONFIG_UTF8 */
51 int cells_save = *cells;
53 if (max_width <= 0) return 0;
55 while (*split && *split != '\n') {
56 unsigned char *next_split;
58 #ifdef CONFIG_UTF8
59 if (utf8) {
60 unsigned char *next_char_begin = split
61 + utf8charlen(split);
63 next_split = split;
65 *cells += utf8_char2cells(split, text_end);
66 while (*next_split && next_split != next_char_begin)
67 next_split++;
69 next_char_begin = next_split;
70 while (is_unsplitable(next_split))
72 if (next_split < next_char_begin) {
73 next_split++;
74 continue;
76 *cells += utf8_char2cells(next_split, text_end);
77 next_char_begin += utf8charlen(next_split);
79 } else
80 #endif /* CONFIG_UTF8 */
82 next_split = split + 1;
84 while (is_unsplitable(next_split))
85 next_split++;
86 *cells = next_split - text;
89 if (*cells > max_width) {
90 /* Force a split if no position was found yet,
91 * meaning there's no splittable substring under
92 * requested width. */
93 if (split == text) {
94 #ifdef CONFIG_UTF8
95 if (utf8) {
96 int m_bytes = utf8_cells2bytes(text,
97 max_width,
98 NULL);
99 split = &text[m_bytes];
100 cells_save = utf8_ptr2cells(text,
101 split);
102 } else
103 #endif /* CONFIG_UTF8 */
105 split = &text[max_width];
106 cells_save = max_width;
109 /* FIXME: Function ispunct won't work correctly
110 * with UTF-8 characters. We need some similar
111 * function for UTF-8 characters. */
112 #ifdef CONFIG_UTF8
113 if (!utf8)
114 #endif /* CONFIG_UTF8 */
116 /* Give preference to split on a
117 * punctuation if any. Note that most
118 * of the time punctuation char is
119 * followed by a space so this rule
120 * will not match often. We match dash
121 * and quotes too. */
122 while (--split != text) {
123 cells_save--;
124 if (!ispunct(*split)) continue;
125 split++;
126 cells_save++;
127 break;
131 /* If no way to do a clean split, just return
132 * requested maximal width. */
133 if (split == text) {
134 *cells = max_width;
135 return max_width;
138 break;
141 cells_save = *cells;
142 split = next_split;
145 *cells = cells_save;
146 return split - text;
149 #undef is_unsplitable
151 #define LINES_GRANULARITY 0x7
152 #define realloc_lines(x, o, n) mem_align_alloc(x, o, n, LINES_GRANULARITY)
154 /* Find the start of each line with the current max width */
155 #ifdef CONFIG_UTF8
156 static unsigned char **
157 split_lines(struct widget_data *widget_data, int max_width, int utf8)
158 #else
159 static unsigned char **
160 split_lines(struct widget_data *widget_data, int max_width)
161 #endif /* CONFIG_UTF8 */
163 unsigned char *text = widget_data->widget->text;
164 unsigned char **lines = (unsigned char **) widget_data->cdata;
165 int line = 0;
167 if (widget_data->info.text.max_width == max_width) return lines;
169 /* We want to recalculate the max line width */
170 widget_data->box.width = 0;
172 while (*text) {
173 int width;
174 int cells = 0;
176 /* Skip first leading \n or space. */
177 if (isspace(*text)) text++;
178 if (!*text) break;
180 #ifdef CONFIG_UTF8
181 width = split_line(text, max_width, &cells, utf8);
182 #else
183 width = split_line(text, max_width, &cells);
184 #endif
186 /* split_line() may return 0. */
187 if (width < 1) {
188 width = 1; /* Infinite loop prevention. */
190 if (cells < 1) {
191 cells = 1; /* Infinite loop prevention. */
194 int_lower_bound(&widget_data->box.width, cells);
196 if (!realloc_lines(&lines, line, line + 1))
197 break;
199 lines[line++] = text;
200 text += width;
203 /* Yes it might be a bit ugly on the other hand it will be autofreed
204 * for us. */
205 widget_data->cdata = (unsigned char *) lines;
206 widget_data->info.text.lines = line;
207 widget_data->info.text.max_width = max_width;
209 return lines;
212 /* Format text according to dialog box and alignment. */
213 void
214 dlg_format_text_do(struct dialog_data *dlg_data,
215 unsigned char *text,
216 int x, int *y, int width, int *real_width,
217 struct color_pair *color, enum format_align align,
218 int format_only)
220 struct terminal *term = dlg_data->win->term;
221 int line_width;
222 int firstline = 1;
224 for (; *text; text += line_width, (*y)++) {
225 int shift;
226 int cells = 0;
228 /* Skip first leading \n or space. */
229 if (!firstline && isspace(*text))
230 text++;
231 else
232 firstline = 0;
233 if (!*text) break;
235 #ifdef CONFIG_UTF8
236 line_width = split_line(text, width, &cells, term->utf8_cp);
237 #else
238 line_width = split_line(text, width, &cells);
239 #endif /* CONFIG_UTF8 */
241 /* split_line() may return 0. */
242 if (line_width < 1) {
243 line_width = 1; /* Infinite loop prevention. */
244 continue;
247 if (real_width) int_lower_bound(real_width, cells);
248 if (format_only || !line_width) continue;
250 /* Calculate the number of chars to indent */
251 if (align == ALIGN_CENTER)
252 shift = (width - cells) / 2;
253 else if (align == ALIGN_RIGHT)
254 shift = width - cells;
255 else
256 shift = 0;
258 assert(cells <= width && shift < width);
260 draw_dlg_text(term, dlg_data, x + shift, *y, text, line_width, 0, color);
264 void
265 dlg_format_text(struct dialog_data *dlg_data,
266 struct widget_data *widget_data,
267 int x, int *y, int width, int *real_width, int max_height,
268 int format_only)
270 struct terminal *term = dlg_data->win->term;
271 unsigned char *text = widget_data->widget->text;
272 unsigned char saved = 0;
273 unsigned char *saved_pos = NULL;
274 int height;
276 height = int_max(0, max_height - 3);
278 /* If we are drawing set up the box before setting up the
279 * scrolling. */
280 set_box(&widget_data->box, x, *y,
281 widget_data->box.width, height);
282 if (height == 0) return;
284 /* Can we scroll and do we even have to? */
285 if (widget_data->widget->info.text.is_scrollable
286 && (widget_data->info.text.max_width != width
287 || height < widget_data->info.text.lines))
289 unsigned char **lines;
290 int current;
291 int visible;
293 /* Ensure that the current split is valid but don't
294 * split if we don't have to */
295 #ifdef CONFIG_UTF8
296 if (widget_data->box.width != width
297 && !split_lines(widget_data, width, term->utf8_cp))
298 return;
299 #else
300 if (widget_data->box.width != width
301 && !split_lines(widget_data, width))
302 return;
303 #endif
305 lines = (unsigned char **) widget_data->cdata;
307 /* Make maximum number of lines available */
308 visible = int_max(widget_data->info.text.lines - height,
309 height);
311 int_bounds(&widget_data->info.text.current, 0, visible);
312 current = widget_data->info.text.current;
314 /* Set the current position */
315 text = lines[current];
317 /* Do we have to force a text end ? */
318 visible = widget_data->info.text.lines - current;
319 if (visible > height) {
320 int lines_pos = current + height;
322 saved_pos = lines[lines_pos];
324 /* We save the start of lines so backtrack to see
325 * if the previous line has a line end that should
326 * also be trimmed. */
327 if (lines_pos > 0 && saved_pos[-1] == '\n')
328 saved_pos--;
330 saved = *saved_pos;
331 *saved_pos = '\0';
334 /* Force dialog to be the width of the longest line */
335 if (real_width) int_lower_bound(real_width, widget_data->box.width);
337 } else {
338 /* Always reset @current if we do not need to scroll */
339 widget_data->info.text.current = 0;
342 dlg_format_text_do(dlg_data, text,
343 x, y, width, real_width,
344 get_bfu_color(term, "dialog.text"),
345 widget_data->widget->info.text.align, format_only);
347 if (widget_data->widget->info.text.is_label) (*y)--;
349 /* If we scrolled and something was trimmed restore it */
350 if (saved && saved_pos) *saved_pos = saved;
353 static widget_handler_status_T
354 display_text(struct dialog_data *dlg_data, struct widget_data *widget_data)
356 struct window *win = dlg_data->win;
357 struct box box;
358 int scale, current, step;
359 int lines = widget_data->info.text.lines;
361 set_box(&box,
362 dlg_data->box.x + dlg_data->box.width - DIALOG_LEFT_BORDER - 1,
363 widget_data->box.y,
365 widget_data->box.height);
367 if (!text_is_scrollable(widget_data) || box.height <= 0)
368 return EVENT_PROCESSED;
370 draw_box(win->term, &box, ' ', 0,
371 get_bfu_color(win->term, "dialog.scrollbar"));
373 current = widget_data->info.text.current;
374 scale = (box.height + 1) * 100 / lines;
376 /* Scale the offset of @current */
377 step = (current + 1) * scale / 100;
378 int_bounds(&step, 0, widget_data->box.height - 1);
380 /* Scale the number of visible lines */
381 box.height = (box.height + 1) * scale / 100;
382 int_bounds(&box.height, 1, int_max(widget_data->box.height - step, 1));
384 /* Ensure we always step to the last position too */
385 if (lines - widget_data->box.height == current) {
386 step = widget_data->box.height - box.height;
388 box.y += step;
390 #ifdef CONFIG_MOUSE
391 /* Save infos about selected scrollbar position and size.
392 * We'll use it when handling mouse. */
393 widget_data->info.text.scroller_height = box.height;
394 widget_data->info.text.scroller_y = box.y;
395 #endif
397 draw_box(win->term, &box, ' ', 0,
398 get_bfu_color(win->term, "dialog.scrollbar-selected"));
400 /* Hope this is at least a bit reasonable. Set cursor
401 * and window pointer to start of the first text line. */
402 set_dlg_cursor(win->term, dlg_data, widget_data->box.x, widget_data->box.y, 1);
403 set_dlg_window_ptr(dlg_data, win, widget_data->box.x, widget_data->box.y);
405 return EVENT_PROCESSED;
408 static void
409 format_and_display_text(struct widget_data *widget_data,
410 struct dialog_data *dlg_data,
411 int current)
413 struct terminal *term = dlg_data->win->term;
414 int y = widget_data->box.y;
415 int height = dialog_max_height(term);
416 int lines = widget_data->info.text.lines;
418 assert(lines >= 0);
419 assert(widget_data->box.height >= 0);
421 int_bounds(&current, 0, lines - widget_data->box.height);
423 if (widget_data->info.text.current == current) return;
425 widget_data->info.text.current = current;
427 draw_box(term, &widget_data->box, ' ', 0,
428 get_bfu_color(term, "dialog.generic"));
430 dlg_format_text(dlg_data, widget_data,
431 widget_data->box.x, &y, widget_data->box.width, NULL,
432 height, 0);
434 display_text(dlg_data, widget_data);
435 redraw_from_window(dlg_data->win);
438 static widget_handler_status_T
439 kbd_text(struct dialog_data *dlg_data, struct widget_data *widget_data)
441 int current = widget_data->info.text.current;
442 struct term_event *ev = dlg_data->term_event;
444 switch (kbd_action(KEYMAP_MENU, ev, NULL)) {
445 case ACT_MENU_UP:
446 current--;
447 break;
449 case ACT_MENU_DOWN:
450 current++;
451 break;
453 case ACT_MENU_PAGE_UP:
454 current -= widget_data->box.height;
455 break;
457 case ACT_MENU_PAGE_DOWN:
458 current += widget_data->box.height;
459 break;
461 case ACT_MENU_HOME:
462 current = 0;
463 break;
465 case ACT_MENU_END:
466 current = widget_data->info.text.lines;
467 break;
469 default:
470 return EVENT_NOT_PROCESSED;
473 format_and_display_text(widget_data, dlg_data, current);
475 return EVENT_PROCESSED;
478 static widget_handler_status_T
479 mouse_text(struct dialog_data *dlg_data, struct widget_data *widget_data)
481 #ifdef CONFIG_MOUSE
482 int border = DIALOG_LEFT_BORDER + DIALOG_LEFT_INNER_BORDER;
483 int current = widget_data->info.text.current;
484 int scroller_y = widget_data->info.text.scroller_y;
485 int scroller_height = widget_data->info.text.scroller_height;
486 int scroller_middle = scroller_y + scroller_height/2
487 - widget_data->info.text.scroller_last_dir;
488 struct box scroller_box;
489 struct term_event *ev = dlg_data->term_event;
491 set_box(&scroller_box,
492 dlg_data->box.x + dlg_data->box.width - 1 - border,
493 widget_data->box.y,
494 DIALOG_LEFT_INNER_BORDER * 2 + 1,
495 widget_data->box.height);
497 /* One can scroll by clicking or rolling the wheel on the scrollbar
498 * or one or two cells to its left or its right. */
499 if (!check_mouse_position(ev, &scroller_box))
500 return EVENT_NOT_PROCESSED;
502 switch (get_mouse_button(ev)) {
503 case B_LEFT:
504 /* Left click scrolls up or down by one step. */
505 if (ev->info.mouse.y <= scroller_middle)
506 current--;
507 else
508 current++;
509 break;
511 case B_RIGHT:
512 /* Right click scrolls up or down by more than one step.
513 * Faster. */
514 if (ev->info.mouse.y <= scroller_middle)
515 current -= 5;
516 else
517 current += 5;
518 break;
520 case B_WHEEL_UP:
521 /* Mouse wheel up scrolls up. */
522 current--;
523 break;
525 case B_WHEEL_DOWN:
526 /* Mouse wheel up scrolls down. */
527 current++;
528 break;
530 default:
531 return EVENT_NOT_PROCESSED;
534 /* Save last direction used. */
535 if (widget_data->info.text.current < current)
536 widget_data->info.text.scroller_last_dir = 1;
537 else
538 widget_data->info.text.scroller_last_dir = -1;
540 format_and_display_text(widget_data, dlg_data, current);
542 #endif /* CONFIG_MOUSE */
544 return EVENT_PROCESSED;
548 const struct widget_ops text_ops = {
549 display_text,
550 NULL,
551 mouse_text,
552 kbd_text,
553 NULL,
554 NULL,