1 /* Text widget implementation. */
13 #include "bfu/dialog.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)))
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
;
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. */
41 split_line(unsigned char *text
, int max_width
, int *cells
, int utf8
)
44 split_line(unsigned char *text
, int max_width
, int *cells
)
45 #endif /* CONFIG_UTF8 */
47 unsigned char *split
= text
;
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
;
60 unsigned char *next_char_begin
= split
65 *cells
+= utf8_char2cells(split
, text_end
);
66 while (*next_split
&& next_split
!= next_char_begin
)
69 next_char_begin
= next_split
;
70 while (is_unsplitable(next_split
))
72 if (next_split
< next_char_begin
) {
76 *cells
+= utf8_char2cells(next_split
, text_end
);
77 next_char_begin
+= utf8charlen(next_split
);
80 #endif /* CONFIG_UTF8 */
82 next_split
= split
+ 1;
84 while (is_unsplitable(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
96 int m_bytes
= utf8_cells2bytes(text
,
99 split
= &text
[m_bytes
];
100 cells_save
= utf8_ptr2cells(text
,
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. */
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
122 while (--split
!= text
) {
124 if (!ispunct(*split
)) continue;
131 /* If no way to do a clean split, just return
132 * requested maximal width. */
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 */
156 static unsigned char **
157 split_lines(struct widget_data
*widget_data
, int max_width
, int utf8
)
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
;
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;
176 /* Skip first leading \n or space. */
177 if (isspace(*text
)) text
++;
181 width
= split_line(text
, max_width
, &cells
, utf8
);
183 width
= split_line(text
, max_width
, &cells
);
186 /* split_line() may return 0. */
188 width
= 1; /* Infinite loop prevention. */
191 cells
= 1; /* Infinite loop prevention. */
194 int_lower_bound(&widget_data
->box
.width
, cells
);
196 if (!realloc_lines(&lines
, line
, line
+ 1))
199 lines
[line
++] = text
;
203 /* Yes it might be a bit ugly on the other hand it will be autofreed
205 widget_data
->cdata
= (unsigned char *) lines
;
206 widget_data
->info
.text
.lines
= line
;
207 widget_data
->info
.text
.max_width
= max_width
;
212 /* Format text according to dialog box and alignment. */
214 dlg_format_text_do(struct dialog_data
*dlg_data
,
216 int x
, int *y
, int width
, int *real_width
,
217 struct color_pair
*color
, enum format_align align
,
220 struct terminal
*term
= dlg_data
->win
->term
;
224 for (; *text
; text
+= line_width
, (*y
)++) {
228 /* Skip first leading \n or space. */
229 if (!firstline
&& isspace(*text
))
236 line_width
= split_line(text
, width
, &cells
, term
->utf8_cp
);
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. */
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
;
258 assert(cells
<= width
&& shift
< width
);
260 draw_dlg_text(term
, dlg_data
, x
+ shift
, *y
, text
, line_width
, 0, color
);
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
,
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
;
276 height
= int_max(0, max_height
- 3);
278 /* If we are drawing set up the box before setting up the
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
;
293 /* Ensure that the current split is valid but don't
294 * split if we don't have to */
296 if (widget_data
->box
.width
!= width
297 && !split_lines(widget_data
, width
, term
->utf8_cp
))
300 if (widget_data
->box
.width
!= width
301 && !split_lines(widget_data
, width
))
305 lines
= (unsigned char **) widget_data
->cdata
;
307 /* Make maximum number of lines available */
308 visible
= int_max(widget_data
->info
.text
.lines
- 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')
334 /* Force dialog to be the width of the longest line */
335 if (real_width
) int_lower_bound(real_width
, widget_data
->box
.width
);
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
;
358 int scale
, current
, step
;
359 int lines
= widget_data
->info
.text
.lines
;
362 dlg_data
->box
.x
+ dlg_data
->box
.width
- DIALOG_LEFT_BORDER
- 1,
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
;
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
;
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
;
409 format_and_display_text(struct widget_data
*widget_data
,
410 struct dialog_data
*dlg_data
,
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
;
419 assert(widget_data
->box
.height
>= 0);
421 int_bounds(¤t
, 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
,
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
)) {
453 case ACT_MENU_PAGE_UP
:
454 current
-= widget_data
->box
.height
;
457 case ACT_MENU_PAGE_DOWN
:
458 current
+= widget_data
->box
.height
;
466 current
= widget_data
->info
.text
.lines
;
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
)
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
,
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
)) {
504 /* Left click scrolls up or down by one step. */
505 if (ev
->info
.mouse
.y
<= scroller_middle
)
512 /* Right click scrolls up or down by more than one step.
514 if (ev
->info
.mouse
.y
<= scroller_middle
)
521 /* Mouse wheel up scrolls up. */
526 /* Mouse wheel up scrolls down. */
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;
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
= {