/* * Copyright 2006 John-Mark Bell * Copyright 2009 Paul Blokus * * This file is part of NetSurf, http://www.netsurf-browser.org/ * * NetSurf is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * NetSurf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /** \file * Single/Multi-line UTF-8 text area (implementation) */ #include #include #include "css/css.h" #include "css/utils.h" #include "desktop/mouse.h" #include "desktop/textarea.h" #include "desktop/textinput.h" #include "desktop/plotters.h" #include "render/font.h" #include "utils/log.h" #include "utils/utf8.h" #include "utils/utils.h" #define MARGIN_LEFT 4 #define MARGIN_RIGHT 4 #define CARET_COLOR 0x0000FF /* background color for readonly textarea */ #define READONLY_BG 0xD9D9D9 #define BACKGROUND_COL 0xFFFFFF #define BORDER_COLOR 0x000000 #define SELECTION_COL 0xFFDDDD static plot_style_t pstyle_fill_selection = { .fill_type = PLOT_OP_TYPE_SOLID, .fill_colour = SELECTION_COL, }; static plot_style_t pstyle_stroke_border = { .stroke_type = PLOT_OP_TYPE_SOLID, .stroke_colour = BORDER_COLOR, .stroke_width = 1, }; static plot_style_t pstyle_stroke_caret = { .stroke_type = PLOT_OP_TYPE_SOLID, .stroke_colour = CARET_COLOR, .stroke_width = 1, }; struct line_info { unsigned int b_start; /**< Byte offset of line start */ unsigned int b_length; /**< Byte length of line */ }; struct text_area { int scroll_x, scroll_y; /**< scroll offsets of the textarea * content */ unsigned int flags; /**< Textarea flags */ int vis_width; /**< Visible width, in pixels */ int vis_height; /**< Visible height, in pixels */ char *text; /**< UTF-8 text */ unsigned int text_alloc; /**< Size of allocated text */ unsigned int text_len; /**< Length of text, in bytes */ unsigned int text_utf8_len; /**< Length of text, in characters * without the trailing NUL */ struct { int line; /**< Line caret is on */ int char_off; /**< Character index of caret within the * specified line */ } caret_pos; int caret_x; /**< cached X coordinate of the caret */ int caret_y; /**< cached Y coordinate of the caret */ int selection_start; /**< Character index of sel start(inclusive) */ int selection_end; /**< Character index of sel end(exclusive) */ plot_font_style_t fstyle; /**< Text style */ int line_count; /**< Count of lines */ #define LINE_CHUNK_SIZE 16 struct line_info *lines; /**< Line info array */ int line_height; /**< Line height obtained from style */ /** Callback function for a redraw request */ textarea_redraw_request_callback redraw_request; void *data; /** < Callback data for both callback functions */ int drag_start_char; /**< Character index at which the drag was * started */ }; static bool textarea_insert_text(struct text_area *ta, unsigned int index, const char *text); static bool textarea_replace_text(struct text_area *ta, unsigned int start, unsigned int end, const char *text); static bool textarea_reflow(struct text_area *ta, unsigned int line); static unsigned int textarea_get_xy_offset(struct text_area *ta, int x, int y); static bool textarea_set_caret_xy(struct text_area *ta, int x, int y); static bool textarea_scroll_visible(struct text_area *ta); static bool textarea_select(struct text_area *ta, int c_start, int c_end); static bool textarea_select_fragment(struct text_area *ta); static void textarea_normalise_text(struct text_area *ta, unsigned int b_start, unsigned int b_len); /** * Create a text area * * \param x X coordinate of left border * \param y Y coordinate of top border * \param width width of the text area * \param height width of the text area * \param flags text area flags * \param style font style * \param redraw_start_callback will be called when textarea wants to redraw * \param redraw_end_callback will be called when textarea finisjes redrawing * \param data user specified data which will be passed to redraw callbacks * \return Opaque handle for textarea or 0 on error */ struct text_area *textarea_create(int width, int height, unsigned int flags, const plot_font_style_t *style, textarea_redraw_request_callback redraw_request, void *data) { struct text_area *ret; if (redraw_request == NULL) { LOG(("no callback provided")); return NULL; } ret = malloc(sizeof(struct text_area)); if (ret == NULL) { LOG(("malloc failed")); return NULL; } ret->redraw_request = redraw_request; ret->data = data; ret->vis_width = width; ret->vis_height = height; ret->scroll_x = 0; ret->scroll_y = 0; ret->drag_start_char = 0; ret->flags = flags; ret->text = malloc(64); if (ret->text == NULL) { LOG(("malloc failed")); free(ret); return NULL; } ret->text[0] = '\0'; ret->text_alloc = 64; ret->text_len = 1; ret->text_utf8_len = 0; ret->fstyle = *style; ret->line_height = FIXTOINT(FDIV((FMUL(FLTTOFIX(1.2), FMUL(nscss_screen_dpi, INTTOFIX((style->size / FONT_SIZE_SCALE))))), F_72)); ret->caret_pos.line = ret->caret_pos.char_off = 0; ret->caret_x = MARGIN_LEFT; ret->caret_y = 0; ret->selection_start = -1; ret->selection_end = -1; ret->line_count = 0; ret->lines = NULL; return ret; } /** * Destroy a text area * * \param ta Text area to destroy */ void textarea_destroy(struct text_area *ta) { free(ta->text); free(ta->lines); free(ta); } /** * Set the text in a text area, discarding any current text * * \param ta Text area * \param text UTF-8 text to set text area's contents to * \return true on success, false on memory exhaustion */ bool textarea_set_text(struct text_area *ta, const char *text) { unsigned int len = strlen(text) + 1; if (len >= ta->text_alloc) { char *temp = realloc(ta->text, len + 64); if (temp == NULL) { LOG(("realloc failed")); return false; } ta->text = temp; ta->text_alloc = len + 64; } memcpy(ta->text, text, len); ta->text_len = len; ta->text_utf8_len = utf8_length(ta->text); textarea_normalise_text(ta, 0, len); return textarea_reflow(ta, 0); } /** * Extract the text from a text area * * \param ta Text area * \param buf Pointer to buffer to receive data, or NULL * to read length required * \param len Length (bytes) of buffer pointed to by buf, or 0 to read length * \return Length (bytes) written/required or -1 on error */ int textarea_get_text(struct text_area *ta, char *buf, unsigned int len) { if (buf == NULL && len == 0) { /* want length */ return ta->text_len; } else if (buf == NULL) { /* Can't write to NULL */ return -1; } if (len < ta->text_len) { LOG(("buffer too small")); return -1; } memcpy(buf, ta->text, ta->text_len); return ta->text_len; } /** * Insert text into the text area * * \param ta Text area * \param index 0-based character index to insert at * \param text UTF-8 text to insert * \return false on memory exhaustion, true otherwise */ bool textarea_insert_text(struct text_area *ta, unsigned int index, const char *text) { unsigned int b_len = strlen(text); size_t b_off; if (ta->flags & TEXTAREA_READONLY) return true; /* Find insertion point */ if (index > ta->text_utf8_len) index = ta->text_utf8_len; /* find byte offset of insertion point */ for (b_off = 0; index-- > 0; b_off = utf8_next(ta->text, ta->text_len, b_off)) ; /* do nothing */ if (b_len + ta->text_len >= ta->text_alloc) { char *temp = realloc(ta->text, b_len + ta->text_len + 64); if (temp == NULL) { LOG(("realloc failed")); return false; } ta->text = temp; ta->text_alloc = b_len + ta->text_len + 64; } /* Shift text following up */ memmove(ta->text + b_off + b_len, ta->text + b_off, ta->text_len - b_off); /* Insert new text */ memcpy(ta->text + b_off, text, b_len); ta->text_len += b_len; ta->text_utf8_len += utf8_length(text); textarea_normalise_text(ta, b_off, b_len); /** \todo calculate line to reflow from */ return textarea_reflow(ta, 0); } /** * Replace text in a text area * * \param ta Text area * \param start Start character index of replaced section (inclusive) * \param end End character index of replaced section (exclusive) * \param text UTF-8 text to insert * \return false on memory exhaustion, true otherwise */ bool textarea_replace_text(struct text_area *ta, unsigned int start, unsigned int end, const char *text) { unsigned int b_len = strlen(text); size_t b_start, b_end, diff; if (ta->flags & TEXTAREA_READONLY) return true; if (start > ta->text_utf8_len) start = ta->text_utf8_len; if (end > ta->text_utf8_len) end = ta->text_utf8_len; if (start == end) return textarea_insert_text(ta, start, text); if (start > end) return false; diff = end - start; /* find byte offset of replace start */ for (b_start = 0; start-- > 0; b_start = utf8_next(ta->text, ta->text_len, b_start)) ; /* do nothing */ /* find byte length of replaced text */ for (b_end = b_start; diff-- > 0; b_end = utf8_next(ta->text, ta->text_len, b_end)) ; /* do nothing */ if (b_len + ta->text_len - (b_end - b_start) >= ta->text_alloc) { char *temp = realloc(ta->text, b_len + ta->text_len - (b_end - b_start) + 64); if (temp == NULL) { LOG(("realloc failed")); return false; } ta->text = temp; ta->text_alloc = b_len + ta->text_len - (b_end - b_start) + 64; } /* Shift text following to new position */ memmove(ta->text + b_start + b_len, ta->text + b_end, ta->text_len - b_end); /* Insert new text */ memcpy(ta->text + b_start, text, b_len); ta->text_len += b_len - (b_end - b_start); ta->text_utf8_len = utf8_length(ta->text); textarea_normalise_text(ta, b_start, b_len); /** \todo calculate line to reflow from */ return textarea_reflow(ta, 0); } /** * Set the caret's position * * \param ta Text area * \param caret 0-based character index to place caret at, -1 removes * the caret * \return true on success false otherwise */ bool textarea_set_caret(struct text_area *ta, int caret) { unsigned int c_len; unsigned int b_off; int i; int index; int x, y; int x0, y0, x1, y1; int text_y_offset; int width, height; if (ta->flags & TEXTAREA_READONLY) return true; c_len = ta->text_utf8_len; if (caret != -1 && (unsigned)caret > c_len) caret = c_len; if (ta->flags & TEXTAREA_MULTILINE) { /* Multiline textarea */ text_y_offset = 0; } else { /* Single line text area; text is vertically centered */ text_y_offset = (ta->vis_height - ta->line_height + 1) / 2; } /* Delete the old caret */ if (ta->caret_pos.char_off != -1) { index = textarea_get_caret(ta); if (index == -1) return false; /* the redraw might happen in response to a text-change and the caret position might be beyond the current text */ if ((unsigned)index > c_len) index = c_len; /* find byte offset of caret position */ for (b_off = 0; index-- > 0; b_off = utf8_next(ta->text, ta->text_len, b_off)) ; /* do nothing */ nsfont.font_width(&ta->fstyle, ta->text + ta->lines[ta->caret_pos.line].b_start, b_off - ta->lines[ta->caret_pos.line].b_start, &x); x += MARGIN_LEFT - ta->scroll_x; y = ta->line_height * ta->caret_pos.line - ta->scroll_y; /* set the caret coordinate beyond the redraw rectangle */ ta->caret_x = x - 2; x0 = x - 1; y0 = y + text_y_offset; width = 2; height = ta->line_height; ta->redraw_request(ta->data, x0, y0, width, height); } /* check if the caret has to be drawn at all */ if (caret != -1) { /* Find byte offset of caret position */ for (b_off = 0; caret > 0; caret--) b_off = utf8_next(ta->text, ta->text_len, b_off); /* Now find line in which byte offset appears */ for (i = 0; i < ta->line_count - 1; i++) if (ta->lines[i + 1].b_start > b_off) break; ta->caret_pos.line = i; /* Now calculate the char. offset of the caret in this line */ for (c_len = 0, ta->caret_pos.char_off = 0; c_len < b_off - ta->lines[i].b_start; c_len = utf8_next(ta->text + ta->lines[i].b_start, ta->lines[i].b_length, c_len)) ta->caret_pos.char_off++; /* Finally, redraw the caret */ index = textarea_get_caret(ta); if (index == -1) return false; /* find byte offset of caret position */ for (b_off = 0; index-- > 0; b_off = utf8_next(ta->text, ta->text_len, b_off)) ; /* do nothing */ nsfont.font_width(&ta->fstyle, ta->text + ta->lines[ta->caret_pos.line].b_start, b_off - ta->lines[ta->caret_pos.line].b_start, &x); x += MARGIN_LEFT - ta->scroll_x; ta->caret_x = x; y = ta->line_height * ta->caret_pos.line - ta->scroll_y; ta->caret_y = y; if (textarea_scroll_visible(ta)) { ta->redraw_request(ta->data, 0, 0, ta->vis_width, ta->vis_height); } else { x0 = max(x - 1, MARGIN_LEFT); y0 = max(y + text_y_offset, 0); x1 = min(x + 1, ta->vis_width - MARGIN_RIGHT); y1 = min(y + ta->line_height + text_y_offset, ta->vis_height); width = x1 - x0; height = y1 - y0; if (width > 0 && height > 0) { ta->redraw_request(ta->data, x0, y0, width, height); } } } return true; } /** * get character offset from the beginning of the text for some coordinates * * \param ta Text area * \param x X coordinate * \param y Y coordinate * \return character offset */ unsigned int textarea_get_xy_offset(struct text_area *ta, int x, int y) { size_t b_off, temp; unsigned int c_off; int line; if (!ta->line_count) return 0; x = x - MARGIN_LEFT + ta->scroll_x; y = y + ta->scroll_y; if (x < 0) x = 0; line = y / ta->line_height; if (line < 0) line = 0; if (ta->line_count - 1 < line) line = ta->line_count - 1; nsfont.font_position_in_string(&ta->fstyle, ta->text + ta->lines[line].b_start, ta->lines[line].b_length, x, &b_off, &x); /* If the calculated byte offset corresponds with the number of bytes * in the line, and the line has been soft-wrapped, then ensure the * caret offset is before the trailing space character, rather than * after it. Otherwise, the caret will be placed at the start of the * following line, which is undesirable. */ if (b_off == (unsigned)ta->lines[line].b_length && ta->text[ta->lines[line].b_start + ta->lines[line].b_length - 1] == ' ') b_off--; for (temp = 0, c_off = 0; temp < b_off + ta->lines[line].b_start; temp = utf8_next(ta->text, ta->text_len, temp)) c_off++; return c_off; } /** * Set the caret's position * * \param ta Text area * \param x X position of caret in a window relative to text area top left * \param y Y position of caret in a window relative to text area top left * \return true on success false otherwise */ bool textarea_set_caret_xy(struct text_area *ta, int x, int y) { unsigned int c_off; if (ta->flags & TEXTAREA_READONLY) return true; c_off = textarea_get_xy_offset(ta, x, y); return textarea_set_caret(ta, c_off); } /** * Get the caret's position * * \param ta Text area * \return 0-based character index of caret location, or -1 on error */ int textarea_get_caret(struct text_area *ta) { unsigned int c_off = 0, b_off; /* if the text is a trailing NUL only */ if (ta->text_utf8_len == 0) return 0; /* Calculate character offset of this line's start */ for (b_off = 0; b_off < ta->lines[ta->caret_pos.line].b_start; b_off = utf8_next(ta->text, ta->text_len, b_off)) c_off++; return c_off + ta->caret_pos.char_off; } /** * Reflow a text area from the given line onwards * * \param ta Text area to reflow * \param line Line number to begin reflow on * \return true on success false otherwise */ bool textarea_reflow(struct text_area *ta, unsigned int line) { char *text; unsigned int len; size_t b_off; int x; char *space; unsigned int line_count = 0; /** \todo pay attention to line parameter */ /** \todo create horizontal scrollbar if needed */ ta->line_count = 0; if (ta->lines == NULL) { ta->lines = malloc(LINE_CHUNK_SIZE * sizeof(struct line_info)); if (ta->lines == NULL) { LOG(("malloc failed")); return false; } } if (!(ta->flags & TEXTAREA_MULTILINE)) { /* Single line */ ta->lines[line_count].b_start = 0; ta->lines[line_count++].b_length = ta->text_len - 1; ta->line_count = line_count; return true; } for (len = ta->text_len - 1, text = ta->text; len > 0; len -= b_off, text += b_off) { nsfont.font_split(&ta->fstyle, text, len, ta->vis_width - MARGIN_LEFT - MARGIN_RIGHT, &b_off, &x); if (line_count > 0 && line_count % LINE_CHUNK_SIZE == 0) { struct line_info *temp = realloc(ta->lines, (line_count + LINE_CHUNK_SIZE) * sizeof(struct line_info)); if (temp == NULL) { LOG(("realloc failed")); return false; } ta->lines = temp; } /* handle LF */ for (space = text; space <= text + b_off; space++) { if (*space == '\n') break; } if (space <= text + b_off) { /* Found newline; use it */ ta->lines[line_count].b_start = text - ta->text; ta->lines[line_count++].b_length = space - text; b_off = space + 1 - text; if (len - b_off == 0) { /* reached end of input => add last line */ ta->lines[line_count].b_start = text + b_off - ta->text; ta->lines[line_count++].b_length = 0; } continue; } if (len - b_off > 0) { /* find last space (if any) */ for (space = text + b_off; space > text; space--) if (*space == ' ') break; if (space != text) b_off = space + 1 - text; } ta->lines[line_count].b_start = text - ta->text; ta->lines[line_count++].b_length = b_off; } ta->line_count = line_count; return true; } /** * Handle redraw requests for text areas * * \param redraw Redraw request block * \param x0 left X coordinate of redraw area * \param y0 top Y coordinate of redraw area * \param x1 right X coordinate of redraw area * \param y1 bottom Y coordinate of redraw area * \param ctx current redraw context */ void textarea_redraw(struct text_area *ta, int x, int y, const struct rect *clip, const struct redraw_context *ctx) { const struct plotter_table *plot = ctx->plot; int line0, line1, line; int chars, offset, text_y_offset, text_y_offset_baseline; unsigned int c_pos, c_len, b_start, b_end, line_len; char *line_text; struct rect r; plot_style_t plot_style_fill_bg = { .fill_type = PLOT_OP_TYPE_SOLID, .fill_colour = BACKGROUND_COL, }; r = *clip; if (r.x1 < x || r.x0 > x + ta->vis_width || r.y1 < y || r.y0 > y + ta->vis_height) /* Textarea outside the clipping rectangle */ return; if (ta->lines == NULL) /* Nothing to redraw */ return; if (ta->flags & TEXTAREA_READONLY) plot_style_fill_bg.fill_colour = READONLY_BG; line0 = (r.y0 - y + ta->scroll_y) / ta->line_height - 1; line1 = (r.y1 - y + ta->scroll_y) / ta->line_height + 1; if (line0 < 0) line0 = 0; if (line1 < 0) line1 = 0; if (ta->line_count - 1 < line0) line0 = ta->line_count - 1; if (ta->line_count - 1 < line1) line1 = ta->line_count - 1; if (line1 < line0) line1 = line0; if (r.x0 < x) r.x0 = x; if (r.y0 < y) r.y0 = y; if (r.x1 > x + ta->vis_width) r.x1 = x + ta->vis_width; if (r.y1 > y + ta->vis_height) r.y1 = y + ta->vis_height; plot->clip(&r); plot->rectangle(r.x0, r.y0, r.x1, r.y1, &plot_style_fill_bg); plot->rectangle(x, y, x + ta->vis_width - 1, y + ta->vis_height - 1, &pstyle_stroke_border); if (r.x0 < x + MARGIN_LEFT) r.x0 = x + MARGIN_LEFT; if (r.x1 > x + ta->vis_width - MARGIN_RIGHT) r.x1 = x + ta->vis_width - MARGIN_RIGHT; plot->clip(&r); if (line0 > 0) c_pos = utf8_bounded_length(ta->text, ta->lines[line0].b_start - 1); else c_pos = 0; if (ta->flags & TEXTAREA_MULTILINE) { /* Multiline textarea */ text_y_offset = 0; text_y_offset_baseline = (ta->line_height * 3 + 2) / 4; } else { /* Single line text area; text is vertically centered */ text_y_offset = (ta->vis_height - ta->line_height + 1) / 2; text_y_offset_baseline = (ta->vis_height * 3 + 2) / 4; } for (line = line0; (line <= line1) && (y + line * ta->line_height <= r.y1 + ta->scroll_y); line++) { if (ta->lines[line].b_length == 0) continue; c_len = utf8_bounded_length( &(ta->text[ta->lines[line].b_start]), ta->lines[line].b_length); /* if there is a newline between the lines count it too */ if (line < ta->line_count - 1 && ta->lines[line + 1].b_start != ta->lines[line].b_start + ta->lines[line].b_length) c_len++; /* check if a part of the line is selected, won't happen if no selection (ta->selection_end = -1) */ if (ta->selection_end != -1 && c_pos < (unsigned)ta->selection_end && c_pos + c_len > (unsigned)ta->selection_start) { /* offset from the beginning of the line */ offset = ta->selection_start - c_pos; chars = ta->selection_end - c_pos - (offset > 0 ? offset:0); line_text = &(ta->text[ta->lines[line].b_start]); line_len = ta->lines[line].b_length; if (offset > 0) { /* find byte start of the selected part */ for (b_start = 0; offset > 0; offset--) b_start = utf8_next(line_text, line_len, b_start); nsfont.font_width(&ta->fstyle, line_text, b_start, &r.x0); r.x0 += x + MARGIN_LEFT; } else { r.x0 = x + MARGIN_LEFT; b_start = 0; } if (chars >= 0) { /* find byte end of the selected part */ for (b_end = b_start; chars > 0 && b_end < line_len; chars--) { b_end = utf8_next(line_text, line_len, b_end); } } else b_end = ta->lines[line].b_length; b_end -= b_start; nsfont.font_width(&ta->fstyle, &(ta->text[ta->lines[line].b_start + b_start]), b_end, &r.x1); r.x1 += r.x0; plot->rectangle(r.x0 - ta->scroll_x, y + line * ta->line_height + 1 - ta->scroll_y + text_y_offset, r.x1 - ta->scroll_x, y + (line + 1) * ta->line_height - 1 - ta->scroll_y + text_y_offset, &pstyle_fill_selection); } c_pos += c_len; r.y0 = y + line * ta->line_height + text_y_offset_baseline; ta->fstyle.background = (ta->flags & TEXTAREA_READONLY) ? READONLY_BG : BACKGROUND_COL, plot->text(x + MARGIN_LEFT - ta->scroll_x, r.y0 - ta->scroll_y, ta->text + ta->lines[line].b_start, ta->lines[line].b_length, &ta->fstyle); } if ((ta->selection_end == -1 || ta->selection_start == ta->selection_end) && x + ta->caret_x >= clip->x0 && x + ta->caret_x <= clip->x1) { /* There is no selection and caret is in horizontal * clip range. */ int caret_height = ta->line_height - 1; y += ta->caret_y + text_y_offset; if (y + caret_height >= clip->y0 && y <= clip->y1) /* Caret in vertical clip range; plot */ plot->line(x + ta->caret_x, y + ta->caret_y, x + ta->caret_x, y + ta->caret_y + caret_height, &pstyle_stroke_caret); } } /** * Key press handling for text areas. * * \param ta The text area which got the keypress * \param key The ucs4 character codepoint * \return true if the keypress is dealt with, false otherwise. */ bool textarea_keypress(struct text_area *ta, uint32_t key) { char utf8[6]; unsigned int caret, caret_init, length, l_len, b_off, b_len; int c_line, c_chars, line; bool redraw = false; bool readonly; caret_init = caret = textarea_get_caret(ta); line = ta->caret_pos.line; readonly = (ta->flags & TEXTAREA_READONLY ? true:false); if (!(key <= 0x001F || (0x007F <= key && key <= 0x009F))) { /* normal character insertion */ length = utf8_from_ucs4(key, utf8); utf8[length] = '\0'; if (!textarea_insert_text(ta, caret, utf8)) return false; caret++; redraw = true; } else switch (key) { case KEY_SELECT_ALL: caret = ta->text_utf8_len; ta->selection_start = 0; ta->selection_end = ta->text_utf8_len; redraw = true; break; case KEY_COPY_SELECTION: break; case KEY_DELETE_LEFT: if (readonly) break; if (ta->selection_start != -1) { if (!textarea_replace_text(ta, ta->selection_start, ta->selection_end, "")) return false; caret = ta->selection_start; ta->selection_start = ta->selection_end = -1; redraw = true; } else { if (caret) { if (!textarea_replace_text(ta, caret - 1, caret, "")) return false; caret--; redraw = true; } } break; case KEY_NL: if (readonly) break; if(!textarea_insert_text(ta, caret, "\n")) return false; caret++; ta->selection_start = ta->selection_end = -1; redraw = true; break; case KEY_CUT_LINE: case KEY_PASTE: case KEY_CUT_SELECTION: break; case KEY_ESCAPE: case KEY_CLEAR_SELECTION: ta->selection_start = -1; ta->selection_end = -1; redraw = true; break; case KEY_LEFT: if (readonly) break; if (caret) caret--; if (ta->selection_start != -1) { ta->selection_start = ta->selection_end = -1; redraw = true; } break; case KEY_RIGHT: if (readonly) break; if (caret < ta->text_utf8_len) caret++; if (ta->selection_start != -1) { ta->selection_start = ta->selection_end = -1; redraw = true; } break; case KEY_PAGE_UP: if (readonly) break; if (ta->flags & TEXTAREA_MULTILINE) { /* +1 because one line is subtracted in KEY_UP */ line = ta->caret_pos.line - (ta->vis_height + ta->line_height - 1) / ta->line_height + 1; } /* fall through */ case KEY_UP: if (readonly) break; if (ta->selection_start != -1) { ta->selection_start = ta->selection_end = -1; redraw = true; } if (ta->flags & TEXTAREA_MULTILINE) { line--; if (line < 0) line = 0; if (line == ta->caret_pos.line) break; b_off = ta->lines[line].b_start; b_len = ta->lines[line].b_length; c_line = ta->caret_pos.line; c_chars = ta->caret_pos.char_off; if (ta->text[b_off + b_len - 1] == ' ' && line < ta->line_count - 1) b_len--; l_len = utf8_bounded_length(&(ta->text[b_off]), b_len); ta->caret_pos.line = line; ta->caret_pos.char_off = min(l_len, (unsigned) ta->caret_pos.char_off); caret = textarea_get_caret(ta); ta->caret_pos.line = c_line; ta->caret_pos.char_off = c_chars; } break; case KEY_PAGE_DOWN: if (readonly) break; if (ta->flags & TEXTAREA_MULTILINE) { /* -1 because one line is added in KEY_DOWN */ line = ta->caret_pos.line + (ta->vis_height + ta->line_height - 1) / ta->line_height - 1; } /* fall through */ case KEY_DOWN: if (readonly) break; if (ta->selection_start != -1) { ta->selection_start = ta->selection_end = -1; redraw = true; } if (ta->flags & TEXTAREA_MULTILINE) { line++; if (line > ta->line_count - 1) line = ta->line_count - 1; if (line == ta->caret_pos.line) break; b_off = ta->lines[line].b_start; b_len = ta->lines[line].b_length; c_line = ta->caret_pos.line; c_chars = ta->caret_pos.char_off; if (ta->text[b_off + b_len - 1] == ' ' && line < ta->line_count - 1) b_len--; l_len = utf8_bounded_length(&(ta->text[b_off]), b_len); ta->caret_pos.line = line; ta->caret_pos.char_off = min(l_len, (unsigned) ta->caret_pos.char_off); caret = textarea_get_caret(ta); ta->caret_pos.line = c_line; ta->caret_pos.char_off = c_chars; } break; case KEY_DELETE_RIGHT: if (readonly) break; if (ta->selection_start != -1) { if (!textarea_replace_text(ta, ta->selection_start, ta->selection_end, "")) return false; caret = ta->selection_start; ta->selection_start = ta->selection_end = -1; redraw = true; } else { if (caret < ta->text_utf8_len) { if (!textarea_replace_text(ta, caret, caret + 1, "")) return false; redraw = true; } } break; case KEY_LINE_START: if (readonly) break; caret -= ta->caret_pos.char_off; if (ta->selection_start != -1) { ta->selection_start = ta->selection_end = -1; redraw = true; } break; case KEY_LINE_END: if (readonly) break; caret = utf8_bounded_length(ta->text, ta->lines[ta->caret_pos.line].b_start + ta->lines[ta->caret_pos.line].b_length); if (ta->text[ta->lines[ta->caret_pos.line].b_start + ta->lines[ta->caret_pos.line].b_length - 1] == ' ') caret--; if (ta->selection_start != -1) { ta->selection_start = ta->selection_end = -1; redraw = true; } break; case KEY_TEXT_START: if (readonly) break; caret = 0; if (ta->selection_start != -1) { ta->selection_start = ta->selection_end = -1; redraw = true; } break; case KEY_TEXT_END: if (readonly) break; caret = ta->text_utf8_len; if (ta->selection_start != -1) { ta->selection_start = ta->selection_end = -1; redraw = true; } break; case KEY_WORD_LEFT: case KEY_WORD_RIGHT: break; case KEY_DELETE_LINE_END: if (readonly) break; if (ta->selection_start != -1) { if (!textarea_replace_text(ta, ta->selection_start, ta->selection_end, "")) return false; ta->selection_start = ta->selection_end = -1; } else { b_off = ta->lines[ta->caret_pos.line].b_start; b_len = ta->lines[ta->caret_pos.line].b_length; l_len = utf8_bounded_length(&(ta->text[b_off]), b_len); if (!textarea_replace_text(ta, caret, caret + l_len, "")) return false; } redraw = true; break; case KEY_DELETE_LINE_START: if (readonly) break; if (ta->selection_start != -1) { if (!textarea_replace_text(ta, ta->selection_start, ta->selection_end, "")) return false; ta->selection_start = ta->selection_end = -1; } else { if (!textarea_replace_text(ta, caret - ta->caret_pos.char_off, caret, "")) return false; caret -= ta->caret_pos.char_off; } redraw = true; break; default: return false; } if (caret != caret_init) textarea_set_caret(ta, caret); //TODO:redraw only the important part if (redraw) { ta->redraw_request(ta->data, 0, 0, ta->vis_width, ta->vis_height); } return true; } /** * Scrolls a textarea to make the caret visible (doesn't perform a redraw) * * \param ta The text area to be scrolled * \return true if textarea was scrolled false otherwise */ bool textarea_scroll_visible(struct text_area *ta) { int x0, x1, y0, y1, x, y; int index, b_off; bool scrolled = false; if (ta->caret_pos.char_off == -1) return false; x0 = MARGIN_LEFT; x1 = ta->vis_width - MARGIN_RIGHT; y0 = 0; y1 = ta->vis_height; index = textarea_get_caret(ta); /* find byte offset of caret position */ for (b_off = 0; index-- > 0; b_off = utf8_next(ta->text, ta->text_len, b_off)) ; /* do nothing */ nsfont.font_width(&ta->fstyle, ta->text + ta->lines[ta->caret_pos.line].b_start, b_off - ta->lines[ta->caret_pos.line].b_start, &x); /* top-left of caret */ x += MARGIN_LEFT - ta->scroll_x; y = ta->line_height * ta->caret_pos.line - ta->scroll_y; /* check and change vertical scroll */ if (y < y0) { ta->scroll_y -= y0 - y; scrolled = true; } else if (y + ta->line_height > y1) { ta->scroll_y += y + ta->line_height - y1; scrolled = true; } /* check and change horizontal scroll */ if (x < x0) { ta->scroll_x -= x0 - x ; scrolled = true; } else if (x > x1 - 1) { ta->scroll_x += x - (x1 - 1); scrolled = true; } return scrolled; } /** * Handles all kinds of mouse action * * \param ta Text area * \param mouse the mouse state at action moment * \param x X coordinate * \param y Y coordinate * \return true if action was handled false otherwise */ bool textarea_mouse_action(struct text_area *ta, browser_mouse_state mouse, int x, int y) { int c_start, c_end; /* mouse button pressed above the text area, move caret */ if (mouse & BROWSER_MOUSE_PRESS_1) { if (!(ta->flags & TEXTAREA_READONLY)) textarea_set_caret_xy(ta, x, y); if (ta->selection_start != -1) { ta->selection_start = ta->selection_end = -1; ta->redraw_request(ta->data, 0, 0, ta->vis_width, ta->vis_height); } } else if (mouse & BROWSER_MOUSE_DOUBLE_CLICK) { if (!(ta->flags & TEXTAREA_READONLY)) { textarea_set_caret_xy(ta, x, y); return textarea_select_fragment(ta); } } else if (mouse & BROWSER_MOUSE_DRAG_1) { ta->drag_start_char = textarea_get_xy_offset(ta, x, y); if (!(ta->flags & TEXTAREA_READONLY)) return textarea_set_caret(ta, -1); } else if (mouse & BROWSER_MOUSE_HOLDING_1) { c_start = ta->drag_start_char; c_end = textarea_get_xy_offset(ta, x, y); return textarea_select(ta, c_start, c_end); } return true; } /** * Handles the end of a drag operation * * \param ta Text area * \param mouse the mouse state at drag end moment * \param x X coordinate * \param y Y coordinate * \return true if drag end was handled false otherwise */ bool textarea_drag_end(struct text_area *ta, browser_mouse_state mouse, int x, int y) { int c_end; c_end = textarea_get_xy_offset(ta, x, y); return textarea_select(ta, ta->drag_start_char, c_end); } /** * Selects a character range in the textarea and redraws it * * \param ta Text area * \param c_start First character (inclusive) * \param c_end Last character (exclusive) * \return true on success false otherwise */ bool textarea_select(struct text_area *ta, int c_start, int c_end) { int swap = -1; /* if start is after end they get swapped, start won't and end will be selected this way */ if (c_start > c_end) { swap = c_start; c_start = c_end; c_end = swap; } ta->selection_start = c_start; ta->selection_end = c_end; if (!(ta->flags & TEXTAREA_READONLY)) { if (swap == -1) return textarea_set_caret(ta, c_end); else return textarea_set_caret(ta, c_start); } ta->redraw_request(ta->data, 0, 0, ta->vis_width, ta->vis_height); return true; } /** * Selects a text fragment, relative to current caret position. * * \param ta Text area * \return True on success, false otherwise */ static bool textarea_select_fragment(struct text_area * ta) { int caret_pos, sel_start = 0, sel_end = 0, index; size_t b_start, b_end; /* Fragment separators must be suitable for URLs and ordinary text */ static const char *sep = " /:.\r\n"; caret_pos = textarea_get_caret(ta); if (caret_pos < 0) { return false; } /* Compute byte offset of caret position */ for (b_start = 0, index = 0; index < caret_pos; b_start = utf8_next(ta->text, ta->text_len, b_start), index++) { /* Cache the character offset of the last separator */ if (strchr(sep, ta->text[b_start]) != NULL) { /* Add one to start to skip over separator */ sel_start = index + 1; } } /* Search for next separator, if any */ for (b_end = b_start; b_end < ta->text_len; b_end = utf8_next(ta->text, ta->text_len, b_end), index++) { if (strchr(sep, ta->text[b_end]) != NULL) { sel_end = index; break; } } if (sel_start < sel_end) { textarea_select(ta, sel_start, sel_end); return true; } return false; } /** * Normalises any line endings within the text, replacing CRLF or CR with * LF as necessary. If the textarea is single line, then all linebreaks are * converted into spaces. * * \param ta Text area * \param b_start Byte offset to start at * \param b_len Byte length to check */ void textarea_normalise_text(struct text_area *ta, unsigned int b_start, unsigned int b_len) { bool multi = (ta->flags & TEXTAREA_MULTILINE) ? true:false; unsigned int index; /* Remove CR characters. If it's a CRLF pair delete it ot replace it * with LF otherwise. */ for (index = 0; index < b_len; index++) { if (ta->text[b_start + index] == '\r') { if (b_start + index + 1 <= ta->text_len && ta->text[b_start + index + 1] == '\n') { ta->text_len--; ta->text_utf8_len--; memmove(ta->text + b_start + index, ta->text + b_start + index + 1, ta->text_len - b_start - index); } else ta->text[b_start + index] = '\n'; } /* Replace newlines with spaces if this is a single line * textarea. */ if (!multi && (ta->text[b_start + index] == '\n')) ta->text[b_start + index] = ' '; } } /** * Gets the dimensions of a textarea * * \param width if not NULL, gets updated to the width of the textarea * \param height if not NULL, gets updated to the height of the textarea */ void textarea_get_dimensions(struct text_area *ta, int *width, int *height) { if (width != NULL) *width = ta->vis_width; if (height != NULL) *height = ta->vis_height; } /** * Set the dimensions of a textarea, causing a reflow and * emitting a redraw request. * * \param width the new width of the textarea * \param height the new height of the textarea */ void textarea_set_dimensions(struct text_area *ta, int width, int height) { ta->vis_width = width; ta->vis_height = height; textarea_reflow(ta, 0); ta->redraw_request(ta->data, 0, 0, ta->vis_width, ta->vis_height); }