elinks-0.11.0
[elinks/images.git] / src / bfu / text.c
blob63f00f4d9969e1e7c0406bf23787d323a60efa96
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 #define is_unsplitable(pos) (*(pos) && *(pos) != '\n' && !isspace(*(pos)))
24 void
25 add_dlg_text(struct dialog *dlg, unsigned char *text,
26 enum format_align align, int bottom_pad)
28 struct widget *widget = &dlg->widgets[dlg->number_of_widgets++];
30 widget->type = WIDGET_TEXT;
31 widget->text = text;
33 widget->info.text.align = align;
34 widget->info.text.is_label = !!bottom_pad;
37 /* Returns length of substring (from start of @text) before a split. */
38 static inline int
39 split_line(unsigned char *text, int max_width)
41 unsigned char *split = text;
43 if (max_width <= 0) return 0;
45 while (*split && *split != '\n') {
46 unsigned char *next_split = split + 1;
48 while (is_unsplitable(next_split))
49 next_split++;
51 if (next_split - text > max_width) {
52 /* Force a split if no position was found yet,
53 * meaning there's no splittable substring under
54 * requested width. */
55 if (split == text) {
56 split = &text[max_width];
58 /* Give preference to split on a punctuation
59 * if any. Note that most of the time
60 * punctuation char is followed by a space so
61 * this rule will not match often.
62 * We match dash and quotes too. */
63 while (--split != text) {
64 if (!ispunct(*split)) continue;
65 split++;
66 break;
69 /* If no way to do a clean split, just return
70 * requested maximal width. */
71 if (split == text)
72 return max_width;
74 break;
77 split = next_split;
80 return split - text;
83 #undef is_unsplitable
85 #define LINES_GRANULARITY 0x7
86 #define realloc_lines(x, o, n) mem_align_alloc(x, o, n, unsigned char *, LINES_GRANULARITY)
88 /* Find the start of each line with the current max width */
89 static unsigned char **
90 split_lines(struct widget_data *widget_data, int max_width)
92 unsigned char *text = widget_data->widget->text;
93 unsigned char **lines = (unsigned char **) widget_data->cdata;
94 int line = 0;
96 if (widget_data->info.text.max_width == max_width) return lines;
98 /* We want to recalculate the max line width */
99 widget_data->box.width = 0;
101 while (*text) {
102 int width;
104 /* Skip first leading \n or space. */
105 if (isspace(*text)) text++;
106 if (!*text) break;
108 width = split_line(text, max_width);
110 /* split_line() may return 0. */
111 if (width < 1) {
112 width = 1; /* Infinite loop prevention. */
115 int_lower_bound(&widget_data->box.width, width);
117 if (!realloc_lines(&lines, line, line + 1))
118 break;
120 lines[line++] = text;
121 text += width;
124 /* Yes it might be a bit ugly on the other hand it will be autofreed
125 * for us. */
126 widget_data->cdata = (unsigned char *) lines;
127 widget_data->info.text.lines = line;
128 widget_data->info.text.max_width = max_width;
130 return lines;
133 /* Format text according to dialog box and alignment. */
134 void
135 dlg_format_text_do(struct terminal *term, unsigned char *text,
136 int x, int *y, int width, int *real_width,
137 struct color_pair *color, enum format_align align)
139 int line_width;
140 int firstline = 1;
142 for (; *text; text += line_width, (*y)++) {
143 int shift;
145 /* Skip first leading \n or space. */
146 if (!firstline && isspace(*text))
147 text++;
148 else
149 firstline = 0;
150 if (!*text) break;
152 line_width = split_line(text, width);
154 /* split_line() may return 0. */
155 if (line_width < 1) {
156 line_width = 1; /* Infinite loop prevention. */
157 continue;
160 if (real_width) int_lower_bound(real_width, line_width);
161 if (!term || !line_width) continue;
163 /* Calculate the number of chars to indent */
164 if (align == ALIGN_CENTER)
165 shift = (width - line_width) / 2;
166 else if (align == ALIGN_RIGHT)
167 shift = width - line_width;
168 else
169 shift = 0;
171 assert(line_width <= width && shift < width);
173 draw_text(term, x + shift, *y, text, line_width, 0, color);
177 void
178 dlg_format_text(struct terminal *term, struct widget_data *widget_data,
179 int x, int *y, int width, int *real_width, int max_height)
181 unsigned char *text = widget_data->widget->text;
182 unsigned char saved = 0;
183 unsigned char *saved_pos = NULL;
184 int height;
186 height = int_max(0, max_height - 3);
188 /* If we are drawing set up the box before setting up the
189 * scrolling. */
190 set_box(&widget_data->box, x, *y,
191 widget_data->box.width, height);
192 if (height == 0) return;
194 /* Can we scroll and do we even have to? */
195 if (widget_data->widget->info.text.is_scrollable
196 && (widget_data->info.text.max_width != width
197 || height < widget_data->info.text.lines))
199 unsigned char **lines;
200 int current;
201 int visible;
203 /* Ensure that the current split is valid but don't
204 * split if we don't have to */
205 if (widget_data->box.width != width
206 && !split_lines(widget_data, width))
207 return;
209 lines = (unsigned char **) widget_data->cdata;
211 /* Make maximum number of lines available */
212 visible = int_max(widget_data->info.text.lines - height,
213 height);
215 int_bounds(&widget_data->info.text.current, 0, visible);
216 current = widget_data->info.text.current;
218 /* Set the current position */
219 text = lines[current];
221 /* Do we have to force a text end ? */
222 visible = widget_data->info.text.lines - current;
223 if (visible > height) {
224 int lines_pos = current + height;
226 saved_pos = lines[lines_pos];
228 /* We save the start of lines so backtrack to see
229 * if the previous line has a line end that should
230 * also be trimmed. */
231 if (lines_pos > 0 && saved_pos[-1] == '\n')
232 saved_pos--;
234 saved = *saved_pos;
235 *saved_pos = '\0';
238 /* Force dialog to be the width of the longest line */
239 if (real_width) int_lower_bound(real_width, widget_data->box.width);
241 } else {
242 /* Always reset @current if we do not need to scroll */
243 widget_data->info.text.current = 0;
246 dlg_format_text_do(term, text,
247 x, y, width, real_width,
248 get_bfu_color(term, "dialog.text"),
249 widget_data->widget->info.text.align);
251 if (widget_data->widget->info.text.is_label) (*y)--;
253 /* If we scrolled and something was trimmed restore it */
254 if (saved && saved_pos) *saved_pos = saved;
257 static widget_handler_status_T
258 display_text(struct dialog_data *dlg_data, struct widget_data *widget_data)
260 struct window *win = dlg_data->win;
261 struct box box;
262 int scale, current, step;
263 int lines = widget_data->info.text.lines;
265 set_box(&box,
266 dlg_data->box.x + dlg_data->box.width - DIALOG_LEFT_BORDER - 1,
267 widget_data->box.y,
269 widget_data->box.height);
271 if (!text_is_scrollable(widget_data) || box.height <= 0)
272 return EVENT_PROCESSED;
274 draw_box(win->term, &box, ' ', 0,
275 get_bfu_color(win->term, "dialog.scrollbar"));
277 current = widget_data->info.text.current;
278 scale = (box.height + 1) * 100 / lines;
280 /* Scale the offset of @current */
281 step = (current + 1) * scale / 100;
282 int_bounds(&step, 0, widget_data->box.height - 1);
284 /* Scale the number of visible lines */
285 box.height = (box.height + 1) * scale / 100;
286 int_bounds(&box.height, 1, int_max(widget_data->box.height - step, 1));
288 /* Ensure we always step to the last position too */
289 if (lines - widget_data->box.height == current) {
290 step = widget_data->box.height - box.height;
292 box.y += step;
294 #ifdef CONFIG_MOUSE
295 /* Save infos about selected scrollbar position and size.
296 * We'll use it when handling mouse. */
297 widget_data->info.text.scroller_height = box.height;
298 widget_data->info.text.scroller_y = box.y;
299 #endif
301 draw_box(win->term, &box, ' ', 0,
302 get_bfu_color(win->term, "dialog.scrollbar-selected"));
304 /* Hope this is at least a bit reasonable. Set cursor
305 * and window pointer to start of the first text line. */
306 set_cursor(win->term, widget_data->box.x, widget_data->box.y, 1);
307 set_window_ptr(win, widget_data->box.x, widget_data->box.y);
309 return EVENT_PROCESSED;
312 static void
313 format_and_display_text(struct widget_data *widget_data,
314 struct dialog_data *dlg_data,
315 int current)
317 struct terminal *term = dlg_data->win->term;
318 int y = widget_data->box.y;
319 int height = dialog_max_height(term);
320 int lines = widget_data->info.text.lines;
322 assert(lines >= 0);
323 assert(widget_data->box.height >= 0);
325 int_bounds(&current, 0, lines - widget_data->box.height);
327 if (widget_data->info.text.current == current) return;
329 widget_data->info.text.current = current;
331 draw_box(term, &widget_data->box, ' ', 0,
332 get_bfu_color(term, "dialog.generic"));
334 dlg_format_text(term, widget_data,
335 widget_data->box.x, &y, widget_data->box.width, NULL,
336 height);
338 display_text(dlg_data, widget_data);
339 redraw_from_window(dlg_data->win);
342 static widget_handler_status_T
343 kbd_text(struct dialog_data *dlg_data, struct widget_data *widget_data)
345 int current = widget_data->info.text.current;
346 struct term_event *ev = dlg_data->term_event;
348 switch (kbd_action(KEYMAP_MENU, ev, NULL)) {
349 case ACT_MENU_UP:
350 current--;
351 break;
353 case ACT_MENU_DOWN:
354 current++;
355 break;
357 case ACT_MENU_PAGE_UP:
358 current -= widget_data->box.height;
359 break;
361 case ACT_MENU_PAGE_DOWN:
362 current += widget_data->box.height;
363 break;
365 case ACT_MENU_HOME:
366 current = 0;
367 break;
369 case ACT_MENU_END:
370 current = widget_data->info.text.lines;
371 break;
373 default:
374 return EVENT_NOT_PROCESSED;
377 format_and_display_text(widget_data, dlg_data, current);
379 return EVENT_PROCESSED;
382 static widget_handler_status_T
383 mouse_text(struct dialog_data *dlg_data, struct widget_data *widget_data)
385 #ifdef CONFIG_MOUSE
386 int border = DIALOG_LEFT_BORDER + DIALOG_LEFT_INNER_BORDER;
387 int current = widget_data->info.text.current;
388 int scroller_y = widget_data->info.text.scroller_y;
389 int scroller_height = widget_data->info.text.scroller_height;
390 int scroller_middle = scroller_y + scroller_height/2
391 - widget_data->info.text.scroller_last_dir;
392 struct box scroller_box;
393 struct term_event *ev = dlg_data->term_event;
395 set_box(&scroller_box,
396 dlg_data->box.x + dlg_data->box.width - 1 - border,
397 widget_data->box.y,
398 DIALOG_LEFT_INNER_BORDER * 2 + 1,
399 widget_data->box.height);
401 /* One can scroll by clicking or rolling the wheel on the scrollbar
402 * or one or two cells to its left or its right. */
403 if (!check_mouse_position(ev, &scroller_box))
404 return EVENT_NOT_PROCESSED;
406 switch (get_mouse_button(ev)) {
407 case B_LEFT:
408 /* Left click scrolls up or down by one step. */
409 if (ev->info.mouse.y <= scroller_middle)
410 current--;
411 else
412 current++;
413 break;
415 case B_RIGHT:
416 /* Right click scrolls up or down by more than one step.
417 * Faster. */
418 if (ev->info.mouse.y <= scroller_middle)
419 current -= 5;
420 else
421 current += 5;
422 break;
424 case B_WHEEL_UP:
425 /* Mouse wheel up scrolls up. */
426 current--;
427 break;
429 case B_WHEEL_DOWN:
430 /* Mouse wheel up scrolls down. */
431 current++;
432 break;
434 default:
435 return EVENT_NOT_PROCESSED;
438 /* Save last direction used. */
439 if (widget_data->info.text.current < current)
440 widget_data->info.text.scroller_last_dir = 1;
441 else
442 widget_data->info.text.scroller_last_dir = -1;
444 format_and_display_text(widget_data, dlg_data, current);
446 #endif /* CONFIG_MOUSE */
448 return EVENT_PROCESSED;
452 struct widget_ops text_ops = {
453 display_text,
454 NULL,
455 mouse_text,
456 kbd_text,
457 NULL,
458 NULL,