1 /*
2 * Copyright 2006 John-Mark Bell <jmb@netsurf-browser.org>
3 * Copyright 2009 Paul Blokus <paul_pl@users.sourceforge.net>
4 *
5 * This file is part of NetSurf, http://www.netsurf-browser.org/
6 *
7 * NetSurf is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; version 2 of the License.
10 *
11 * NetSurf is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20 /**
21 * \file
22 *
23 * Single/Multi-line UTF-8 text area implementation.
24 */
25
26 #include <stdint.h>
27 #include <string.h>
28
29 #include "utils/log.h"
30 #include "utils/utf8.h"
31 #include "utils/utils.h"
32 #include "netsurf/browser_window.h"
33 #include "netsurf/plotters.h"
34 #include "netsurf/mouse.h"
35 #include "netsurf/keypress.h"
36 #include "netsurf/clipboard.h"
37 #include "netsurf/layout.h"
38 #include "css/utils.h"
39
40 #include "desktop/textarea.h"
41 #include "desktop/scrollbar.h"
42 #include "desktop/gui_internal.h"
43
44 #define CARET_COLOR 0x0000FF
45 #define TA_ALLOC_STEP 512
46
47 static plot_style_t pstyle_stroke_caret = {
48 .stroke_type = PLOT_OP_TYPE_SOLID,
49 .stroke_colour = CARET_COLOR,
50 .stroke_width = plot_style_int_to_fixed(1),
51 };
52
53 struct line_info {
54 unsigned int b_start; /**< Byte offset of line start */
55 unsigned int b_length; /**< Byte length of line */
56 int width; /**< Width in pixels of line */
57 };
58 struct textarea_drag {
59 textarea_drag_type type;
60 union {
61 struct scrollbar* scrollbar;
62 } data;
63 };
64
65 struct textarea_utf8 {
66 char *data; /**< UTF-8 text */
67 unsigned int alloc; /**< Size of allocated text */
68 unsigned int len; /**< Length of text, in bytes */
69 unsigned int utf8_len; /**< Length of text, in characters without
70 * trailing NULL */
71 };
72
73 struct textarea_undo_detail {
74 unsigned int b_start; /**< Offset to detail's text in undo buffer */
75 unsigned int b_end; /**< End of text (exclusive) */
76 unsigned int b_limit; /**< End of detail space (exclusive) */
77
78 unsigned int b_text_start; /**< Start of textarea text. */
79 unsigned int b_text_end; /**< End of textarea text (exclusive) */
80 };
81
82 struct textarea_undo {
83 unsigned int details_alloc; /**< Details allocated for */
84 unsigned int next_detail; /**< Next detail pos */
85 unsigned int last_detail; /**< Last detail used */
86 struct textarea_undo_detail *details; /**< Array of undo details */
87
88 struct textarea_utf8 text;
89 };
90
91 struct textarea {
92
93 int scroll_x, scroll_y; /**< scroll offsets for the textarea */
94
95 struct scrollbar *bar_x; /**< Horizontal scroll. */
96 struct scrollbar *bar_y; /**< Vertical scroll. */
97
98 unsigned int flags; /**< Textarea flags */
99 int vis_width; /**< Visible width, in pixels */
100 int vis_height; /**< Visible height, in pixels */
101
102 int pad_top; /**< Top padding, inside border, in pixels */
103 int pad_right; /**< Right padding, inside border, in pixels */
104 int pad_bottom; /**< Bottom padding, inside border, in pixels */
105 int pad_left; /**< Left padding, inside border, in pixels */
106
107 int border_width; /**< Border width, in pixels */
108 colour border_col; /**< Border colour */
109
110 int text_y_offset; /**< Vertical dist to 1st line top */
111 int text_y_offset_baseline; /**< Vertical dist to 1st baseline */
112
113 plot_font_style_t fstyle; /**< Text style, inc. textarea bg col */
114 plot_font_style_t sel_fstyle; /**< Selected text style */
115 int line_height; /**< Line height obtained from style */
116
117 struct textarea_utf8 text; /**< Textarea text content */
118 #define PASSWORD_REPLACEMENT "\xe2\x80\xa2"
119 #define PASSWORD_REPLACEMENT_W (sizeof(PASSWORD_REPLACEMENT) - 1)
120 struct textarea_utf8 password; /**< Text for obscured display */
121
122 struct textarea_utf8 *show; /**< Points at .text or .password */
123
124 struct {
125 int line; /**< Line caret is on */
126 int byte_off; /**< Character index of caret on line */
127 } caret_pos;
128
129 int caret_x; /**< cached X coordinate of the caret */
130 int caret_y; /**< cached Y coordinate of the caret */
131
132 int sel_start; /**< Character index of sel start (inclusive) */
133 int sel_end; /**< Character index of sel end (exclusive) */
134
135 int h_extent; /**< Width of content in px */
136 int v_extent; /**< Height of content in px */
137
138 int line_count; /**< Count of lines */
139
140 #define LINE_CHUNK_SIZE 32
141 struct line_info *lines; /**< Line info array */
142 unsigned int lines_alloc_size; /**< Number of LINE_CHUNK_SIZEs */
143
144 /** Callback function for messages to client */
145 textarea_client_callback callback;
146
147 void *data; /**< Client data for callback */
148
149 int drag_start; /**< Byte offset of drag start (in ta->show) */
150 struct textarea_drag drag_info; /**< Drag information */
151
152 struct textarea_undo undo; /**< Undo/redo information */
153 };
154
155
156
157 /**
158 * Normalises any line endings within the text, replacing CRLF or CR with
159 * LF as necessary. If the textarea is single line, then all linebreaks are
160 * converted into spaces.
161 *
162 * \param ta Text area
163 * \param b_start Byte offset to start at
164 * \param b_len Byte length to check
165 */
textarea_normalise_text(struct textarea * ta,unsigned int b_start,unsigned int b_len)166 static void textarea_normalise_text(struct textarea *ta,
167 unsigned int b_start, unsigned int b_len)
168 {
169 bool multi = (ta->flags & TEXTAREA_MULTILINE) ? true : false;
170 struct textarea_msg msg;
171 unsigned int index;
172
173 /* Remove CR characters. If it's a CRLF pair delete the CR, or replace
174 * CR with LF otherwise.
175 */
176 for (index = 0; index < b_len; index++) {
177 if (ta->text.data[b_start + index] == '\r') {
178 if (b_start + index + 1 <= ta->text.len &&
179 ta->text.data[b_start + index + 1] ==
180 '\n') {
181 ta->text.len--;
182 ta->text.utf8_len--;
183 memmove(ta->text.data + b_start + index,
184 ta->text.data + b_start +
185 index + 1,
186 ta->text.len - b_start - index);
187 }
188 else
189 ta->text.data[b_start + index] = '\n';
190 }
191 /* Replace newlines with spaces if this is a single line
192 * textarea.
193 */
194 if (!multi && (ta->text.data[b_start + index] == '\n'))
195 ta->text.data[b_start + index] = ' ';
196 }
197
198 /* Build text modified message */
199 msg.ta = ta;
200 msg.type = TEXTAREA_MSG_TEXT_MODIFIED;
201 msg.data.modified.text = ta->text.data;
202 msg.data.modified.len = ta->text.len;
203
204 /* Pass message to client */
205 ta->callback(ta->data, &msg);
206 }
207
208
209 /**
210 * Reset the selection (no redraw)
211 *
212 * \param ta Text area
213 */
textarea_reset_selection(struct textarea * ta)214 static inline void textarea_reset_selection(struct textarea *ta)
215 {
216 ta->sel_start = ta->sel_end = -1;
217 }
218
219
220 /**
221 * Get the caret's position
222 *
223 * \param ta Text area
224 * \return 0-based byte offset of caret location, or -1 on error
225 */
textarea_get_caret(struct textarea * ta)226 static int textarea_get_caret(struct textarea *ta)
227 {
228 /* Ensure caret isn't hidden */
229 if (ta->caret_pos.byte_off < 0)
230 textarea_set_caret(ta, 0);
231
232 /* If the text is a trailing NULL only */
233 if (ta->text.utf8_len == 0)
234 return 0;
235
236 /* If caret beyond text */
237 if (ta->caret_pos.line >= ta->line_count)
238 return ta->show->len - 1;
239
240 /* Byte offset of line, plus byte offset of caret on line */
241 return ta->lines[ta->caret_pos.line].b_start + ta->caret_pos.byte_off;
242 }
243
244
245 /**
246 * Scrolls a textarea to make the caret visible (doesn't perform a redraw)
247 *
248 * \param ta The text area to be scrolled
249 * \return true if textarea was scrolled false otherwise
250 */
textarea_scroll_visible(struct textarea * ta)251 static bool textarea_scroll_visible(struct textarea *ta)
252 {
253 int x0, x1, y0, y1; /* area we want caret inside */
254 int x, y; /* caret pos */
255 int xs = ta->scroll_x;
256 int ys = ta->scroll_y;
257 int vis;
258 int scrollbar_width;
259 bool scrolled = false;
260
261 if (ta->caret_pos.byte_off < 0)
262 return false;
263
264 scrollbar_width = (ta->bar_y == NULL) ? 0 : SCROLLBAR_WIDTH;
265 x0 = ta->border_width + ta->pad_left;
266 x1 = ta->vis_width - (ta->border_width + ta->pad_right);
267
268 /* Adjust scroll pos for reduced extents */
269 vis = ta->vis_width - 2 * ta->border_width - scrollbar_width;
270 if (ta->h_extent - xs < vis)
271 xs -= vis - (ta->h_extent - xs);
272
273 /* Get caret pos on screen */
274 x = ta->caret_x - xs;
275
276 /* scroll as required */
277 if (x < x0)
278 xs += (x - x0);
279 else if (x > x1)
280 xs += (x - x1);
281
282 if (ta->bar_x == NULL && ta->scroll_x != 0 &&
283 ta->flags & TEXTAREA_MULTILINE) {
284 /* Scrollbar removed, set to zero */
285 ta->scroll_x = 0;
286 scrolled = true;
287
288 } else if (xs != ta->scroll_x) {
289 /* Scrolled, set new pos. */
290 if (ta->bar_x != NULL) {
291 scrollbar_set(ta->bar_x, xs, false);
292 xs = scrollbar_get_offset(ta->bar_x);
293 if (xs != ta->scroll_x) {
294 ta->scroll_x = xs;
295 scrolled = true;
296 }
297
298 } else if (!(ta->flags & TEXTAREA_MULTILINE)) {
299 ta->scroll_x = xs;
300 scrolled = true;
301 }
302 }
303
304 /* check and change vertical scroll */
305 if (ta->flags & TEXTAREA_MULTILINE) {
306 scrollbar_width = (ta->bar_x == NULL) ? 0 : SCROLLBAR_WIDTH;
307 y0 = 0;
308 y1 = ta->vis_height - 2 * ta->border_width -
309 ta->pad_top - ta->pad_bottom;
310
311 /* Adjust scroll pos for reduced extents */
312 vis = ta->vis_height - 2 * ta->border_width - scrollbar_width;
313 if (ta->v_extent - ys < vis)
314 ys -= vis - (ta->v_extent - ys);
315
316 /* Get caret pos on screen */
317 y = ta->caret_y - ys;
318
319 /* scroll as required */
320 if (y < y0)
321 ys += (y - y0);
322 else if (y + ta->line_height > y1)
323 ys += (y + ta->line_height - y1);
324
325 if (ys != ta->scroll_y && ta->bar_y != NULL) {
326 /* Scrolled, set new pos. */
327 scrollbar_set(ta->bar_y, ys, false);
328 ys = scrollbar_get_offset(ta->bar_y);
329 if (ys != ta->scroll_y) {
330 ta->scroll_y = ys;
331 scrolled = true;
332 }
333
334 } else if (ta->bar_y == NULL && ta->scroll_y != 0) {
335 /* Scrollbar removed, set to zero */
336 ta->scroll_y = 0;
337 scrolled = true;
338 }
339 }
340
341 return scrolled;
342 }
343
344
345 /**
346 * Set the caret position
347 *
348 * \param ta Text area
349 * \param caret_b Byte offset to caret
350 * \return true iff caret placement caused a scroll
351 */
textarea_set_caret_internal(struct textarea * ta,int caret_b)352 static bool textarea_set_caret_internal(struct textarea *ta, int caret_b)
353 {
354 unsigned int b_off;
355 int i;
356 int index;
357 int x, y;
358 int x0, y0, x1, y1;
359 int width, height;
360 struct textarea_msg msg;
361 bool scrolled = false;
362
363 if (caret_b != -1 && caret_b > (signed)(ta->show->len - 1))
364 caret_b = ta->show->len - 1;
365
366 /* Delete the old caret */
367 if (ta->caret_pos.byte_off != -1 &&
368 ta->flags & TEXTAREA_INTERNAL_CARET) {
369 x0 = ta->caret_x - ta->scroll_x;
370 y0 = ta->caret_y - ta->scroll_y;
371 width = 2;
372 height = ta->line_height;
373
374 msg.ta = ta;
375 msg.type = TEXTAREA_MSG_REDRAW_REQUEST;
376 msg.data.redraw.x0 = x0;
377 msg.data.redraw.y0 = y0;
378 msg.data.redraw.x1 = x0 + width;
379 msg.data.redraw.y1 = y0 + height;
380
381 /* Ensure it is hidden */
382 ta->caret_pos.byte_off = -1;
383
384 ta->callback(ta->data, &msg);
385 }
386
387 /* check if the caret has to be drawn at all */
388 if (caret_b != -1) {
389 /* Find byte offset of caret position */
390 b_off = caret_b;
391
392 /* Now find line in which byte offset appears */
393 for (i = 0; i < ta->line_count - 1; i++)
394 if (ta->lines[i + 1].b_start > b_off)
395 break;
396
397 /* Set new caret pos */
398 ta->caret_pos.line = i;
399 ta->caret_pos.byte_off = b_off - ta->lines[i].b_start;
400
401 /* Finally, redraw the caret */
402 index = textarea_get_caret(ta);
403
404 /* find byte offset of caret position */
405 b_off = index;
406
407 guit->layout->width(&ta->fstyle,
408 ta->show->data +
409 ta->lines[ta->caret_pos.line].b_start,
410 b_off - ta->lines[ta->caret_pos.line].b_start,
411 &x);
412
413 x += ta->border_width + ta->pad_left;
414 ta->caret_x = x;
415 y = ta->line_height * ta->caret_pos.line + ta->text_y_offset;
416 ta->caret_y = y;
417
418 scrolled = textarea_scroll_visible(ta);
419
420 if (!scrolled && ta->flags & TEXTAREA_INTERNAL_CARET) {
421 /* Didn't scroll, just moved caret.
422 * Caret is internal caret, redraw it */
423 x -= ta->scroll_x;
424 y -= ta->scroll_y;
425 x0 = max(x - 1, ta->border_width);
426 y0 = max(y, 0);
427 x1 = min(x + 1, ta->vis_width - ta->border_width);
428 y1 = min(y + ta->line_height,
429 ta->vis_height);
430
431 width = x1 - x0;
432 height = y1 - y0;
433
434 if (width > 0 && height > 0) {
435 msg.ta = ta;
436 msg.type = TEXTAREA_MSG_REDRAW_REQUEST;
437 msg.data.redraw.x0 = x0;
438 msg.data.redraw.y0 = y0;
439 msg.data.redraw.x1 = x0 + width;
440 msg.data.redraw.y1 = y0 + height;
441
442 ta->callback(ta->data, &msg);
443 }
444 } else if (scrolled && !(ta->flags & TEXTAREA_MULTILINE)) {
445 /* Textarea scrolled, whole area needs redraw */
446 /* With multi-line textareas, the scrollbar
447 * callback will have requested redraw. */
448 msg.ta = ta;
449 msg.type = TEXTAREA_MSG_REDRAW_REQUEST;
450 msg.data.redraw.x0 = 0;
451 msg.data.redraw.y0 = 0;
452 msg.data.redraw.x1 = ta->vis_width;
453 msg.data.redraw.y1 = ta->vis_height;
454
455 ta->callback(ta->data, &msg);
456 }
457
458 if (!(ta->flags & TEXTAREA_INTERNAL_CARET)) {
459 /* Tell client where caret should be placed */
460 struct rect cr = {
461 .x0 = ta->border_width,
462 .y0 = ta->border_width,
463 .x1 = ta->vis_width - ta->border_width -
464 ((ta->bar_y == NULL) ?
465 0 : SCROLLBAR_WIDTH),
466 .y1 = ta->vis_height - ta->border_width -
467 ((ta->bar_x == NULL) ?
468 0 : SCROLLBAR_WIDTH)
469 };
470 msg.ta = ta;
471 msg.type = TEXTAREA_MSG_CARET_UPDATE;
472 msg.data.caret.type = TEXTAREA_CARET_SET_POS;
473 msg.data.caret.pos.x = x - ta->scroll_x;
474 msg.data.caret.pos.y = y - ta->scroll_y;
475 msg.data.caret.pos.height = ta->line_height;
476 msg.data.caret.pos.clip = &cr;
477
478 ta->callback(ta->data, &msg);
479 }
480
481 } else if (!(ta->flags & TEXTAREA_INTERNAL_CARET)) {
482 /* Caret hidden, and client is responsible: tell client */
483 msg.ta = ta;
484 msg.type = TEXTAREA_MSG_CARET_UPDATE;
485 msg.data.caret.type = TEXTAREA_CARET_HIDE;
486
487 ta->callback(ta->data, &msg);
488 }
489
490 return scrolled;
491 }
492
493
494 /**
495 * Selects a character range in the textarea and redraws it
496 *
497 * \param ta Text area
498 * \param b_start First character (inclusive) byte offset
499 * \param b_end Last character (exclusive) byte offset
500 * \param force_redraw Redraw whether selection changed or not
501 * \return true on success false otherwise
502 */
textarea_select(struct textarea * ta,int b_start,int b_end,bool force_redraw)503 static bool textarea_select(struct textarea *ta, int b_start, int b_end,
504 bool force_redraw)
505 {
506 int swap;
507 bool pre_existing_selection = (ta->sel_start != -1);
508 struct textarea_msg msg;
509
510 if (b_start == b_end) {
511 textarea_clear_selection(ta);
512 return true;
513 }
514
515 /* Ensure start is the beginning of the selection */
516 if (b_start > b_end) {
517 swap = b_start;
518 b_start = b_end;
519 b_end = swap;
520 }
521
522 if (ta->sel_start == b_start && ta->sel_end == b_end &&
523 !force_redraw)
524 return true;
525
526 msg.ta = ta;
527 msg.type = TEXTAREA_MSG_REDRAW_REQUEST;
528 msg.data.redraw.x0 = ta->border_width;
529 msg.data.redraw.x1 = ta->vis_width - ta->border_width -
530 ((ta->bar_y == NULL) ? 0 : SCROLLBAR_WIDTH);
531
532 if (force_redraw) {
533 /* Asked to redraw everything */
534 msg.data.redraw.y0 = ta->border_width;
535 msg.data.redraw.y1 = ta->vis_height - ta->border_width -
536 ((ta->bar_x == NULL) ? 0 : SCROLLBAR_WIDTH);
537 } else {
538 /* Try to minimise redraw region */
539 unsigned int b_low, b_high;
540 int line_start = 0, line_end = 0;
541
542 if (!pre_existing_selection) {
543 /* There's a new selection */
544 b_low = b_start;
545 b_high = b_end;
546
547 } else if (ta->sel_start != b_start && ta->sel_end != b_end) {
548 /* Both ends of the selection have moved */
549 b_low = (ta->sel_start < b_start) ?
550 ta->sel_start : b_start;
551 b_high = (ta->sel_end > b_end) ?
552 ta->sel_end : b_end;
553
554 } else if (ta->sel_start != b_start) {
555 /* Selection start changed */
556 if ((signed)ta->sel_start < b_start) {
557 b_low = ta->sel_start;
558 b_high = b_start;
559 } else {
560 b_low = b_start;
561 b_high = ta->sel_start;
562 }
563
564 } else {
565 /* Selection end changed */
566 if ((signed)ta->sel_end < b_end) {
567 b_low = ta->sel_end;
568 b_high = b_end;
569 } else {
570 b_low = b_end;
571 b_high = ta->sel_end;
572 }
573 }
574
575 /* Find redraw start/end lines */
576 for (line_end = 0; line_end < ta->line_count - 1; line_end++)
577 if (ta->lines[line_end + 1].b_start > b_low) {
578 line_start = line_end;
579 break;
580 }
581 for (; line_end < ta->line_count - 1; line_end++)
582 if (ta->lines[line_end + 1].b_start > b_high)
583 break;
584
585 /* Set vertical redraw range */
586 msg.data.redraw.y0 = max(ta->border_width,
587 ta->line_height * line_start +
588 ta->text_y_offset - ta->scroll_y);
589 msg.data.redraw.y1 = min(ta->vis_height - ta->border_width -
590 ((ta->bar_x == NULL) ? 0 : SCROLLBAR_WIDTH),
591 ta->line_height * line_end + ta->text_y_offset +
592 ta->line_height - ta->scroll_y);
593 }
594
595 ta->callback(ta->data, &msg);
596
597 ta->sel_start = b_start;
598 ta->sel_end = b_end;
599
600 if (!pre_existing_selection && ta->sel_start != -1) {
601 /* Didn't have a selection before, but do now */
602 msg.type = TEXTAREA_MSG_SELECTION_REPORT;
603
604 msg.data.selection.have_selection = true;
605 msg.data.selection.read_only = (ta->flags & TEXTAREA_READONLY);
606
607 ta->callback(ta->data, &msg);
608
609 if (!(ta->flags & TEXTAREA_INTERNAL_CARET)) {
610 /* Caret hidden, and client is responsible */
611 msg.type = TEXTAREA_MSG_CARET_UPDATE;
612 msg.data.caret.type = TEXTAREA_CARET_HIDE;
613
614 ta->callback(ta->data, &msg);
615 }
616 }
617
618 return true;
619 }
620
621
622 /**
623 * Selects a text fragment, relative to current caret position.
624 *
625 * \param ta Text area
626 * \return True on success, false otherwise
627 */
textarea_select_fragment(struct textarea * ta)628 static bool textarea_select_fragment(struct textarea * ta)
629 {
630 int caret_pos;
631 size_t sel_start, sel_end;
632
633 /* Fragment separators must be suitable for URLs and ordinary text */
634 static const char *sep = " /:.\r\n";
635
636 caret_pos = textarea_get_caret(ta);
637 if (caret_pos < 0) {
638 return false;
639 }
640
641 if (ta->show->len == 0) {
642 return false;
643 }
644
645 /* Compute byte offset of caret position */
646 for (sel_start = (caret_pos > 0 ? caret_pos - 1 : caret_pos);
647 sel_start > 0; sel_start--) {
648 /* Cache the character offset of the last separator */
649 if (strchr(sep, ta->show->data[sel_start]) != NULL) {
650 /* Found start,
651 * add one to start to skip over separator */
652 sel_start++;
653 break;
654 }
655 }
656
657 /* Search for next separator, if any */
658 for (sel_end = caret_pos; sel_end < ta->show->len - 1; sel_end++) {
659 if (strchr(sep, ta->show->data[sel_end]) != NULL) {
660 break;
661 }
662 }
663
664 if (sel_start < sel_end) {
665 textarea_select(ta, sel_start, sel_end, false);
666 return true;
667 }
668
669 return false;
670 }
671
672
673 /**
674 * Selects paragraph, at current caret position.
675 *
676 * \param ta textarea widget
677 * \return True on success, false otherwise
678 */
textarea_select_paragraph(struct textarea * ta)679 static bool textarea_select_paragraph(struct textarea * ta)
680 {
681 int caret_pos;
682 size_t sel_start, sel_end;
683
684 caret_pos = textarea_get_caret(ta);
685 if (caret_pos < 0) {
686 return false;
687 }
688
689 /* Work back from caret, looking for a place to start selection */
690 for (sel_start = (caret_pos > 0 ? caret_pos - 1 : caret_pos);
691 sel_start > 0; sel_start--) {
692 /* Set selection start as character after any new line found */
693 if (ta->show->data[sel_start] == '\n') {
694 /* Add one to start to skip over separator */
695 sel_start++;
696 break;
697 }
698 }
699
700 /* Search for end of selection */
701 for (sel_end = caret_pos; sel_end < ta->show->len - 1; sel_end++) {
702 if (ta->show->data[sel_end] == '\n') {
703 break;
704 }
705 }
706
707 if (sel_start < sel_end) {
708 textarea_select(ta, sel_start, sel_end, false);
709 return true;
710 }
711
712 return false;
713 }
714
715
716 /**
717 * Callback for scrollbar widget.
718 */
textarea_scrollbar_callback(void * client_data,struct scrollbar_msg_data * scrollbar_data)719 static void textarea_scrollbar_callback(void *client_data,
720 struct scrollbar_msg_data *scrollbar_data)
721 {
722 struct textarea *ta = client_data;
723 struct textarea_msg msg;
724
725 switch(scrollbar_data->msg) {
726 case SCROLLBAR_MSG_MOVED:
727 /* Scrolled; redraw everything */
728 ta->scroll_x = scrollbar_get_offset(ta->bar_x);
729 ta->scroll_y = scrollbar_get_offset(ta->bar_y);
730
731 msg.ta = ta;
732 msg.type = TEXTAREA_MSG_REDRAW_REQUEST;
733 msg.data.redraw.x0 = 0;
734 msg.data.redraw.y0 = 0;
735 msg.data.redraw.x1 = ta->vis_width;
736 msg.data.redraw.y1 = ta->vis_height;
737
738 ta->callback(ta->data, &msg);
739
740 if (!(ta->flags & TEXTAREA_INTERNAL_CARET) &&
741 ta->sel_start < 0 &&
742 ta->caret_pos.byte_off >= 0) {
743 /* Tell client where caret should be placed */
744 int x = ta->caret_x - ta->scroll_x;
745 int y = ta->caret_y - ta->scroll_y;
746 int h = ta->line_height;
747 struct rect cr = {
748 .x0 = ta->border_width,
749 .y0 = ta->border_width,
750 .x1 = ta->vis_width - ta->border_width -
751 ((ta->bar_y == NULL) ?
752 0 : SCROLLBAR_WIDTH),
753 .y1 = ta->vis_height - ta->border_width -
754 ((ta->bar_x == NULL) ?
755 0 : SCROLLBAR_WIDTH)
756 };
757
758 msg.ta = ta;
759 msg.type = TEXTAREA_MSG_CARET_UPDATE;
760
761 if ((x >= cr.x0 && x < cr.x1) &&
762 (y + h >= cr.y0 && y < cr.y1)) {
763 /* Caret inside textarea */
764 msg.data.caret.type = TEXTAREA_CARET_SET_POS;
765 msg.data.caret.pos.x = x;
766 msg.data.caret.pos.y = y;
767 msg.data.caret.pos.height = h;
768 msg.data.caret.pos.clip = &cr;
769 } else {
770 /* Caret fully outside textarea */
771 msg.data.caret.type = TEXTAREA_CARET_HIDE;
772 }
773
774 ta->callback(ta->data, &msg);
775 }
776 break;
777
778 case SCROLLBAR_MSG_SCROLL_START:
779 ta->drag_info.type = TEXTAREA_DRAG_SCROLLBAR;
780 ta->drag_info.data.scrollbar = scrollbar_data->scrollbar;
781
782 msg.ta = ta;
783 msg.type = TEXTAREA_MSG_DRAG_REPORT;
784 msg.data.drag = ta->drag_info.type;
785
786 /* Tell client we're handling a drag */
787 ta->callback(ta->data, &msg);
788 break;
789
790 case SCROLLBAR_MSG_SCROLL_FINISHED:
791 ta->drag_info.type = TEXTAREA_DRAG_NONE;
792
793 msg.ta = ta;
794 msg.type = TEXTAREA_MSG_DRAG_REPORT;
795 msg.data.drag = ta->drag_info.type;
796
797 /* Tell client we finished handling the drag */
798 ta->callback(ta->data, &msg);
799 break;
800
801 default:
802 break;
803 }
804 }
805
806
807
808 /**
809 * Reflow a single line textarea
810 *
811 * \param ta Textarea widget to reflow
812 * \param b_off 0-based byte offset in ta->show's text to start of modification
813 * \param r Modified/reduced to area where redraw is required
814 * \return true on success false otherwise
815 */
textarea_reflow_singleline(struct textarea * ta,size_t b_off,struct rect * r)816 static bool textarea_reflow_singleline(struct textarea *ta, size_t b_off,
817 struct rect *r)
818 {
819 int x;
820 int shift;
821 int retained_width = 0;
822 int w = ta->vis_width - 2 * ta->border_width -
823 ta->pad_left - ta->pad_right;
824
825 assert(!(ta->flags & TEXTAREA_MULTILINE));
826
827 if (ta->lines == NULL) {
828 ta->lines =
829 malloc(LINE_CHUNK_SIZE * sizeof(struct line_info));
830 if (ta->lines == NULL) {
831 NSLOG(netsurf, INFO, "malloc failed");
832 return false;
833 }
834 ta->lines_alloc_size = LINE_CHUNK_SIZE;
835
836 ta->lines[0].b_start = 0;
837 ta->lines[0].b_length = 0;
838 ta->lines[0].width = 0;
839 }
840
841 if (ta->flags & TEXTAREA_PASSWORD &&
842 ta->text.utf8_len != ta->password.utf8_len) {
843 /* Make password-obscured text have same number of
844 * characters as underlying text */
845 unsigned int c, b;
846 int diff = ta->text.utf8_len - ta->password.utf8_len;
847 unsigned int rep_len = PASSWORD_REPLACEMENT_W;
848 unsigned int b_len = ta->text.utf8_len * rep_len + 1;
849
850 if (diff > 0 && b_len > ta->password.alloc) {
851 /* Increase password alloaction */
852 char *temp = realloc(ta->password.data,
853 b_len + TA_ALLOC_STEP);
854 if (temp == NULL) {
855 NSLOG(netsurf, INFO, "realloc failed");
856 return false;
857 }
858
859 ta->password.data = temp;
860 ta->password.alloc = b_len + TA_ALLOC_STEP;
861 }
862
863 b_len--;
864 for (c = 0; c < b_len; c += rep_len) {
865 for (b = 0; b < rep_len; b++) {
866 ta->password.data[c + b] =
867 PASSWORD_REPLACEMENT[b];
868 }
869 }
870 ta->password.data[b_len] = '\0';
871 ta->password.len = b_len + 1;
872 ta->password.utf8_len = ta->text.utf8_len;
873 }
874
875 /* Measure new width */
876 guit->layout->width(&ta->fstyle, ta->show->data,
877 ta->show->len - 1, &x);
878
879 /* Get width of retained text */
880 if (b_off != ta->lines[0].b_length) {
881 guit->layout->width(&ta->fstyle, ta->show->data,
882 b_off, &retained_width);
883 } else {
884 retained_width = ta->lines[0].width;
885 }
886
887 shift = ta->border_width + ta->pad_left - ta->scroll_x;
888
889 r->x0 = max(r->x0, retained_width + shift - 1);
890 r->x1 = min(r->x1, max(x, ta->lines[0].width) + shift + 1);
891
892 ta->lines[0].b_start = 0;
893 ta->lines[0].b_length = ta->show->len - 1;
894 ta->lines[0].width = x;
895
896 if (x > w)
897 w = x;
898
899 ta->h_extent = w + ta->pad_left + ta->pad_right;
900 ta->line_count = 1;
901
902 return true;
903 }
904
905
906
907 /**
908 * Reflow a multiline textarea from the given line onwards
909 *
910 * \param ta Textarea to reflow
911 * \param b_start 0-based byte offset in ta->text to start of modification
912 * \param b_length Byte length of change in textarea text
913 * \param r Modified/reduced to area where redraw is required
914 * \return true on success false otherwise
915 */
textarea_reflow_multiline(struct textarea * ta,const size_t b_start,const int b_length,struct rect * r)916 static bool textarea_reflow_multiline(struct textarea *ta,
917 const size_t b_start, const int b_length, struct rect *r)
918 {
919 char *text;
920 unsigned int len;
921 unsigned int start;
922 size_t b_off;
923 size_t b_start_line_end;
924 int x;
925 char *space, *para_end;
926 unsigned int line; /* line count */
927 unsigned int scroll_lines;
928 int avail_width;
929 int h_extent; /* horizontal extent */
930 int v_extent; /* vertical extent */
931 bool restart = false;
932 bool skip_line = false;
933
934 assert(ta->flags & TEXTAREA_MULTILINE);
935
936 if (ta->lines == NULL) {
937 ta->lines = calloc(sizeof(struct line_info), LINE_CHUNK_SIZE);
938 if (ta->lines == NULL) {
939 NSLOG(netsurf, INFO,
940 "Failed to allocate memory for textarea lines");
941 return false;
942 }
943 ta->lines_alloc_size = LINE_CHUNK_SIZE;
944 }
945
946 /* Get line of start of changes */
947 for (start = 0; (signed) start < ta->line_count - 1; start++)
948 if (ta->lines[start + 1].b_start > b_start)
949 break;
950
951 /* Find max number of lines before vertical scrollbar is required */
952 scroll_lines = (ta->vis_height - 2 * ta->border_width -
953 ta->pad_top - ta->pad_bottom) /
954 ta->line_height;
955
956 /* Start on the line before the first change, in case the
957 * modification on this line alters what fits on the line
958 * above. For example adding a space or deleting text on
959 * a soft-wrapped line */
960 if (start != 0)
961 start--;
962
963 /* Record original end pos of start line */
964 b_start_line_end = ta->lines[start].b_start + ta->lines[start].b_length;
965
966 /* During layout we may decide we need to restart again from the
967 * textarea's first line. */
968 do {
969 /* If a vertical scrollbar has been added or removed, we need
970 * to restart from the first line in the textarea. */
971 if (restart)
972 start = 0;
973
974 /* Set current line to the starting line */
975 line = start;
976
977 /* Find available width */
978 avail_width = ta->vis_width - 2 * ta->border_width -
979 ta->pad_left - ta->pad_right;
980 if (avail_width < 0)
981 avail_width = 0;
982 h_extent = avail_width;
983
984 /* Set up length of remaining text and offset to current point
985 * in text. Initially set it to start of textarea */
986 len = ta->text.len - 1;
987 text = ta->text.data;
988
989 if (line != 0) {
990 /* Not starting at the beginning of the textarea, so
991 * jump forward, and make sure the horizontal extents
992 * accommodate the width of the skipped lines. */
993 unsigned int i;
994 len -= ta->lines[line].b_start;
995 text += ta->lines[line].b_start;
996
997 for (i = 0; i < line; i++) {
998 if (ta->lines[i].width > h_extent) {
999 h_extent = ta->lines[i].width;
1000 }
1001 }
1002 }
1003
1004 if (ta->text.len == 1) {
1005 /* Handle empty textarea */
1006 assert(ta->text.data[0] == '\0');
1007 ta->lines[line].b_start = 0;
1008 ta->lines[line].b_length = 0;
1009 ta->lines[line++].width = 0;
1010 ta->line_count = 1;
1011 }
1012
1013 restart = false;
1014 for (; len > 0; len -= b_off, text += b_off) {
1015 /* Find end of paragraph */
1016 for (para_end = text; para_end < text + len;
1017 para_end++) {
1018 if (*para_end == '\n')
1019 break;
1020 }
1021
1022 /* Wrap current line in paragraph */
1023 guit->layout->split(&ta->fstyle, text, para_end - text,
1024 avail_width, &b_off, &x);
1025 /* b_off now marks space, or end of paragraph */
1026
1027 if (x > h_extent) {
1028 h_extent = x;
1029 }
1030 if (x > avail_width && ta->bar_x == NULL) {
1031 /* We need to insert a horizontal scrollbar */
1032 int w = ta->vis_width - 2 * ta->border_width;
1033 if (scrollbar_create(true, w, w, w,
1034 ta, textarea_scrollbar_callback,
1035 &(ta->bar_x)) != NSERROR_OK) {
1036 return false;
1037 }
1038 if (ta->bar_y != NULL)
1039 scrollbar_make_pair(ta->bar_x,
1040 ta->bar_y);
1041 ta->pad_bottom += SCROLLBAR_WIDTH;
1042
1043 /* Find new max visible lines */
1044 scroll_lines = (ta->vis_height -
1045 2 * ta->border_width -
1046 ta->pad_top - ta->pad_bottom) /
1047 ta->line_height;
1048 }
1049
1050 /* Ensure enough storage for lines data */
1051 if (line > ta->lines_alloc_size - 2) {
1052 /* Up to two lines my be added in a pass */
1053 struct line_info *temp = realloc(ta->lines,
1054 (line + 2 + LINE_CHUNK_SIZE) *
1055 sizeof(struct line_info));
1056 if (temp == NULL) {
1057 NSLOG(netsurf, INFO, "realloc failed");
1058 return false;
1059 }
1060
1061 ta->lines = temp;
1062 ta->lines_alloc_size = line + 2 +
1063 LINE_CHUNK_SIZE;
1064 }
1065
1066 if (para_end == text + b_off && *para_end == '\n') {
1067 /* Not found any spaces to wrap at, and we
1068 * have a newline char */
1069 ta->lines[line].b_start = text - ta->text.data;
1070 ta->lines[line].b_length = para_end - text;
1071 ta->lines[line++].width = x;
1072
1073 /* Jump newline */
1074 b_off++;
1075
1076 if (len - b_off == 0) {
1077 /* reached end of input;
1078 * add last line */
1079 ta->lines[line].b_start = text +
1080 b_off - ta->text.data;
1081 ta->lines[line].b_length = 0;
1082 ta->lines[line++].width = x;
1083 }
1084
1085 if (line > scroll_lines && ta->bar_y == NULL)
1086 break;
1087
1088 continue;
1089
1090 } else if (len - b_off > 0) {
1091 /* soft wrapped, find last space (if any) */
1092 for (space = text + b_off; space > text;
1093 space--) {
1094 if (*space == ' ')
1095 break;
1096 }
1097
1098 if (space != text)
1099 b_off = space + 1 - text;
1100 }
1101
1102 ta->lines[line].b_start = text - ta->text.data;
1103 ta->lines[line].b_length = b_off;
1104 ta->lines[line++].width = x;
1105
1106 if (line > scroll_lines && ta->bar_y == NULL)
1107 break;
1108 }
1109
1110 if (h_extent <= avail_width && ta->bar_x != NULL) {
1111 /* We need to remove a horizontal scrollbar */
1112 scrollbar_destroy(ta->bar_x);
1113 ta->bar_x = NULL;
1114 ta->pad_bottom -= SCROLLBAR_WIDTH;
1115
1116 /* Find new max visible lines */
1117 scroll_lines = (ta->vis_height - 2 * ta->border_width -
1118 ta->pad_top - ta->pad_bottom) /
1119 ta->line_height;
1120 }
1121
1122 if (line > scroll_lines && ta->bar_y == NULL) {
1123 /* Add vertical scrollbar */
1124 int h = ta->vis_height - 2 * ta->border_width;
1125 if (scrollbar_create(false, h, h, h,
1126 ta, textarea_scrollbar_callback,
1127 &(ta->bar_y)) != NSERROR_OK) {
1128 return false;
1129 }
1130 if (ta->bar_x != NULL)
1131 scrollbar_make_pair(ta->bar_x,
1132 ta->bar_y);
1133 ta->pad_right += SCROLLBAR_WIDTH;
1134 restart = true;
1135
1136 } else if (line <= scroll_lines && ta->bar_y != NULL) {
1137 /* Remove vertical scrollbar */
1138 scrollbar_destroy(ta->bar_y);
1139 ta->bar_y = NULL;
1140 ta->pad_right -= SCROLLBAR_WIDTH;
1141 restart = true;
1142 }
1143 } while (restart);
1144
1145 h_extent += ta->pad_left + ta->pad_right -
1146 (ta->bar_y != NULL ? SCROLLBAR_WIDTH : 0);
1147 v_extent = line * ta->line_height + ta->pad_top +
1148 ta->pad_bottom -
1149 (ta->bar_x != NULL ? SCROLLBAR_WIDTH : 0);
1150
1151 if (ta->bar_x != NULL) {
1152 /* Set horizontal scrollbar extents */
1153 int w = ta->vis_width - 2 * ta->border_width -
1154 (ta->bar_y != NULL ? SCROLLBAR_WIDTH : 0);
1155 scrollbar_set_extents(ta->bar_x, w, w, h_extent);
1156 }
1157
1158 if (ta->bar_y != NULL) {
1159 /* Set vertical scrollbar extents */
1160 int h = ta->vis_height - 2 * ta->border_width;
1161 scrollbar_set_extents(ta->bar_y, h,
1162 h - (ta->bar_x != NULL ? SCROLLBAR_WIDTH : 0),
1163 v_extent);
1164 }
1165
1166 ta->h_extent = h_extent;
1167 ta->v_extent = v_extent;
1168 ta->line_count = line;
1169
1170 /* Update start line end byte pos, if it's increased */
1171 if (ta->lines[start].b_start + ta->lines[start].b_length >
1172 b_start_line_end) {
1173 b_start_line_end = ta->lines[start].b_start +
1174 ta->lines[start].b_length;
1175 }
1176
1177 /* Don't need to redraw above changes, so update redraw request rect */
1178 if (b_start_line_end < b_start && restart == false) {
1179 /* Start line is unchanged */
1180 start++;
1181 skip_line = true;
1182 }
1183
1184 r->y0 = max(r->y0, (signed)(ta->line_height * start +
1185 ta->text_y_offset - ta->scroll_y));
1186
1187 /* Reduce redraw region to single line if possible */
1188 if ((skip_line || start == 0) &&
1189 ta->lines[start].b_start + ta->lines[start].b_length >=
1190 b_start + b_length) {
1191 size_t b_line_end = ta->lines[start].b_start +
1192 ta->lines[start].b_length;
1193 text = ta->text.data + b_line_end;
1194 if (*text == '\0' || *text == '\n') {
1195 r->y1 = min(r->y1, (signed)
1196 (ta->line_height * (start + 1) +
1197 ta->text_y_offset - ta->scroll_y));
1198 if (b_start > ta->lines[start].b_start &&
1199 b_start <= b_line_end) {
1200 /* Remove unchanged text at start of line
1201 * from redraw region */
1202 int retained_width = 0;
1203 size_t retain_end = b_start -
1204 ta->lines[start].b_start;
1205 text = ta->text.data + ta->lines[start].b_start;
1206
1207 guit->layout->width(&ta->fstyle, text,
1208 retain_end, &retained_width);
1209
1210 r->x0 = max(r->x0,
1211 retained_width +
1212 ta->border_width +
1213 ta->pad_left -
1214 ta->scroll_x - 1);
1215 }
1216 }
1217 }
1218
1219 return true;
1220 }
1221
1222
1223 /**
1224 * get byte offset from the beginning of the text for some coordinates
1225 *
1226 * \param ta textarea widget
1227 * \param x X coordinate
1228 * \param y Y coordinate
1229 * \param visible true iff (x,y) is wrt visiable area, false for global
1230 * \return byte offset
1231 */
textarea_get_b_off_xy(struct textarea * ta,int x,int y,bool visible)1232 static size_t textarea_get_b_off_xy(struct textarea *ta, int x, int y,
1233 bool visible)
1234 {
1235 size_t bpos; /* Byte position in utf8 string */
1236 int line;
1237
1238 if (!ta->line_count) {
1239 return 0;
1240 }
1241
1242 x = x - ta->border_width - ta->pad_left +
1243 (visible ? ta->scroll_x : 0);
1244 y = y - ta->border_width - ta->pad_top +
1245 (visible ? ta->scroll_y : 0);
1246
1247 if (x < 0)
1248 x = 0;
1249
1250 line = y / ta->line_height;
1251
1252 if (ta->line_count - 1 < line)
1253 line = ta->line_count - 1;
1254 if (line < 0)
1255 line = 0;
1256
1257 /* Get byte position */
1258 guit->layout->position(&ta->fstyle,
1259 ta->show->data + ta->lines[line].b_start,
1260 ta->lines[line].b_length, x, &bpos, &x);
1261
1262
1263 /* If the calculated byte offset corresponds with the number of bytes
1264 * in the line, and the line has been soft-wrapped, then ensure the
1265 * caret offset is before the trailing space character, rather than
1266 * after it. Otherwise, the caret will be placed at the start of the
1267 * following line, which is undesirable.
1268 */
1269 if (ta->flags & TEXTAREA_MULTILINE && ta->lines[line].b_length > 1 &&
1270 bpos == (unsigned)ta->lines[line].b_length &&
1271 ta->show->data[ta->lines[line].b_start +
1272 ta->lines[line].b_length - 1] == ' ')
1273 bpos--;
1274
1275 /* Set the return byte offset */
1276 return bpos + ta->lines[line].b_start;
1277 }
1278
1279
1280 /**
1281 * Set the caret's position
1282 *
1283 * \param ta textarea widget
1284 * \param x X coordinate
1285 * \param y Y coordinate
1286 * \param visible true iff (x,y) is wrt visiable area, false for global
1287 * \return true iff caret placement caused a scroll
1288 */
textarea_set_caret_xy(struct textarea * ta,int x,int y,bool visible)1289 static bool textarea_set_caret_xy(struct textarea *ta, int x, int y,
1290 bool visible)
1291 {
1292 unsigned int b_off = textarea_get_b_off_xy(ta, x, y, visible);
1293
1294 return textarea_set_caret_internal(ta, b_off);
1295 }
1296
1297
1298 /**
1299 * Insert text into the textarea
1300 *
1301 * \param ta Textarea widget
1302 * \param text UTF-8 text to insert
1303 * \param b_off 0-based byte offset in ta->show's text to insert at
1304 * \param b_len Byte length of UTF-8 text
1305 * \param byte_delta Updated to change in byte count in textarea (ta->show)
1306 * \param r Modified/reduced to area where redraw is required
1307 * \return false on memory exhaustion, true otherwise
1308 *
1309 * Note: b_off must be for ta->show
1310 */
textarea_insert_text(struct textarea * ta,const char * text,size_t b_off,size_t b_len,int * byte_delta,struct rect * r)1311 static bool textarea_insert_text(struct textarea *ta, const char *text,
1312 size_t b_off, size_t b_len, int *byte_delta, struct rect *r)
1313 {
1314 int char_delta;
1315 const size_t show_b_off = b_off;
1316
1317 if (ta->flags & TEXTAREA_READONLY)
1318 return true;
1319
1320 /* If password field, we must convert from ta->password byte offset to
1321 * ta->text byte offset */
1322 if (ta->flags & TEXTAREA_PASSWORD) {
1323 size_t c_off;
1324
1325 c_off = utf8_bounded_length(ta->password.data, b_off);
1326 b_off = utf8_bounded_byte_length(ta->text.data,
1327 ta->text.len - 1, c_off);
1328 }
1329
1330 /* Find insertion point */
1331 if (b_off > ta->text.len - 1)
1332 b_off = ta->text.len - 1;
1333
1334 if (b_len + ta->text.len >= ta->text.alloc) {
1335 char *temp = realloc(ta->text.data, b_len + ta->text.len +
1336 TA_ALLOC_STEP);
1337 if (temp == NULL) {
1338 NSLOG(netsurf, INFO, "realloc failed");
1339 return false;
1340 }
1341
1342 ta->text.data = temp;
1343 ta->text.alloc = b_len + ta->text.len + TA_ALLOC_STEP;
1344 }
1345
1346 /* Shift text following up */
1347 memmove(ta->text.data + b_off + b_len, ta->text.data + b_off,
1348 ta->text.len - b_off);
1349 /* Insert new text */
1350 memcpy(ta->text.data + b_off, text, b_len);
1351
1352 char_delta = ta->text.utf8_len;
1353 *byte_delta = ta->text.len;
1354
1355 /* Update lengths, and normalise */
1356 ta->text.len += b_len;
1357 ta->text.utf8_len += utf8_bounded_length(text, b_len);
1358 textarea_normalise_text(ta, b_off, b_len);
1359
1360 /* Get byte delta */
1361 if (ta->flags & TEXTAREA_PASSWORD) {
1362 char_delta = ta->text.utf8_len - char_delta;
1363 *byte_delta = char_delta * PASSWORD_REPLACEMENT_W;
1364 } else {
1365 *byte_delta = ta->text.len - *byte_delta;
1366 }
1367
1368 /* See to reflow */
1369 if (ta->flags & TEXTAREA_MULTILINE) {
1370 if (!textarea_reflow_multiline(ta, show_b_off, b_len, r))
1371 return false;
1372 } else {
1373 if (!textarea_reflow_singleline(ta, show_b_off, r))
1374 return false;
1375 }
1376
1377 return true;
1378 }
1379
1380
1381 /**
1382 * Helper for replace_text function converts character offset to byte offset
1383 *
1384 * text utf8 textarea text object
1385 * start start character offset
1386 * end end character offset
1387 * b_start updated to byte offset of start in text
1388 * b_end updated to byte offset of end in text
1389 */
textarea_char_to_byte_offset(struct textarea_utf8 * text,unsigned int start,unsigned int end,size_t * b_start,size_t * b_end)1390 static inline void textarea_char_to_byte_offset(struct textarea_utf8 *text,
1391 unsigned int start, unsigned int end,
1392 size_t *b_start, size_t *b_end)
1393 {
1394 size_t diff = end - start;
1395 /* find byte offset of replace start */
1396 for (*b_start = 0; start-- > 0;
1397 *b_start = utf8_next(text->data, text->len - 1,
1398 *b_start))
1399 ; /* do nothing */
1400
1401 /* find byte length of replaced text */
1402 for (*b_end = *b_start; diff-- > 0;
1403 *b_end = utf8_next(text->data, text->len - 1, *b_end))
1404 ; /* do nothing */
1405 }
1406
1407
1408 /**
1409 * Perform actual text replacment in a textarea
1410 *
1411 * \param ta Textarea widget
1412 * \param b_start Start byte index of replaced section (inclusive)
1413 * \param b_end End byte index of replaced section (exclusive)
1414 * \param rep Replacement UTF-8 text to insert
1415 * \param rep_len Byte length of replacement UTF-8 text
1416 * \param add_to_clipboard True iff replaced text to be added to clipboard
1417 * \param byte_delta Updated to change in byte count in textarea (ta->show)
1418 * \param r Updated to area where redraw is required
1419 * \return false on memory exhaustion, true otherwise
1420 *
1421 * Note, b_start and b_end must be the byte offsets in ta->show, so in the
1422 * password textarea case, they are for ta->password.
1423 */
textarea_replace_text_internal(struct textarea * ta,size_t b_start,size_t b_end,const char * rep,size_t rep_len,bool add_to_clipboard,int * byte_delta,struct rect * r)1424 static bool textarea_replace_text_internal(struct textarea *ta, size_t b_start,
1425 size_t b_end, const char *rep, size_t rep_len,
1426 bool add_to_clipboard, int *byte_delta, struct rect *r)
1427 {
1428 int char_delta;
1429 const size_t show_b_off = b_start;
1430 *byte_delta = 0;
1431
1432 if ((ta->flags & TEXTAREA_READONLY) &&
1433 !(rep == NULL && rep_len == 0 && add_to_clipboard))
1434 /* Can't edit if readonly, and we're not just copying */
1435 return true;
1436
1437 if (b_start > ta->show->len - 1)
1438 b_start = ta->show->len - 1;
1439 if (b_end > ta->show->len - 1)
1440 b_end = ta->show->len - 1;
1441
1442 /* Set up initial redraw rect */
1443 r->x0 = ta->border_width;
1444 r->y0 = ta->border_width;
1445 r->x1 = ta->vis_width - ta->border_width -
1446 ((ta->bar_y == NULL) ? 0 : SCROLLBAR_WIDTH);
1447 r->y1 = ta->vis_height - ta->border_width -
1448 ((ta->bar_x == NULL) ? 0 : SCROLLBAR_WIDTH);
1449
1450 /* Early exit if just inserting */
1451 if (b_start == b_end && rep != NULL)
1452 return textarea_insert_text(ta, rep, b_start, rep_len,
1453 byte_delta, r);
1454
1455 if (b_start > b_end)
1456 return false;
1457
1458 /* Place CUTs on clipboard */
1459 if (add_to_clipboard) {
1460 guit->clipboard->set(ta->show->data + b_start, b_end - b_start,
1461 NULL, 0);
1462 }
1463
1464 if (rep == NULL) {
1465 /* No replacement text */
1466 return true;
1467 }
1468
1469 /* If password field, we must convert from ta->password byte offset to
1470 * ta->text byte offset */
1471 if (ta->flags & TEXTAREA_PASSWORD) {
1472 size_t c_start, c_end;
1473
1474 c_start = utf8_bounded_length(ta->password.data, b_start);
1475 c_end = c_start;
1476 c_end += utf8_bounded_length(ta->password.data + b_start,
1477 b_end - b_start);
1478 textarea_char_to_byte_offset(&ta->text, c_start, c_end,
1479 &b_start, &b_end);
1480 }
1481
1482 /* Ensure textarea's text buffer is large enough */
1483 if (rep_len + ta->text.len - (b_end - b_start) >= ta->text.alloc) {
1484 char *temp = realloc(ta->text.data,
1485 rep_len + ta->text.len - (b_end - b_start) +
1486 TA_ALLOC_STEP);
1487 if (temp == NULL) {
1488 NSLOG(netsurf, INFO, "realloc failed");
1489 return false;
1490 }
1491
1492 ta->text.data = temp;
1493 ta->text.alloc = rep_len + ta->text.len - (b_end - b_start) +
1494 TA_ALLOC_STEP;
1495 }
1496
1497 /* Shift text following to new position */
1498 memmove(ta->text.data + b_start + rep_len, ta->text.data + b_end,
1499 ta->text.len - b_end);
1500
1501 /* Insert new text */
1502 memcpy(ta->text.data + b_start, rep, rep_len);
1503
1504 char_delta = ta->text.utf8_len;
1505 *byte_delta = ta->text.len;
1506
1507 /* Update lengths, and normalise */
1508 ta->text.len += (int)rep_len - (b_end - b_start);
1509 ta->text.utf8_len = utf8_length(ta->text.data);
1510 textarea_normalise_text(ta, b_start, rep_len);
1511
1512 /* Get byte delta */
1513 if (ta->flags & TEXTAREA_PASSWORD) {
1514 char_delta = ta->text.utf8_len - char_delta;
1515 *byte_delta = char_delta * PASSWORD_REPLACEMENT_W;
1516 } else {
1517 *byte_delta = ta->text.len - *byte_delta;
1518 }
1519
1520 /* See to reflow */
1521 if (ta->flags & TEXTAREA_MULTILINE) {
1522 if (!textarea_reflow_multiline(ta, b_start, *byte_delta, r))
1523 return false;
1524 } else {
1525 if (!textarea_reflow_singleline(ta, show_b_off, r))
1526 return false;
1527 }
1528
1529 return true;
1530 }
1531
1532
1533 /**
1534 * Update undo buffer by adding any text to be replaced, and allocating
1535 * space as appropriate.
1536 *
1537 * \param ta Textarea widget
1538 * \param b_start Start byte index of replaced section (inclusive)
1539 * \param b_end End byte index of replaced section (exclusive)
1540 * \param rep_len Byte length of replacement UTF-8 text
1541 * \return false on memory exhaustion, true otherwise
1542 */
textarea_copy_to_undo_buffer(struct textarea * ta,size_t b_start,size_t b_end,size_t rep_len)1543 static bool textarea_copy_to_undo_buffer(struct textarea *ta,
1544 size_t b_start, size_t b_end, size_t rep_len)
1545 {
1546 struct textarea_undo *undo;
1547 size_t b_offset;
1548 unsigned int len = b_end - b_start;
1549
1550 undo = &ta->undo;
1551
1552 if (undo->next_detail == 0)
1553 b_offset = 0;
1554 else
1555 b_offset = undo->details[undo->next_detail - 1].b_start +
1556 undo->details[undo->next_detail - 1].b_limit;
1557
1558 len = len > rep_len ? len : rep_len;
1559
1560 if (b_offset + len >= undo->text.alloc) {
1561 /* Need more memory for undo buffer */
1562 char *temp = realloc(undo->text.data,
1563 b_offset + len + TA_ALLOC_STEP);
1564 if (temp == NULL) {
1565 NSLOG(netsurf, INFO, "realloc failed");
1566 return false;
1567 }
1568
1569 undo->text.data = temp;
1570 undo->text.alloc = b_offset + len + TA_ALLOC_STEP;
1571 }
1572
1573 if (undo->next_detail >= undo->details_alloc) {
1574 /* Need more memory for undo details */
1575 struct textarea_undo_detail *temp = realloc(undo->details,
1576 (undo->next_detail + 128) *
1577 sizeof(struct textarea_undo_detail));
1578 if (temp == NULL) {
1579 NSLOG(netsurf, INFO, "realloc failed");
1580 return false;
1581 }
1582
1583 undo->details = temp;
1584 undo->details_alloc = undo->next_detail + 128;
1585 }
1586
1587 /* Put text into buffer */
1588 memcpy(undo->text.data + b_offset, ta->text.data + b_start,
1589 b_end - b_start);
1590
1591 /* Update next_detail */
1592 undo->details[undo->next_detail].b_start = b_offset;
1593 undo->details[undo->next_detail].b_end = b_offset + b_end - b_start;
1594 undo->details[undo->next_detail].b_limit = len;
1595
1596 undo->details[undo->next_detail].b_text_start = b_start;
1597
1598 return true;
1599 }
1600
1601
1602 /**
1603 * Replace text in a textarea, updating undo buffer.
1604 *
1605 * \param ta Textarea widget
1606 * \param b_start Start byte index of replaced section (inclusive)
1607 * \param b_end End byte index of replaced section (exclusive)
1608 * \param rep Replacement UTF-8 text to insert
1609 * \param rep_len Byte length of replacement UTF-8 text
1610 * \param add_to_clipboard True iff replaced text to be added to clipboard
1611 * \param byte_delta Updated to change in byte count in textarea (ta->show)
1612 * \param r Updated to area where redraw is required
1613 * \return false on memory exhaustion, true otherwise
1614 *
1615 * Note, b_start and b_end must be the byte offsets in ta->show, so in the
1616 * password textarea case, they are for ta->password.
1617 */
textarea_replace_text(struct textarea * ta,size_t b_start,size_t b_end,const char * rep,size_t rep_len,bool add_to_clipboard,int * byte_delta,struct rect * r)1618 static bool textarea_replace_text(struct textarea *ta, size_t b_start,
1619 size_t b_end, const char *rep, size_t rep_len,
1620 bool add_to_clipboard, int *byte_delta, struct rect *r)
1621 {
1622 if (!(b_start != b_end && rep == NULL && add_to_clipboard) &&
1623 !(ta->flags & TEXTAREA_PASSWORD)) {
1624 /* Not just copying to clipboard, and not a password field;
1625 * Sort out undo buffer. */
1626 if (textarea_copy_to_undo_buffer(ta, b_start, b_end,
1627 rep_len) == false)
1628 return false;
1629 }
1630
1631 /* Replace the text in the textarea, and reflow it */
1632 if (textarea_replace_text_internal(ta, b_start, b_end, rep, rep_len,
1633 add_to_clipboard, byte_delta, r) == false) {
1634 return false;
1635 }
1636
1637 if (!(b_start != b_end && rep == NULL && add_to_clipboard) &&
1638 !(ta->flags & TEXTAREA_PASSWORD)) {
1639 /* Not just copying to clipboard, and not a password field;
1640 * Update UNDO buffer */
1641 ta->undo.details[ta->undo.next_detail].b_text_end =
1642 b_end + *byte_delta;
1643 ta->undo.last_detail = ta->undo.next_detail;
1644 ta->undo.next_detail++;
1645 }
1646
1647 return true;
1648 }
1649
1650
1651 /**
1652 * Undo or redo previous change.
1653 *
1654 * \param ta Textarea widget
1655 * \param forward Iff true, redo, else undo
1656 * \param caret Updated to new caret pos in textarea (ta->show)
1657 * \param r Updated to area where redraw is required
1658 * \return false if nothing to undo/redo, true otherwise
1659 */
textarea_undo(struct textarea * ta,bool forward,unsigned int * caret,struct rect * r)1660 static bool textarea_undo(struct textarea *ta, bool forward,
1661 unsigned int *caret, struct rect *r)
1662 {
1663 unsigned int detail_n;
1664 struct textarea_undo_detail *detail;
1665 char *temp = NULL;
1666 unsigned int b_len;
1667 unsigned int b_text_len;
1668 int byte_delta;
1669
1670 if (ta->flags & TEXTAREA_PASSWORD || ta->flags & TEXTAREA_READONLY)
1671 /* No undo/redo for password or readonly fields */
1672 return false;
1673
1674 if (forward) {
1675 /* Redo */
1676 if (ta->undo.next_detail > ta->undo.last_detail)
1677 /* Nothing to redo */
1678 return false;
1679
1680 detail_n = ta->undo.next_detail;
1681 } else {
1682 /* Undo */
1683 if (ta->undo.next_detail == 0)
1684 /* Nothing to undo */
1685 return false;
1686
1687 detail_n = ta->undo.next_detail - 1;
1688 }
1689
1690 detail = &(ta->undo.details[detail_n]);
1691
1692 b_len = detail->b_end - detail->b_start;
1693 b_text_len = detail->b_text_end - detail->b_text_start;
1694
1695 /* Take copy of any current textarea text that undo/redo will remove */
1696 if (detail->b_text_end > detail->b_text_start) {
1697 temp = malloc(b_text_len);
1698 if (temp == NULL) {
1699 /* TODO */
1700 return false;
1701 }
1702
1703 memcpy(temp, ta->text.data + detail->b_text_start, b_text_len);
1704 }
1705
1706 /* Replace textarea text with undo buffer text */
1707 textarea_replace_text_internal(ta,
1708 detail->b_text_start, detail->b_text_end,
1709 ta->undo.text.data + detail->b_start, b_len,
1710 false, &byte_delta, r);
1711
1712 /* Update undo buffer for redo */
1713 if (temp != NULL)
1714 memcpy(ta->undo.text.data + detail->b_start, temp, b_text_len);
1715
1716 detail->b_text_end = detail->b_text_start + b_len;
1717 detail->b_end = detail->b_start + b_text_len;
1718
1719 *caret = detail->b_text_end;
1720
1721 if (forward) {
1722 /* Redo */
1723 ta->undo.next_detail++;
1724 } else {
1725 /* Undo */
1726 ta->undo.next_detail--;
1727 }
1728
1729 free(temp);
1730
1731 return true;
1732 }
1733
1734
1735 /**
1736 * Handles the end of a drag operation
1737 *
1738 * \param ta Text area
1739 * \param mouse the mouse state at drag end moment
1740 * \param x X coordinate
1741 * \param y Y coordinate
1742 * \return true if drag end was handled false otherwise
1743 */
textarea_drag_end(struct textarea * ta,browser_mouse_state mouse,int x,int y)1744 static bool textarea_drag_end(struct textarea *ta, browser_mouse_state mouse,
1745 int x, int y)
1746 {
1747 size_t b_end;
1748 struct textarea_msg msg;
1749
1750 assert(ta->drag_info.type != TEXTAREA_DRAG_NONE);
1751
1752 switch (ta->drag_info.type) {
1753 case TEXTAREA_DRAG_SCROLLBAR:
1754 if (ta->drag_info.data.scrollbar == ta->bar_x) {
1755 x -= ta->border_width;
1756 y -= ta->vis_height - ta->border_width -
1757 SCROLLBAR_WIDTH;
1758 } else {
1759 x -= ta->vis_width - ta->border_width -
1760 SCROLLBAR_WIDTH;
1761 y -= ta->border_width;
1762 }
1763 scrollbar_mouse_drag_end(ta->drag_info.data.scrollbar,
1764 mouse, x, y);
1765 assert(ta->drag_info.type == TEXTAREA_DRAG_NONE);
1766
1767 /* Return, since drag end already reported to textarea client */
1768 return true;
1769
1770 case TEXTAREA_DRAG_SELECTION:
1771 ta->drag_info.type = TEXTAREA_DRAG_NONE;
1772
1773 b_end = textarea_get_b_off_xy(ta, x, y, true);
1774
1775 if (!textarea_select(ta, ta->drag_start, b_end, false))
1776 return false;
1777
1778 break;
1779
1780 default:
1781 return false;
1782 }
1783
1784 /* Report drag end to client, if not already reported */
1785 assert(ta->drag_info.type == TEXTAREA_DRAG_NONE);
1786
1787 msg.ta = ta;
1788 msg.type = TEXTAREA_MSG_DRAG_REPORT;
1789 msg.data.drag = ta->drag_info.type;
1790
1791 ta->callback(ta->data, &msg);
1792
1793 return true;
1794 }
1795
1796
1797 /**
1798 * Setup text offsets after height / border / padding change
1799 *
1800 * \param ta Textarea widget
1801 */
textarea_setup_text_offsets(struct textarea * ta)1802 static void textarea_setup_text_offsets(struct textarea *ta)
1803 {
1804 int text_y_offset, text_y_offset_baseline;
1805
1806 ta->line_height = FIXTOINT(FMUL(FLTTOFIX(1.3), FDIV(FMUL(
1807 nscss_screen_dpi, FDIV(INTTOFIX(ta->fstyle.size),
1808 INTTOFIX(PLOT_STYLE_SCALE))), F_72)));
1809
1810 text_y_offset = text_y_offset_baseline = ta->border_width;
1811 if (ta->flags & TEXTAREA_MULTILINE) {
1812 /* Multiline textarea */
1813 text_y_offset += ta->pad_top;
1814 text_y_offset_baseline +=
1815 (ta->line_height * 3 + 2) / 4 + ta->pad_top;
1816 } else {
1817 /* Single line text area; text is vertically centered */
1818 int vis_height = ta->vis_height - 2 * ta->border_width;
1819 text_y_offset += (vis_height - ta->line_height + 1) / 2;
1820 text_y_offset_baseline +=
1821 (2 * vis_height + ta->line_height + 2) / 4;
1822 }
1823
1824 ta->text_y_offset = text_y_offset;
1825 ta->text_y_offset_baseline = text_y_offset_baseline;
1826 }
1827
1828
1829 /**
1830 * Set font styles up for a textarea.
1831 *
1832 * \param[in] ta Textarea to update.
1833 * \param[in] fstyle Font style to set in textarea.
1834 * \param[in] selected_text Textarea selected text colour.
1835 * \param[in] selected_bg Textarea selection background colour.
1836 */
textarea_set_text_style(struct textarea * ta,const plot_font_style_t * fstyle,colour selected_text,colour selected_bg)1837 static void textarea_set_text_style(
1838 struct textarea *ta,
1839 const plot_font_style_t *fstyle,
1840 colour selected_text,
1841 colour selected_bg)
1842 {
1843 ta->fstyle = *fstyle;
1844
1845 ta->sel_fstyle = *fstyle;
1846 ta->sel_fstyle.foreground = selected_text;
1847 ta->sel_fstyle.background = selected_bg;
1848 }
1849
1850
1851 /* exported interface, documented in textarea.h */
textarea_create(const textarea_flags flags,const textarea_setup * setup,textarea_client_callback callback,void * data)1852 struct textarea *textarea_create(const textarea_flags flags,
1853 const textarea_setup *setup,
1854 textarea_client_callback callback, void *data)
1855 {
1856 struct textarea *ret;
1857 struct rect r = {0, 0, 0, 0};
1858
1859 /* Sanity check flags */
1860 assert(!(flags & TEXTAREA_MULTILINE &&
1861 flags & TEXTAREA_PASSWORD));
1862
1863 if (callback == NULL) {
1864 NSLOG(netsurf, INFO, "no callback provided");
1865 return NULL;
1866 }
1867
1868 ret = malloc(sizeof(struct textarea));
1869 if (ret == NULL) {
1870 NSLOG(netsurf, INFO, "malloc failed");
1871 return NULL;
1872 }
1873
1874 ret->callback = callback;
1875 ret->data = data;
1876
1877 ret->flags = flags;
1878 ret->vis_width = setup->width;
1879 ret->vis_height = setup->height;
1880
1881 ret->pad_top = setup->pad_top;
1882 ret->pad_right = setup->pad_right;
1883 ret->pad_bottom = setup->pad_bottom;
1884 ret->pad_left = setup->pad_left;
1885
1886 ret->border_width = setup->border_width;
1887 ret->border_col = setup->border_col;
1888
1889 textarea_set_text_style(ret,
1890 &setup->text,
1891 setup->selected_text,
1892 setup->selected_bg);
1893
1894 ret->scroll_x = 0;
1895 ret->scroll_y = 0;
1896 ret->bar_x = NULL;
1897 ret->bar_y = NULL;
1898 ret->h_extent = setup->width;
1899 ret->v_extent = setup->height;
1900 ret->drag_start = 0;
1901 ret->drag_info.type = TEXTAREA_DRAG_NONE;
1902
1903 ret->undo.details_alloc = 0;
1904 ret->undo.next_detail = 0;
1905 ret->undo.last_detail = 0;
1906 ret->undo.details = NULL;
1907
1908 ret->undo.text.data = NULL;
1909 ret->undo.text.alloc = 0;
1910 ret->undo.text.len = 0;
1911 ret->undo.text.utf8_len = 0;
1912
1913
1914 ret->text.data = malloc(TA_ALLOC_STEP);
1915 if (ret->text.data == NULL) {
1916 NSLOG(netsurf, INFO, "malloc failed");
1917 free(ret);
1918 return NULL;
1919 }
1920 ret->text.data[0] = '\0';
1921 ret->text.alloc = TA_ALLOC_STEP;
1922 ret->text.len = 1;
1923 ret->text.utf8_len = 0;
1924
1925 if (flags & TEXTAREA_PASSWORD) {
1926 ret->password.data = malloc(TA_ALLOC_STEP);
1927 if (ret->password.data == NULL) {
1928 NSLOG(netsurf, INFO, "malloc failed");
1929 free(ret->text.data);
1930 free(ret);
1931 return NULL;
1932 }
1933 ret->password.data[0] = '\0';
1934 ret->password.alloc = TA_ALLOC_STEP;
1935 ret->password.len = 1;
1936 ret->password.utf8_len = 0;
1937
1938 ret->show = &ret->password;
1939
1940 } else {
1941 ret->password.data = NULL;
1942 ret->password.alloc = 0;
1943 ret->password.len = 0;
1944 ret->password.utf8_len = 0;
1945
1946 ret->show = &ret->text;
1947 }
1948
1949 ret->line_height = FIXTOINT(FMUL(FLTTOFIX(1.3), FDIV(FMUL(
1950 nscss_screen_dpi, FDIV(INTTOFIX(setup->text.size),
1951 INTTOFIX(PLOT_STYLE_SCALE))), F_72)));
1952
1953 ret->caret_pos.line = ret->caret_pos.byte_off = -1;
1954 ret->caret_x = 0;
1955 ret->caret_y = 0;
1956 ret->sel_start = -1;
1957 ret->sel_end = -1;
1958
1959 ret->line_count = 0;
1960 ret->lines = NULL;
1961 ret->lines_alloc_size = 0;
1962
1963 textarea_setup_text_offsets(ret);
1964
1965 if (flags & TEXTAREA_MULTILINE)
1966 textarea_reflow_multiline(ret, 0, 0, &r);
1967 else
1968 textarea_reflow_singleline(ret, 0, &r);
1969
1970 return ret;
1971 }
1972
1973
1974 /* exported interface, documented in textarea.h */
textarea_destroy(struct textarea * ta)1975 void textarea_destroy(struct textarea *ta)
1976 {
1977 if (ta->bar_x)
1978 scrollbar_destroy(ta->bar_x);
1979 if (ta->bar_y)
1980 scrollbar_destroy(ta->bar_y);
1981
1982 if (ta->flags & TEXTAREA_PASSWORD)
1983 free(ta->password.data);
1984
1985 free(ta->undo.text.data);
1986 free(ta->undo.details);
1987
1988 free(ta->text.data);
1989 free(ta->lines);
1990 free(ta);
1991 }
1992
1993
1994 /* exported interface, documented in textarea.h */
textarea_set_text(struct textarea * ta,const char * text)1995 bool textarea_set_text(struct textarea *ta, const char *text)
1996 {
1997 unsigned int len = strlen(text) + 1;
1998 struct rect r = {0, 0, 0, 0};
1999
2000 if (len >= ta->text.alloc) {
2001 char *temp = realloc(ta->text.data, len + TA_ALLOC_STEP);
2002 if (temp == NULL) {
2003 NSLOG(netsurf, INFO, "realloc failed");
2004 return false;
2005 }
2006 ta->text.data = temp;
2007 ta->text.alloc = len + TA_ALLOC_STEP;
2008 }
2009
2010 memcpy(ta->text.data, text, len);
2011 ta->text.len = len;
2012 ta->text.utf8_len = utf8_length(ta->text.data);
2013
2014 ta->undo.next_detail = 0;
2015 ta->undo.last_detail = 0;
2016
2017 textarea_normalise_text(ta, 0, len);
2018
2019 if (ta->flags & TEXTAREA_MULTILINE) {
2020 if (!textarea_reflow_multiline(ta, 0, len - 1, &r))
2021 return false;
2022 } else {
2023 if (!textarea_reflow_singleline(ta, 0, &r))
2024 return false;
2025 }
2026
2027 return true;
2028 }
2029
2030
2031 /* exported interface, documented in textarea.h */
textarea_drop_text(struct textarea * ta,const char * text,size_t text_length)2032 bool textarea_drop_text(struct textarea *ta, const char *text,
2033 size_t text_length)
2034 {
2035 struct textarea_msg msg;
2036 struct rect r; /**< Redraw rectangle */
2037 unsigned int caret_pos;
2038 int byte_delta;
2039
2040 if (ta->flags & TEXTAREA_READONLY)
2041 return false;
2042
2043 if (text == NULL)
2044 return false;
2045
2046 caret_pos = textarea_get_caret(ta);
2047
2048 if (ta->sel_start != -1) {
2049 if (!textarea_replace_text(ta, ta->sel_start, ta->sel_end,
2050 text, text_length, false, &byte_delta, &r))
2051 return false;
2052
2053 caret_pos = ta->sel_end;
2054 ta->sel_start = ta->sel_end = -1;
2055 } else {
2056 if (!textarea_replace_text(ta, caret_pos, caret_pos,
2057 text, text_length, false, &byte_delta, &r))
2058 return false;
2059 }
2060
2061 caret_pos += byte_delta;
2062 textarea_set_caret_internal(ta, caret_pos);
2063
2064 msg.ta = ta;
2065 msg.type = TEXTAREA_MSG_REDRAW_REQUEST;
2066 msg.data.redraw.x0 = 0;
2067 msg.data.redraw.y0 = 0;
2068 msg.data.redraw.x1 = ta->vis_width;
2069 msg.data.redraw.y1 = ta->vis_height;
2070
2071 ta->callback(ta->data, &msg);
2072
2073 return true;
2074 }
2075
2076
2077 /* exported interface, documented in textarea.h */
textarea_get_text(struct textarea * ta,char * buf,unsigned int len)2078 int textarea_get_text(struct textarea *ta, char *buf, unsigned int len)
2079 {
2080 if (buf == NULL && len == 0) {
2081 /* want length */
2082 return ta->text.len;
2083
2084 } else if (buf == NULL) {
2085 /* Can't write to NULL */
2086 return -1;
2087 }
2088
2089 if (len < ta->text.len) {
2090 NSLOG(netsurf, INFO, "buffer too small");
2091 return -1;
2092 }
2093
2094 memcpy(buf, ta->text.data, ta->text.len);
2095
2096 return ta->text.len;
2097 }
2098
2099
2100 /* exported interface, documented in textarea.h */
textarea_data(struct textarea * ta,unsigned int * len)2101 const char * textarea_data(struct textarea *ta, unsigned int *len)
2102 {
2103 if (len != NULL) {
2104 *len = ta->text.len;
2105 }
2106
2107 return ta->text.data;
2108 }
2109
2110
2111 /* exported interface, documented in textarea.h */
textarea_set_caret(struct textarea * ta,int caret)2112 bool textarea_set_caret(struct textarea *ta, int caret)
2113 {
2114 int b_off;
2115
2116 if (caret < 0) {
2117 textarea_set_caret_internal(ta, -1);
2118 } else if (caret == 0) {
2119 textarea_set_caret_internal(ta, 0);
2120 } else {
2121 b_off = utf8_bounded_byte_length(ta->show->data,
2122 ta->show->len - 1, caret);
2123 textarea_set_caret_internal(ta, b_off);
2124 }
2125
2126 return true;
2127 }
2128
2129
2130 /* exported interface, documented in textarea.h */
textarea_redraw(struct textarea * ta,int x,int y,colour bg,float scale,const struct rect * clip,const struct redraw_context * ctx)2131 void textarea_redraw(struct textarea *ta, int x, int y, colour bg, float scale,
2132 const struct rect *clip, const struct redraw_context *ctx)
2133 {
2134 int line0, line1, line, left, right, line_y;
2135 int text_y_offset, text_y_offset_baseline;
2136 unsigned int b_pos, b_len, b_len_part, b_end;
2137 unsigned int sel_start, sel_end;
2138 char *line_text;
2139 struct rect r, s;
2140 struct rect rect;
2141 bool selected = false;
2142 plot_font_style_t fstyle;
2143 int fsize = ta->fstyle.size;
2144 int line_height = ta->line_height;
2145 plot_style_t plot_style_fill_bg = {
2146 .stroke_type = PLOT_OP_TYPE_NONE,
2147 .stroke_width = 0,
2148 .stroke_colour = NS_TRANSPARENT,
2149 .fill_type = PLOT_OP_TYPE_SOLID,
2150 .fill_colour = ta->border_col
2151 };
2152
2153 r = *clip;
2154
2155 /* Nothing to render if textarea is outside clip rectangle */
2156 if (r.x1 < x || r.y1 < y)
2157 return;
2158 if (scale == 1.0) {
2159 if (r.x0 > x + ta->vis_width || r.y0 > y + ta->vis_height)
2160 return;
2161 } else {
2162 if (r.x0 > x + ta->vis_width * scale ||
2163 r.y0 > y + ta->vis_height * scale)
2164 return;
2165 }
2166
2167 if (ta->lines == NULL)
2168 /* Nothing to redraw */
2169 return;
2170
2171 line0 = (r.y0 - y + ta->scroll_y) / ta->line_height - 1;
2172 line1 = (r.y1 - y + ta->scroll_y) / ta->line_height + 1;
2173
2174 if (line0 < 0)
2175 line0 = 0;
2176 if (line1 < 0)
2177 line1 = 0;
2178 if (ta->line_count - 1 < line0)
2179 line0 = ta->line_count - 1;
2180 if (ta->line_count - 1 < line1)
2181 line1 = ta->line_count - 1;
2182 if (line1 < line0)
2183 line1 = line0;
2184
2185 if (r.x0 < x)
2186 r.x0 = x;
2187 if (r.y0 < y)
2188 r.y0 = y;
2189 if (scale == 1.0) {
2190 if (r.x1 > x + ta->vis_width)
2191 r.x1 = x + ta->vis_width;
2192 if (r.y1 > y + ta->vis_height)
2193 r.y1 = y + ta->vis_height;
2194 } else {
2195 if (r.x1 > x + ta->vis_width * scale)
2196 r.x1 = x + ta->vis_width * scale;
2197 if (r.y1 > y + ta->vis_height * scale)
2198 r.y1 = y + ta->vis_height * scale;
2199 }
2200
2201 ctx->plot->clip(ctx, &r);
2202 if (ta->border_col != NS_TRANSPARENT &&
2203 ta->border_width > 0) {
2204 /* Plot border */
2205 rect.x0 = x;
2206 rect.y0 = y;
2207 rect.x1 = x + ta->vis_width;
2208 rect.y1 = y + ta->vis_height;
2209 ctx->plot->rectangle(ctx, &plot_style_fill_bg, &rect);
2210 }
2211 if (ta->fstyle.background != NS_TRANSPARENT) {
2212 /* Plot background */
2213 plot_style_fill_bg.fill_colour = ta->fstyle.background;
2214 rect.x0 = x + ta->border_width;
2215 rect.y0 = y + ta->border_width;
2216 rect.x1 = x + ta->vis_width - ta->border_width;
2217 rect.y1 = y + ta->vis_height - ta->border_width;
2218 ctx->plot->rectangle(ctx, &plot_style_fill_bg, &rect);
2219 }
2220
2221 if (scale == 1.0) {
2222 if (r.x0 < x + ta->border_width)
2223 r.x0 = x + ta->border_width;
2224 if (r.x1 > x + ta->vis_width - ta->border_width)
2225 r.x1 = x + ta->vis_width - ta->border_width;
2226 if (r.y0 < y + ta->border_width)
2227 r.y0 = y + ta->border_width;
2228 if (r.y1 > y + ta->vis_height - ta->border_width -
2229 (ta->bar_x != NULL ? SCROLLBAR_WIDTH : 0))
2230 r.y1 = y + ta->vis_height - ta->border_width -
2231 (ta->bar_x != NULL ? SCROLLBAR_WIDTH :
2232 0);
2233 } else {
2234 if (r.x0 < x + ta->border_width * scale)
2235 r.x0 = x + ta->border_width * scale;
2236 if (r.x1 > x + (ta->vis_width - ta->border_width) * scale)
2237 r.x1 = x + (ta->vis_width - ta->border_width) * scale;
2238 if (r.y0 < y + ta->border_width * scale)
2239 r.y0 = y + ta->border_width * scale;
2240 if (r.y1 > y + (ta->vis_height - ta->border_width -
2241 (ta->bar_x != NULL ? SCROLLBAR_WIDTH : 0)) *
2242 scale)
2243 r.y1 = y + (ta->vis_height - ta->border_width -
2244 (ta->bar_x != NULL ? SCROLLBAR_WIDTH :
2245 0)) * scale;
2246 }
2247
2248 if (line0 > 0)
2249 b_pos = ta->lines[line0].b_start;
2250 else
2251 b_pos = 0;
2252
2253 text_y_offset = ta->text_y_offset;
2254 text_y_offset_baseline = ta->text_y_offset_baseline;
2255
2256 if (scale != 1.0) {
2257 text_y_offset *= scale;
2258 text_y_offset_baseline *= scale;
2259
2260 fsize *= scale;
2261 line_height *= scale;
2262 }
2263
2264 plot_style_fill_bg.fill_colour = ta->sel_fstyle.background;
2265
2266 for (line = line0;
2267 (line <= line1) && (y + line * ta->line_height <= r.y1 + ta->scroll_y);
2268 line++) {
2269 if (ta->lines[line].b_length == 0) {
2270 b_pos++;
2271 continue;
2272 }
2273
2274 /* reset clip rectangle */
2275 ctx->plot->clip(ctx, &r);
2276
2277 b_len = ta->lines[line].b_length;
2278
2279 b_end = 0;
2280 right = x + ta->border_width + ta->pad_left - ta->scroll_x;
2281
2282 line_y = line * ta->line_height - ta->scroll_y;
2283
2284 if (scale != 1.0) {
2285 line_y *= scale;
2286 }
2287
2288 sel_start = ta->sel_start;
2289 sel_end = ta->sel_end;
2290
2291 if (ta->sel_end == -1 || ta->sel_end == ta->sel_start ||
2292 sel_end < ta->lines[line].b_start ||
2293 sel_start > ta->lines[line].b_start +
2294 ta->lines[line].b_length) {
2295 /* Simple case; no selection on this line */
2296 fstyle = ta->fstyle;
2297 fstyle.size = fsize;
2298
2299 ctx->plot->text(ctx,
2300 &fstyle,
2301 x + ta->border_width + ta->pad_left - ta->scroll_x,
2302 y + line_y + text_y_offset_baseline,
2303 ta->show->data + ta->lines[line].b_start,
2304 ta->lines[line].b_length);
2305
2306 b_pos += b_len;
2307
2308 } else do {
2309 /* get length of part of line */
2310 if (sel_end <= b_pos || sel_start > b_pos + b_len) {
2311 /* rest of line unselected */
2312 selected = false;
2313 b_len_part = b_len;
2314 fstyle = ta->fstyle;
2315
2316 } else if (sel_start <= b_pos &&
2317 sel_end > b_pos + b_len) {
2318 /* rest of line selected */
2319 selected = true;
2320 b_len_part = b_len;
2321 fstyle = ta->sel_fstyle;
2322
2323 } else if (sel_start > b_pos) {
2324 /* next part of line unselected */
2325 selected = false;
2326 b_len_part = sel_start - b_pos;
2327 fstyle = ta->fstyle;
2328
2329 } else if (sel_end > b_pos) {
2330 /* next part of line selected */
2331 selected = true;
2332 b_len_part = sel_end - b_pos;
2333 fstyle = ta->sel_fstyle;
2334
2335 } else {
2336 assert(0);
2337 }
2338 fstyle.size = fsize;
2339
2340 line_text = &(ta->show->data[ta->lines[line].b_start]);
2341
2342 /* find b_end for this part of the line */
2343 b_end += b_len_part;
2344
2345 /* find clip left/right for this part of line */
2346 left = right;
2347 if (b_len_part != b_len) {
2348 guit->layout->width(&fstyle, line_text, b_end,
2349 &right);
2350 } else {
2351 right = ta->lines[line].width;
2352 if (scale != 1.0)
2353 right *= scale;
2354 }
2355 right += x + ta->border_width + ta->pad_left -
2356 ta->scroll_x;
2357
2358 /* set clip rectangle for line part */
2359 s = r;
2360
2361 if (s.x1 <= left || s.x0 > right) {
2362 /* Skip this span, it's outside the visible */
2363 b_pos += b_len_part;
2364 b_len -= b_len_part;
2365 continue;
2366 }
2367
2368 /* Adjust clip rectangle to span limits */
2369 if (s.x0 < left)
2370 s.x0 = left;
2371 if (s.x1 > right)
2372 s.x1 = right;
2373
2374 if (right <= left) {
2375 /* Skip this span, it's outside the visible */
2376 b_pos += b_len_part;
2377 b_len -= b_len_part;
2378 continue;
2379 }
2380
2381 ctx->plot->clip(ctx, &s);
2382
2383 if (selected) {
2384 /* draw selection fill */
2385 rect.x0 = s.x0;
2386 rect.y0 = y + line_y + text_y_offset;
2387 rect.x1 = s.x1;
2388 rect.y1 = y + line_y + line_height + text_y_offset;
2389 ctx->plot->rectangle(ctx, &plot_style_fill_bg, &rect);
2390 }
2391
2392 /* draw text */
2393 ctx->plot->text(ctx,
2394 &fstyle,
2395 x + ta->border_width + ta->pad_left - ta->scroll_x,
2396 y + line_y + text_y_offset_baseline,
2397 ta->show->data + ta->lines[line].b_start,
2398 ta->lines[line].b_length);
2399
2400 b_pos += b_len_part;
2401 b_len -= b_len_part;
2402
2403 } while (b_pos < b_pos + b_len);
2404
2405 /* if there is a newline between the lines, skip it */
2406 if (line < ta->line_count - 1 &&
2407 ta->lines[line + 1].b_start !=
2408 ta->lines[line].b_start +
2409 ta->lines[line].b_length)
2410 b_pos++;
2411 }
2412
2413 if (ta->flags & TEXTAREA_INTERNAL_CARET &&
2414 (ta->sel_end == -1 || ta->sel_start == ta->sel_end) &&
2415 ta->caret_pos.byte_off >= 0) {
2416 /* No native caret, there is no selection, and caret visible */
2417 int caret_y = y - ta->scroll_y + ta->caret_y;
2418
2419 ctx->plot->clip(ctx, &r);
2420
2421 /* Render our own caret */
2422 rect.x0 = x - ta->scroll_x + ta->caret_x;
2423 rect.y0 = caret_y;
2424 rect.x1 = x - ta->scroll_x + ta->caret_x;
2425 rect.y1 = caret_y + ta->line_height;
2426 ctx->plot->line(ctx, &pstyle_stroke_caret, &rect);
2427 }
2428
2429 ctx->plot->clip(ctx, clip);
2430
2431 if (ta->bar_x != NULL) {
2432 scrollbar_redraw(ta->bar_x,
2433 x / scale + ta->border_width,
2434 y / scale + ta->vis_height - ta->border_width -
2435 SCROLLBAR_WIDTH,
2436 clip, scale, ctx);
2437 }
2438
2439 if (ta->bar_y != NULL) {
2440 scrollbar_redraw(ta->bar_y,
2441 x / scale + ta->vis_width - ta->border_width -
2442 SCROLLBAR_WIDTH,
2443 y / scale + ta->border_width,
2444 clip, scale, ctx);
2445 }
2446 }
2447
2448
2449 /* exported interface, documented in textarea.h */
textarea_keypress(struct textarea * ta,uint32_t key)2450 bool textarea_keypress(struct textarea *ta, uint32_t key)
2451 {
2452 struct textarea_msg msg;
2453 struct rect r; /**< Redraw rectangle */
2454 char utf8[6];
2455 unsigned int caret, length, b_off, b_len;
2456 int h_extent = ta->h_extent;
2457 int v_extent = ta->v_extent;
2458 int line;
2459 int byte_delta = 0;
2460 int x, y;
2461 bool redraw = false;
2462 bool readonly;
2463 bool bar_x = ta->bar_x;
2464 bool bar_y = ta->bar_y;
2465
2466 /* Word separators */
2467 static const char *sep = " .\n";
2468
2469 caret = textarea_get_caret(ta);
2470 line = ta->caret_pos.line;
2471 readonly = (ta->flags & TEXTAREA_READONLY ? true : false);
2472
2473 if (!(key <= 0x001F || (0x007F <= key && key <= 0x009F))) {
2474 /* normal character insertion */
2475 length = utf8_from_ucs4(key, utf8);
2476 utf8[length] = '\0';
2477
2478 if (ta->sel_start != -1) {
2479 if (!textarea_replace_text(ta,
2480 ta->sel_start, ta->sel_end, utf8,
2481 length, false, &byte_delta, &r))
2482 return false;
2483
2484 redraw = true;
2485 caret = ta->sel_end;
2486 textarea_reset_selection(ta);
2487 } else {
2488 if (!textarea_replace_text(ta, caret, caret,
2489 utf8, length, false, &byte_delta, &r))
2490 return false;
2491 redraw = true;
2492 }
2493 caret += byte_delta;
2494
2495 } else switch (key) {
2496 case NS_KEY_SELECT_ALL:
2497 textarea_select(ta, 0, ta->show->len - 1, true);
2498 return true;
2499 case NS_KEY_COPY_SELECTION:
2500 if (ta->sel_start != -1) {
2501 if (!textarea_replace_text(ta,
2502 ta->sel_start, ta->sel_end,
2503 NULL, 0, true, &byte_delta, &r))
2504 return false;
2505 }
2506 break;
2507 case NS_KEY_DELETE_LEFT:
2508 if (readonly)
2509 break;
2510 if (ta->sel_start != -1) {
2511 if (!textarea_replace_text(ta,
2512 ta->sel_start, ta->sel_end,
2513 "", 0, false, &byte_delta, &r))
2514 return false;
2515
2516 redraw = true;
2517 caret = ta->sel_end;
2518 textarea_reset_selection(ta);
2519 } else if (caret > 0) {
2520 b_off = utf8_prev(ta->show->data, caret);
2521 if (!textarea_replace_text(ta, b_off, caret,
2522 "", 0, false, &byte_delta, &r))
2523 return false;
2524 redraw = true;
2525 }
2526 caret += byte_delta;
2527 break;
2528 case NS_KEY_DELETE_RIGHT:
2529 if (readonly)
2530 break;
2531 if (ta->sel_start != -1) {
2532 if (!textarea_replace_text(ta,
2533 ta->sel_start, ta->sel_end,
2534 "", 0, false, &byte_delta, &r))
2535 return false;
2536
2537 redraw = true;
2538 caret = ta->sel_end;
2539 textarea_reset_selection(ta);
2540 } else if (caret < ta->show->len - 1) {
2541 b_off = utf8_next(ta->show->data,
2542 ta->show->len - 1, caret);
2543 if (!textarea_replace_text(ta, caret, b_off,
2544 "", 0, false, &byte_delta, &r))
2545 return false;
2546 caret = b_off;
2547 redraw = true;
2548 }
2549 caret += byte_delta;
2550 break;
2551 case NS_KEY_CR:
2552 case NS_KEY_NL:
2553 if (readonly)
2554 break;
2555
2556 if (ta->sel_start != -1) {
2557 if (!textarea_replace_text(ta,
2558 ta->sel_start, ta->sel_end,
2559 "\n", 1, false,
2560 &byte_delta, &r))
2561 return false;
2562
2563 redraw = true;
2564 caret = ta->sel_end;
2565 textarea_reset_selection(ta);
2566 } else {
2567 if (!textarea_replace_text(ta, caret, caret,
2568 "\n", 1, false,
2569 &byte_delta, &r))
2570 return false;
2571 redraw = true;
2572 }
2573 caret += byte_delta;
2574 break;
2575 case NS_KEY_PASTE:
2576 {
2577 char *clipboard = NULL;
2578 size_t clipboard_length;
2579
2580 if (readonly)
2581 break;
2582
2583 guit->clipboard->get(&clipboard, &clipboard_length);
2584 if (clipboard == NULL)
2585 return false;
2586
2587 if (ta->sel_start != -1) {
2588 if (!textarea_replace_text(ta,
2589 ta->sel_start, ta->sel_end,
2590 clipboard, clipboard_length,
2591 false, &byte_delta, &r))
2592 return false;
2593
2594 redraw = true;
2595 caret = ta->sel_end;
2596 textarea_reset_selection(ta);
2597 } else {
2598 if (!textarea_replace_text(ta,
2599 caret, caret,
2600 clipboard, clipboard_length,
2601 false, &byte_delta, &r))
2602 return false;
2603 redraw = true;
2604 }
2605 caret += byte_delta;
2606
2607 free(clipboard);
2608 }
2609 break;
2610 case NS_KEY_CUT_SELECTION:
2611 if (readonly)
2612 break;
2613 if (ta->sel_start != -1) {
2614 if (!textarea_replace_text(ta,
2615 ta->sel_start, ta->sel_end,
2616 "", 0, true, &byte_delta, &r))
2617 return false;
2618
2619 redraw = true;
2620 caret = ta->sel_end;
2621 caret += byte_delta;
2622 textarea_reset_selection(ta);
2623 }
2624 break;
2625 case NS_KEY_ESCAPE:
2626 /* Fall through to NS_KEY_CLEAR_SELECTION */
2627 case NS_KEY_CLEAR_SELECTION:
2628 return textarea_clear_selection(ta);
2629 case NS_KEY_LEFT:
2630 if (readonly)
2631 break;
2632 if (caret > 0)
2633 caret = utf8_prev(ta->show->data, caret);
2634 if (ta->sel_start != -1) {
2635 textarea_clear_selection(ta);
2636 }
2637 break;
2638 case NS_KEY_RIGHT:
2639 if (readonly)
2640 break;
2641 if (caret < ta->show->len - 1)
2642 caret = utf8_next(ta->show->data,
2643 ta->show->len - 1, caret);
2644 if (ta->sel_start != -1) {
2645 textarea_clear_selection(ta);
2646 }
2647 break;
2648 case NS_KEY_UP:
2649 if (readonly)
2650 break;
2651 if (ta->sel_start != -1) {
2652 textarea_clear_selection(ta);
2653 }
2654 if (!(ta->flags & TEXTAREA_MULTILINE))
2655 break;
2656
2657 line--;
2658 if (line < 0)
2659 line = 0;
2660 if (line == ta->caret_pos.line)
2661 break;
2662
2663 x = ta->caret_x;
2664 y = ta->text_y_offset_baseline + line * ta->line_height;
2665 textarea_set_caret_xy(ta, x, y, false);
2666
2667 return true;
2668 case NS_KEY_DOWN:
2669 if (readonly)
2670 break;
2671 if (ta->sel_start != -1) {
2672 textarea_clear_selection(ta);
2673 }
2674 if (!(ta->flags & TEXTAREA_MULTILINE))
2675 break;
2676
2677 line++;
2678 if (line > ta->line_count - 1)
2679 line = ta->line_count - 1;
2680 if (line == ta->caret_pos.line)
2681 break;
2682
2683 x = ta->caret_x;
2684 y = ta->text_y_offset_baseline + line * ta->line_height;
2685 textarea_set_caret_xy(ta, x, y, false);
2686
2687 return true;
2688 case NS_KEY_PAGE_UP:
2689 if (!(ta->flags & TEXTAREA_MULTILINE))
2690 break;
2691 y = ta->vis_height - 2 * ta->border_width -
2692 ta->pad_top - ta->pad_bottom -
2693 ta->line_height;
2694 textarea_scroll(ta, 0, -y);
2695 return true;
2696 case NS_KEY_PAGE_DOWN:
2697 if (!(ta->flags & TEXTAREA_MULTILINE))
2698 break;
2699 y = ta->vis_height - 2 * ta->border_width -
2700 ta->pad_top - ta->pad_bottom -
2701 ta->line_height;
2702 textarea_scroll(ta, 0, y);
2703 return true;
2704 case NS_KEY_LINE_START:
2705 if (readonly)
2706 break;
2707 caret -= ta->caret_pos.byte_off;
2708 if (ta->sel_start != -1) {
2709 textarea_clear_selection(ta);
2710 }
2711 break;
2712 case NS_KEY_LINE_END:
2713 if (readonly)
2714 break;
2715
2716 caret = ta->lines[line].b_start +
2717 ta->lines[line].b_length;
2718
2719 if (!(ta->flags & TEXTAREA_PASSWORD) &&
2720 caret > 0 &&
2721 ta->text.data[caret - 1] == ' ')
2722 caret--;
2723 if (ta->sel_start != -1) {
2724 textarea_clear_selection(ta);
2725 }
2726 break;
2727 case NS_KEY_TEXT_START:
2728 if (readonly)
2729 break;
2730 caret = 0;
2731 if (ta->sel_start != -1) {
2732 textarea_clear_selection(ta);
2733 }
2734 break;
2735 case NS_KEY_TEXT_END:
2736 if (readonly)
2737 break;
2738 caret = ta->show->len - 1;
2739 if (ta->sel_start != -1) {
2740 textarea_clear_selection(ta);
2741 }
2742 break;
2743 case NS_KEY_WORD_LEFT:
2744 if (readonly)
2745 break;
2746 if (caret == 0)
2747 break;
2748 caret--;
2749 while (strchr(sep, ta->show->data[caret]) != NULL &&
2750 caret > 0)
2751 caret--;
2752 for (; caret > 0; caret--) {
2753 if (strchr(sep, ta->show->data[caret])
2754 != NULL) {
2755 caret++;
2756 break;
2757 }
2758 }
2759 if (ta->sel_start != -1) {
2760 textarea_clear_selection(ta);
2761 }
2762 break;
2763 case NS_KEY_WORD_RIGHT:
2764 if (readonly)
2765 break;
2766 if (strchr(sep, ta->show->data[caret]) != NULL &&
2767 caret < ta->show->len - 1) {
2768 while (strchr(sep, ta->show->data[caret]) !=
2769 NULL &&
2770 caret < ta->show->len - 1) {
2771 caret++;
2772 }
2773 break;
2774 }
2775 for (; caret < ta->show->len - 1; caret++) {
2776 if (strchr(sep, ta->show->data[caret]) != NULL)
2777 break;
2778 }
2779 while (strchr(sep, ta->show->data[caret]) != NULL &&
2780 caret < ta->show->len - 1)
2781 caret++;
2782 if (ta->sel_start != -1) {
2783 textarea_clear_selection(ta);
2784 }
2785 break;
2786 case NS_KEY_DELETE_LINE:
2787 if (readonly)
2788 break;
2789 if (ta->sel_start != -1) {
2790 if (!textarea_replace_text(ta,
2791 ta->sel_start, ta->sel_end,
2792 "", 0, false, &byte_delta, &r))
2793 return false;
2794 redraw = true;
2795 caret = ta->sel_end;
2796 textarea_reset_selection(ta);
2797 } else {
2798 if (ta->lines[line].b_length != 0) {
2799 /* Delete line */
2800 caret = ta->lines[line].b_start;
2801 b_len = ta->lines[line].b_length;
2802 if (!textarea_replace_text(ta, caret,
2803 caret + b_len, "", 0,
2804 false, &byte_delta, &r))
2805 return false;
2806 caret = caret + b_len;
2807 } else if (caret < ta->show->len - 1) {
2808 /* Delete blank line */
2809 if (!textarea_replace_text(ta,
2810 caret, caret + 1, "", 0,
2811 false, &byte_delta, &r))
2812 return false;
2813 caret++;
2814 }
2815 redraw = true;
2816 }
2817 caret += byte_delta;
2818 break;
2819 case NS_KEY_DELETE_LINE_END:
2820 if (readonly)
2821 break;
2822 if (ta->sel_start != -1) {
2823 if (!textarea_replace_text(ta,
2824 ta->sel_start, ta->sel_end,
2825 "", 0, false, &byte_delta, &r))
2826 return false;
2827 redraw = true;
2828 caret = ta->sel_end;
2829 textarea_reset_selection(ta);
2830 } else {
2831 b_len = ta->lines[line].b_length;
2832 b_off = ta->lines[line].b_start + b_len;
2833 if (!textarea_replace_text(ta, caret, b_off,
2834 "", 0, false, &byte_delta, &r))
2835 return false;
2836 caret = b_off;
2837 redraw = true;
2838 }
2839 caret += byte_delta;
2840 break;
2841 case NS_KEY_DELETE_LINE_START:
2842 if (readonly)
2843 break;
2844 if (ta->sel_start != -1) {
2845 if (!textarea_replace_text(ta,
2846 ta->sel_start, ta->sel_end,
2847 "", 0, false, &byte_delta, &r))
2848 return false;
2849 redraw = true;
2850 caret = ta->sel_end;
2851 textarea_reset_selection(ta);
2852 } else {
2853 if (!textarea_replace_text(ta,
2854 caret - ta->caret_pos.byte_off,
2855 caret, "", 0, false,
2856 &byte_delta, &r))
2857 return false;
2858 redraw = true;
2859 }
2860 caret += byte_delta;
2861 break;
2862 case NS_KEY_UNDO:
2863 if (!textarea_undo(ta, false, &caret, &r)) {
2864 /* We consume the UNDO, even if we can't act
2865 * on it. */
2866 return true;
2867 }
2868 if (ta->sel_start != -1) {
2869 textarea_clear_selection(ta);
2870 }
2871 redraw = true;
2872 break;
2873 case NS_KEY_REDO:
2874 if (!textarea_undo(ta, true, &caret, &r)) {
2875 /* We consume the REDO, even if we can't act
2876 * on it. */
2877 return true;
2878 }
2879 if (ta->sel_start != -1) {
2880 textarea_clear_selection(ta);
2881 }
2882 redraw = true;
2883 break;
2884 default:
2885 return false;
2886 }
2887
2888 redraw &= !textarea_set_caret_internal(ta, caret);
2889
2890 /* TODO: redraw only the bit that changed */
2891 msg.ta = ta;
2892 msg.type = TEXTAREA_MSG_REDRAW_REQUEST;
2893
2894 if (bar_x != (ta->bar_x != NULL) || bar_y != (ta->bar_y != NULL) ||
2895 h_extent != ta->h_extent || v_extent != ta->v_extent) {
2896 /* Must redraw since scrollbars have changed */
2897 msg.data.redraw.x0 = ta->border_width;
2898 msg.data.redraw.y0 = ta->border_width;
2899 msg.data.redraw.x1 = ta->vis_width - ta->border_width;
2900 msg.data.redraw.y1 = ta->vis_height - ta->border_width;
2901 ta->callback(ta->data, &msg);
2902
2903 } else if (redraw) {
2904 msg.data.redraw = r;
2905 ta->callback(ta->data, &msg);
2906 }
2907
2908 return true;
2909 }
2910
2911
2912 /* Handle textarea scrollbar mouse action
2913 * Helper for textarea_mouse_action()
2914 *
2915 * \param ta Text area
2916 * \param mouse the mouse state at action moment
2917 * \param x X coordinate
2918 * \param y Y coordinate
2919 * \return textarea mouse state
2920 */
textarea_mouse_scrollbar_action(struct textarea * ta,browser_mouse_state mouse,int x,int y)2921 static textarea_mouse_status textarea_mouse_scrollbar_action(
2922 struct textarea *ta, browser_mouse_state mouse, int x, int y)
2923 {
2924 int sx, sy; /* xy coord offset for scrollbar */
2925 int sl; /* scrollbar length */
2926
2927 assert(SCROLLBAR_MOUSE_USED == (1 << 0));
2928 assert(TEXTAREA_MOUSE_SCR_USED == (1 << 3));
2929
2930 /* Existing scrollbar drag */
2931 if (ta->drag_info.type == TEXTAREA_DRAG_SCROLLBAR) {
2932 /* Scrollbar drag in progress; pass input to scrollbar */
2933 if (ta->drag_info.data.scrollbar == ta->bar_x) {
2934 x -= ta->border_width;
2935 y -= ta->vis_height - ta->border_width -
2936 SCROLLBAR_WIDTH;
2937 } else {
2938 x -= ta->vis_width - ta->border_width -
2939 SCROLLBAR_WIDTH;
2940 y -= ta->border_width;
2941 }
2942 return (scrollbar_mouse_action(ta->drag_info.data.scrollbar,
2943 mouse, x, y) << 3);
2944 }
2945
2946 /* Horizontal scrollbar */
2947 if (ta->bar_x != NULL && ta->drag_info.type == TEXTAREA_DRAG_NONE) {
2948 /* No drag happening, but mouse input is over scrollbar;
2949 * pass input to scrollbar */
2950 sx = x - ta->border_width;
2951 sy = y - (ta->vis_height - ta->border_width - SCROLLBAR_WIDTH);
2952 sl = ta->vis_width - 2 * ta->border_width -
2953 (ta->bar_y != NULL ? SCROLLBAR_WIDTH : 0);
2954
2955 if (sx >= 0 && sy >= 0 && sx < sl && sy < SCROLLBAR_WIDTH) {
2956 return (scrollbar_mouse_action(ta->bar_x, mouse,
2957 sx, sy) << 3);
2958 }
2959 }
2960
2961 /* Vertical scrollbar */
2962 if (ta->bar_y != NULL && ta->drag_info.type == TEXTAREA_DRAG_NONE) {
2963 /* No drag happening, but mouse input is over scrollbar;
2964 * pass input to scrollbar */
2965 sx = x - (ta->vis_width - ta->border_width - SCROLLBAR_WIDTH);
2966 sy = y - ta->border_width;
2967 sl = ta->vis_height - 2 * ta->border_width;
2968
2969 if (sx >= 0 && sy >= 0 && sx < SCROLLBAR_WIDTH && sy < sl) {
2970 return (scrollbar_mouse_action(ta->bar_y, mouse,
2971 sx, sy) << 3);
2972 }
2973 }
2974
2975 return TEXTAREA_MOUSE_NONE;
2976 }
2977
2978
2979 /* exported interface, documented in textarea.h */
textarea_mouse_action(struct textarea * ta,browser_mouse_state mouse,int x,int y)2980 textarea_mouse_status textarea_mouse_action(struct textarea *ta,
2981 browser_mouse_state mouse, int x, int y)
2982 {
2983 int b_start, b_end;
2984 unsigned int b_off;
2985 struct textarea_msg msg;
2986 textarea_mouse_status status = TEXTAREA_MOUSE_NONE;
2987
2988 if (ta->drag_info.type != TEXTAREA_DRAG_NONE &&
2989 mouse == BROWSER_MOUSE_HOVER) {
2990 /* There is a drag that we must end */
2991 textarea_drag_end(ta, mouse, x, y);
2992 }
2993
2994 /* Mouse action might be a scrollbar's responsibility */
2995 status = textarea_mouse_scrollbar_action(ta, mouse, x, y);
2996 if (status != TEXTAREA_MOUSE_NONE) {
2997 /* Mouse action was handled by a scrollbar */
2998 return status;
2999 }
3000
3001 /* Might be outside textarea, and not dragging */
3002 if ((x >= ta->vis_width || y >= ta->vis_height) &&
3003 ta->drag_info.type == TEXTAREA_DRAG_NONE &&
3004 ta->flags & TEXTAREA_MULTILINE) {
3005 return status;
3006 }
3007
3008 status |= TEXTAREA_MOUSE_EDITOR;
3009
3010 /* Mouse action is textarea's responsibility */
3011 if (mouse & BROWSER_MOUSE_DOUBLE_CLICK) {
3012 /* Select word */
3013 textarea_set_caret_xy(ta, x, y, true);
3014 textarea_select_fragment(ta);
3015 status |= TEXTAREA_MOUSE_USED;
3016
3017 } else if (mouse & BROWSER_MOUSE_TRIPLE_CLICK) {
3018 /* Select paragraph */
3019 textarea_set_caret_xy(ta, x, y, true);
3020 textarea_select_paragraph(ta);
3021 status |= TEXTAREA_MOUSE_USED;
3022
3023 } else if (mouse & BROWSER_MOUSE_PRESS_1) {
3024 /* Place caret */
3025 b_off = textarea_get_b_off_xy(ta, x, y, true);
3026 ta->drag_start = b_off;
3027
3028 textarea_set_caret_internal(ta, b_off);
3029 if (ta->sel_start != -1) {
3030 /* Clear selection */
3031 textarea_clear_selection(ta);
3032 }
3033 status |= TEXTAREA_MOUSE_USED;
3034
3035 } else if (mouse & BROWSER_MOUSE_PRESS_2) {
3036 b_off = textarea_get_b_off_xy(ta, x, y, true);
3037
3038 if (ta->sel_start != -1) {
3039 /* Adjust selection */
3040 b_start = (ta->sel_end - ta->sel_start) / 2 +
3041 ta->sel_start;
3042 b_start = ((unsigned)b_start > b_off) ?
3043 ta->sel_end : ta->sel_start;
3044 ta->drag_start = b_start;
3045 textarea_select(ta, b_start, b_off, false);
3046 } else {
3047 /* Select to caret */
3048 b_start = textarea_get_caret(ta);
3049 ta->drag_start = b_start;
3050 textarea_select(ta, b_start, b_off, false);
3051 }
3052 status |= TEXTAREA_MOUSE_USED;
3053
3054 } else if (mouse & (BROWSER_MOUSE_DRAG_1 | BROWSER_MOUSE_DRAG_2)) {
3055 /* Selection start */
3056 b_off = textarea_get_b_off_xy(ta, x, y, true);
3057 b_start = ta->drag_start;
3058 b_end = b_off;
3059 ta->drag_info.type = TEXTAREA_DRAG_SELECTION;
3060
3061 msg.ta = ta;
3062 msg.type = TEXTAREA_MSG_DRAG_REPORT;
3063 msg.data.drag = ta->drag_info.type;
3064
3065 ta->callback(ta->data, &msg);
3066
3067 textarea_select(ta, b_start, b_end, false);
3068 status |= TEXTAREA_MOUSE_USED;
3069
3070 } else if (mouse &
3071 (BROWSER_MOUSE_HOLDING_1 | BROWSER_MOUSE_HOLDING_2) &&
3072 ta->drag_info.type == TEXTAREA_DRAG_SELECTION) {
3073 /* Selection track */
3074 int scrx = 0;
3075 int scry = 0;
3076 int w, h;
3077 bool need_redraw = false;
3078
3079 b_off = textarea_get_b_off_xy(ta, x, y, true);
3080 b_start = ta->drag_start;
3081 b_end = b_off;
3082
3083 w = ta->vis_width - ta->border_width -
3084 ((ta->bar_y == NULL) ? 0 : SCROLLBAR_WIDTH);
3085 h = ta->vis_height - ta->border_width -
3086 ((ta->bar_x == NULL) ? 0 : SCROLLBAR_WIDTH);
3087
3088 /* selection auto-scroll */
3089 if (x < ta->border_width)
3090 scrx = (x - ta->border_width) / 4;
3091 else if (x > w)
3092 scrx = (x - w) / 4;
3093
3094 if (y < ta->border_width)
3095 scry = (y - ta->border_width) / 4;
3096 else if (y > h)
3097 scry = (y - h) / 4;
3098
3099 if (scrx || scry)
3100 need_redraw = textarea_scroll(ta, scrx, scry);
3101
3102 textarea_select(ta, b_start, b_end, need_redraw);
3103 status |= TEXTAREA_MOUSE_USED;
3104 }
3105
3106 if (ta->sel_start != -1) {
3107 /* Have selection */
3108 status |= TEXTAREA_MOUSE_SELECTION;
3109 }
3110
3111 return status;
3112 }
3113
3114
3115 /* exported interface, documented in textarea.h */
textarea_clear_selection(struct textarea * ta)3116 bool textarea_clear_selection(struct textarea *ta)
3117 {
3118 struct textarea_msg msg;
3119 int line_end, line_start = 0;
3120
3121 if (ta->sel_start == -1)
3122 /* No selection to clear */
3123 return false;
3124
3125 /* Find selection start & end lines */
3126 for (line_end = 0; line_end < ta->line_count - 1; line_end++)
3127 if (ta->lines[line_end + 1].b_start > (unsigned)ta->sel_start) {
3128 line_start = line_end;
3129 break;
3130 }
3131 for (; line_end < ta->line_count - 1; line_end++)
3132 if (ta->lines[line_end + 1].b_start > (unsigned)ta->sel_end)
3133 break;
3134
3135 /* Clear selection and redraw */
3136 textarea_reset_selection(ta);
3137
3138 msg.ta = ta;
3139 msg.type = TEXTAREA_MSG_REDRAW_REQUEST;
3140 msg.data.redraw.x0 = ta->border_width;
3141 msg.data.redraw.y0 = max(ta->border_width,
3142 ta->line_height * line_start +
3143 ta->text_y_offset - ta->scroll_y);
3144 msg.data.redraw.x1 = ta->vis_width - ta->border_width -
3145 ((ta->bar_y == NULL) ? 0 : SCROLLBAR_WIDTH);
3146 msg.data.redraw.y1 = min(ta->vis_height - ta->border_width -
3147 ((ta->bar_x == NULL) ? 0 : SCROLLBAR_WIDTH),
3148 ta->line_height * line_end + ta->text_y_offset +
3149 ta->line_height - ta->scroll_y);
3150
3151 ta->callback(ta->data, &msg);
3152
3153 /* No more selection */
3154 msg.type = TEXTAREA_MSG_SELECTION_REPORT;
3155
3156 msg.data.selection.have_selection = false;
3157 msg.data.selection.read_only = (ta->flags & TEXTAREA_READONLY);
3158
3159 ta->callback(ta->data, &msg);
3160
3161 if (!(ta->flags & TEXTAREA_INTERNAL_CARET)) {
3162 /* Tell client where caret should be placed */
3163 struct rect cr = {
3164 .x0 = ta->border_width,
3165 .y0 = ta->border_width,
3166 .x1 = ta->vis_width - ta->border_width -
3167 ((ta->bar_y == NULL) ?
3168 0 : SCROLLBAR_WIDTH),
3169 .y1 = ta->vis_height - ta->border_width -
3170 ((ta->bar_x == NULL) ?
3171 0 : SCROLLBAR_WIDTH)
3172 };
3173 msg.ta = ta;
3174 msg.type = TEXTAREA_MSG_CARET_UPDATE;
3175 msg.data.caret.type = TEXTAREA_CARET_SET_POS;
3176 msg.data.caret.pos.x = ta->caret_x - ta->scroll_x;
3177 msg.data.caret.pos.y = ta->caret_y - ta->scroll_y;
3178 msg.data.caret.pos.height = ta->line_height;
3179 msg.data.caret.pos.clip = &cr;
3180
3181 ta->callback(ta->data, &msg);
3182 }
3183
3184 return true;
3185 }
3186
3187
3188 /* exported interface, documented in textarea.h */
textarea_get_selection(struct textarea * ta)3189 char *textarea_get_selection(struct textarea *ta)
3190 {
3191 char *ret;
3192 size_t b_start, b_end, b_len;
3193
3194 if (ta->sel_start == -1)
3195 /* No selection get */
3196 return NULL;
3197
3198 b_start = ta->sel_start;
3199 b_end = ta->sel_end;
3200 b_len = b_end - b_start;
3201
3202 if (b_len == 0)
3203 /* No selection get */
3204 return NULL;
3205
3206 ret = malloc(b_len + 1); /* Add space for '\0' */
3207 if (ret == NULL)
3208 /* Can't get selection; no memory */
3209 return NULL;
3210
3211 memcpy(ret, ta->show->data + b_start, b_len);
3212 ret[b_len] = '\0';
3213
3214 return ret;
3215 }
3216
3217
3218 /* exported interface, documented in textarea.h */
textarea_get_dimensions(struct textarea * ta,int * width,int * height)3219 void textarea_get_dimensions(struct textarea *ta, int *width, int *height)
3220 {
3221 if (width != NULL)
3222 *width = ta->vis_width;
3223 if (height != NULL)
3224 *height = ta->vis_height;
3225 }
3226
3227
3228 /* exported interface, documented in textarea.h */
textarea_set_dimensions(struct textarea * ta,int width,int height)3229 void textarea_set_dimensions(struct textarea *ta, int width, int height)
3230 {
3231 struct rect r = {0, 0, 0, 0};
3232
3233 ta->vis_width = width;
3234 ta->vis_height = height;
3235
3236 textarea_setup_text_offsets(ta);
3237
3238 if (ta->flags & TEXTAREA_MULTILINE) {
3239 textarea_reflow_multiline(ta, 0, ta->show->len -1, &r);
3240 } else {
3241 textarea_reflow_singleline(ta, 0, &r);
3242 }
3243 }
3244
3245
3246 /* exported interface, documented in textarea.h */
textarea_set_layout(struct textarea * ta,const plot_font_style_t * fstyle,int width,int height,int top,int right,int bottom,int left)3247 void textarea_set_layout(
3248 struct textarea *ta,
3249 const plot_font_style_t *fstyle,
3250 int width, int height,
3251 int top, int right,
3252 int bottom, int left)
3253 {
3254 struct rect r = {0, 0, 0, 0};
3255
3256 ta->vis_width = width;
3257 ta->vis_height = height;
3258 ta->pad_top = top;
3259 ta->pad_right = right + ((ta->bar_y == NULL) ? 0 : SCROLLBAR_WIDTH);
3260 ta->pad_bottom = bottom + ((ta->bar_x == NULL) ? 0 : SCROLLBAR_WIDTH);
3261 ta->pad_left = left;
3262
3263 textarea_set_text_style(ta, fstyle,
3264 ta->sel_fstyle.foreground,
3265 ta->sel_fstyle.background);
3266
3267 textarea_setup_text_offsets(ta);
3268
3269 if (ta->flags & TEXTAREA_MULTILINE) {
3270 textarea_reflow_multiline(ta, 0, ta->show->len -1, &r);
3271 } else {
3272 textarea_reflow_singleline(ta, 0, &r);
3273 }
3274 }
3275
3276
3277 /* exported interface, documented in textarea.h */
textarea_scroll(struct textarea * ta,int scrx,int scry)3278 bool textarea_scroll(struct textarea *ta, int scrx, int scry)
3279 {
3280 bool handled_scroll = false;
3281
3282 if (ta->flags & TEXTAREA_MULTILINE) {
3283 /* Multi line textareas have scrollbars to handle this */
3284 if (ta->bar_x != NULL && scrx != 0 &&
3285 scrollbar_scroll(ta->bar_x, scrx))
3286 handled_scroll = true;
3287 if (ta->bar_y != NULL && scry != 0 &&
3288 scrollbar_scroll(ta->bar_y, scry))
3289 handled_scroll = true;
3290
3291 } else {
3292 /* Single line. Can only scroll horizontally. */
3293 int xs = ta->scroll_x;
3294
3295 /* Apply offset */
3296 xs += scrx;
3297
3298 /* Clamp to limits */
3299 if (xs < 0)
3300 xs = 0;
3301 else if (xs > ta->h_extent - ta->vis_width - ta->border_width)
3302 xs = ta->h_extent - ta->vis_width - ta->border_width;
3303
3304 if (xs != ta->scroll_x) {
3305 ta->scroll_x = xs;
3306 handled_scroll = true;
3307 }
3308 }
3309
3310 return handled_scroll;
3311 }
3312