/* * Copyright 2003 Phil Mellor * Copyright 2004 James Bursa * Copyright 2004 Andrew Timmins * Copyright 2004 John Tytgat * Copyright 2005 Adrian Lees * * 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 * HTML form text input handling (implementation) */ #include #include #include #include "desktop/browser.h" #include "desktop/gui.h" #include "desktop/mouse.h" #include "desktop/scrollbar.h" #include "desktop/selection.h" #include "desktop/textinput.h" #include "render/box.h" #include "render/font.h" #include "render/form.h" #include "render/html_internal.h" #include "render/layout.h" #include "render/textinput.h" #include "utils/log.h" #include "utils/talloc.h" #include "utils/utf8.h" #include "utils/utils.h" /* Define to enable textinput debug */ #undef TEXTINPUT_DEBUG static bool textinput_textbox_delete(struct content *c, struct box *text_box, unsigned char_offset, unsigned utf8_len); /* Textarea callbacks */ static bool textinput_textarea_callback(struct browser_window *bw, uint32_t key, void *p1, void *p2); static void textinput_textarea_move_caret(struct browser_window *bw, void *p1, void *p2); static bool textinput_textarea_paste_text(struct browser_window *bw, const char *utf8, unsigned utf8_len, bool last, void *p1, void *p2); /* Text input callbacks */ static bool textinput_input_callback(struct browser_window *bw, uint32_t key, void *p1, void *p2); static void textinput_input_move_caret(struct browser_window *bw, void *p1, void *p2); static bool textinput_input_paste_text(struct browser_window *bw, const char *utf8, unsigned utf8_len, bool last, void *p1, void *p2); #define SPACE_LEN(b) ((b->space == 0) ? 0 : 1) /** * Given the x,y co-ordinates of a point within a textarea, return the * TEXT box pointer, and the character and pixel offsets within that * box at which the caret should be positioned. (eg. for mouse clicks, * drag-and-drop insertions etc) * * \param textarea the textarea being considered * \param x x ordinate of point * \param y y ordinate of point * \param pchar_offset receives the char offset within the TEXT box * \param ppixel_offset receives the pixel offset within the TEXT box * \return pointer to TEXT box */ static struct box *textinput_textarea_get_position(struct box *textarea, int x, int y, int *pchar_offset, int *ppixel_offset) { /* A textarea is an INLINE_BLOCK containing a single * INLINE_CONTAINER, which contains the text as runs of TEXT * separated by BR. There is at least one TEXT. The first and * last boxes are TEXT. Consecutive BR may not be present. These * constraints are satisfied by using a 0-length TEXT for blank * lines. */ struct box *inline_container, *text_box; plot_font_style_t fstyle; size_t char_offset = 0; inline_container = textarea->children; if (inline_container->y + inline_container->height < y) { /* below the bottom of the textarea: place caret at end */ text_box = inline_container->last; assert(text_box->type == BOX_TEXT); assert(text_box->text); font_plot_style_from_css(text_box->style, &fstyle); /** \todo handle errors */ nsfont.font_position_in_string(&fstyle, text_box->text, text_box->length, (unsigned int)(x - text_box->x), &char_offset, ppixel_offset); } else { /* find the relevant text box */ y -= inline_container->y; for (text_box = inline_container->children; text_box && text_box->y + text_box->height < y; text_box = text_box->next) ; for (; text_box && text_box->type != BOX_BR && text_box->y <= y && text_box->x + text_box->width < x; text_box = text_box->next) ; if (!text_box) { /* past last text box */ text_box = inline_container->last; assert(text_box->type == BOX_TEXT); assert(text_box->text); font_plot_style_from_css(text_box->style, &fstyle); nsfont.font_position_in_string(&fstyle, text_box->text, text_box->length, textarea->width, &char_offset, ppixel_offset); } else { /* in a text box */ if (text_box->type == BOX_BR) text_box = text_box->prev; else if (y < text_box->y && text_box->prev) { if (text_box->prev->type == BOX_BR) { assert(text_box->prev->prev); text_box = text_box->prev->prev; } else text_box = text_box->prev; } assert(text_box->type == BOX_TEXT); assert(text_box->text); font_plot_style_from_css(text_box->style, &fstyle); nsfont.font_position_in_string(&fstyle, text_box->text, text_box->length, (unsigned int)(x - text_box->x), &char_offset, ppixel_offset); } } *pchar_offset = char_offset; assert(text_box); return text_box; } /** * Delete some text from a box, or delete the box in its entirety * * \param c html content * \param b box * \param offset start offset of text to be deleted (in bytes) * \param length length of text to be deleted * \return true iff successful */ static bool textinput_delete_handler(struct content *c, struct box *b, int offset, size_t length) { size_t text_length = b->length + SPACE_LEN(b); /* only remove if its not the first box */ if (offset <= 0 && length >= text_length && b->prev != NULL) { /* remove the entire box */ box_unlink_and_free(b); return true; } else return textinput_textbox_delete(c, b, offset, min(length, text_length - offset)); } /** * Remove the selected text from a text box and gadget (if applicable) * * \param c The content containing the selection * \param s The selection to be removed */ static void textinput_delete_selection(struct content *c, struct selection *s) { size_t start_offset, end_offset; struct box *text_box; struct box *end_box; struct box *next; size_t sel_len; int beginning = 0; assert(s->defined); text_box = selection_get_start(s, &start_offset); end_box = selection_get_end(s, &end_offset); sel_len = s->end_idx - s->start_idx; /* Clear selection so that deletion from textboxes proceeds */ selection_clear(s, true); /* handle first box */ textinput_delete_handler(c, text_box, start_offset, sel_len); if (text_box == end_box) return; for (text_box = text_box->next; text_box != end_box; text_box = next) { next = text_box->next; box_unlink_and_free(text_box); } textinput_delete_handler(c, end_box, beginning, end_offset); } /** * Insert a number of chars into a text box * * \param c html_content * \param text_box text box * \param char_offset offset (bytes) at which to insert text * \param utf8 UTF-8 text to insert * \param utf8_len length (bytes) of UTF-8 text to insert * \return true iff successful */ static bool textinput_textbox_insert(struct content *c, struct box *text_box, unsigned char_offset, const char *utf8, unsigned utf8_len) { html_content *html = (html_content *)c; char *text; struct box *input = text_box->parent->parent; bool hide; if (html->bw && html->sel.defined) textinput_delete_selection(c, &html->sel); /* insert into form gadget (text and password inputs only) */ if (input->gadget && (input->gadget->type == GADGET_TEXTBOX || input->gadget->type == GADGET_PASSWORD) && input->gadget->value) { size_t form_offset = input->gadget->caret_form_offset; char *value = realloc(input->gadget->value, input->gadget->length + utf8_len + 1); if (!value) { warn_user("NoMemory", 0); return true; } input->gadget->value = value; memmove(input->gadget->value + form_offset + utf8_len, input->gadget->value + form_offset, input->gadget->length - form_offset); memcpy(input->gadget->value + form_offset, utf8, utf8_len); input->gadget->length += utf8_len; input->gadget->value[input->gadget->length] = 0; } hide = (input->gadget && input->gadget->type == GADGET_PASSWORD); if (hide) { /* determine the number of '*'s to be inserted */ const char *eutf8 = utf8 + utf8_len; utf8_len = 0; while (utf8 < eutf8) { utf8 += utf8_next(utf8, eutf8 - utf8, 0); utf8_len++; } } /* insert in text box */ text = talloc_realloc(c, text_box->text, char, text_box->length + SPACE_LEN(text_box) + utf8_len + 1); if (!text) { warn_user("NoMemory", 0); return false; } text_box->text = text; if (text_box->space != 0 && char_offset == text_box->length + SPACE_LEN(text_box)) { if (hide) text_box->space = 0; else { unsigned int last_off = utf8_prev(utf8, utf8_len); if (utf8[last_off] != ' ') text_box->space = 0; else utf8_len = last_off; } text_box->text[text_box->length++] = ' '; } else { memmove(text_box->text + char_offset + utf8_len, text_box->text + char_offset, text_box->length - char_offset); } if (hide) memset(text_box->text + char_offset, '*', utf8_len); else memcpy(text_box->text + char_offset, utf8, utf8_len); text_box->length += utf8_len; /* nothing should assume that the text is terminated, * but just in case */ text_box->text[text_box->length] = 0; text_box->width = UNKNOWN_WIDTH; return true; } /** * Calculates the form_offset from the box_offset * * \param input The root box containing both the textbox and gadget * \param text_box The textbox containing the caret * \param char_offset The caret offset within text_box * \return the translated form_offset */ static size_t textinput_get_form_offset(struct box* input, struct box* text_box, size_t char_offset) { int uchars; unsigned int offset; for (uchars = 0, offset = 0; offset < char_offset; uchars++) { if ((text_box->text[offset] & 0x80) == 0x00) { offset++; continue; } assert((text_box->text[offset] & 0xC0) == 0xC0); for (++offset; offset < char_offset && (text_box->text[offset] & 0xC0) == 0x80; offset++) /* do nothing */; } /* uchars is the number of real Unicode characters at the left * side of the caret. */ for (offset = 0; uchars > 0 && offset < input->gadget->length; uchars--) { if ((input->gadget->value[offset] & 0x80) == 0x00) { offset++; continue; } assert((input->gadget->value[offset] & 0xC0) == 0xC0); for (++offset; offset < input->gadget->length && (input->gadget->value[offset] & 0xC0) == 0x80; offset++) /* do nothing */; } assert(uchars == 0); return offset; } /** * Delete a number of chars from a text box * * \param c html content * \param text_box text box * \param char_offset offset within text box (bytes) of first char to delete * \param utf8_len length (bytes) of chars to be deleted * \return true on success, false otherwise * * ::char_offset and ::utf8_len are only considered when there is no selection. * If there is a selection, the entire selected area is deleted. */ bool textinput_textbox_delete(struct content *c, struct box *text_box, unsigned char_offset, unsigned utf8_len) { html_content *html = (html_content *)c; unsigned next_offset = char_offset + utf8_len; struct box *form = text_box->parent->parent; if (html->bw && html->sel.defined) { textinput_delete_selection(c, &html->sel); return true; } /* delete from form gadget (text and password inputs only) */ if (form->gadget && (form->gadget->type == GADGET_TEXTBOX || form->gadget->type == GADGET_PASSWORD) && form->gadget->value) { size_t form_offset = textinput_get_form_offset(form, text_box, char_offset); size_t next_offset = textinput_get_form_offset(form, text_box, char_offset + utf8_len); memmove(form->gadget->value + form_offset, form->gadget->value + next_offset, form->gadget->length - next_offset); form->gadget->length -= (next_offset - form_offset); form->gadget->value[form->gadget->length] = 0; } /* delete from visible textbox */ if (next_offset <= text_box->length + SPACE_LEN(text_box)) { /* handle removal of trailing space */ if (text_box->space != 0 && next_offset > text_box->length) { if (char_offset > 0) { /* is the trailing character still a space? */ int tmp = utf8_prev(text_box->text, char_offset); if (isspace(text_box->text[tmp])) char_offset = tmp; else text_box->space = 0; } else { text_box->space = 0; } text_box->length = char_offset; } else { memmove(text_box->text + char_offset, text_box->text + next_offset, text_box->length - next_offset); text_box->length -= utf8_len; } /* nothing should assume that the text is terminated, * but just in case */ text_box->text[text_box->length] = 0; text_box->width = UNKNOWN_WIDTH; return true; } return false; } /** * Locate the first inline box at the start of this line * * \param text_box text box from which to start searching */ static struct box *textinput_line_start(struct box *text_box) { while (text_box->prev && text_box->prev->type == BOX_TEXT) text_box = text_box->prev; return text_box; } /** * Locate the last inline box in this line * * \param text_box text box from which to start searching */ static struct box *textinput_line_end(struct box *text_box) { while (text_box->next && text_box->next->type == BOX_TEXT) text_box = text_box->next; return text_box; } /** * Backtrack to the start of the previous line, if there is one. */ static struct box *textinput_line_above(struct box *text_box) { struct box *prev; text_box = textinput_line_start(text_box); prev = text_box->prev; while (prev && prev->type == BOX_BR) prev = prev->prev; return prev ? textinput_line_start(prev) : text_box; } /** * Advance to the start of the next line, if there is one. */ static struct box *textinput_line_below(struct box *text_box) { struct box *next; text_box = textinput_line_end(text_box); next = text_box->next; while (next && next->type == BOX_BR) next = next->next; return next ? next : text_box; } /** * Cut a range of text from a text box, * possibly placing it on the global clipboard. * * \param c html content * \param start_box text box at start of range * \param start_idx index (bytes) within start box * \param end_box text box at end of range * \param end_idx index (bytes) within end box * \param clipboard whether to place text on the clipboard * \return true iff successful */ static bool textinput_textarea_cut(struct content *c, struct box *start_box, unsigned start_idx, struct box *end_box, unsigned end_idx, bool clipboard) { struct box *box = start_box; bool success = true; bool del = false; /* caller expects start_box to persist */ if (clipboard && !gui_empty_clipboard()) return false; while (box && box != end_box) { /* read before deletion, in case the whole box goes */ struct box *next = box->next; if (box->type == BOX_BR) { if (clipboard && !gui_add_to_clipboard("\n", 1, false)) { gui_commit_clipboard(); return false; } box_unlink_and_free(box); } else { /* append box text to clipboard and then delete it */ if (clipboard && !gui_add_to_clipboard(box->text + start_idx, box->length - start_idx, SPACE_LEN(box))) { gui_commit_clipboard(); return false; } if (del) { if (!textinput_delete_handler(c, box, start_idx, (box->length + SPACE_LEN(box)) - start_idx) && clipboard) { gui_commit_clipboard(); return false; } } else { textinput_textbox_delete(c, box, start_idx, (box->length + SPACE_LEN(box)) - start_idx); } } del = true; start_idx = 0; box = next; } /* and the last box */ if (box) { if (clipboard && !gui_add_to_clipboard(box->text + start_idx, end_idx - start_idx, end_idx > box->length)) { success = false; } else { if (del) { if (!textinput_delete_handler(c, box, start_idx, end_idx - start_idx)) success = false; } else { textinput_textbox_delete(c, box, start_idx, end_idx - start_idx); } } } if (clipboard && !gui_commit_clipboard()) success = false; return success; } /** * Break a text box into two * * \param c html content * \param text_box text box to be split * \param char_offset offset (in bytes) at which text box is to be split */ static struct box *textinput_textarea_insert_break(struct content *c, struct box *text_box, size_t char_offset) { struct box *new_br, *new_text; char *text; text = talloc_array(c, char, text_box->length + 1); if (!text) { warn_user("NoMemory", 0); return NULL; } new_br = box_create(NULL, text_box->style, false, 0, 0, text_box->title, 0, c); new_text = talloc(c, struct box); if (!new_text) { warn_user("NoMemory", 0); return NULL; } new_br->type = BOX_BR; box_insert_sibling(text_box, new_br); memcpy(new_text, text_box, sizeof (struct box)); new_text->flags |= CLONE; new_text->text = text; memcpy(new_text->text, text_box->text + char_offset, text_box->length - char_offset); new_text->length = text_box->length - char_offset; text_box->length = char_offset; text_box->width = new_text->width = UNKNOWN_WIDTH; box_insert_sibling(new_br, new_text); return new_text; } /** * Reflow textarea preserving width and height * * \param c html content * \param textarea text area box * \param inline_container container holding text box */ static void textinput_textarea_reflow(struct content *c, struct box *textarea, struct box *inline_container) { int width = textarea->width; int height = textarea->height; assert(c != NULL); if (!layout_inline_container(inline_container, width, textarea, 0, 0, (struct html_content *) c)) warn_user("NoMemory", 0); textarea->width = width; textarea->height = height; layout_calculate_descendant_bboxes(textarea); box_handle_scrollbars(c, textarea, box_hscrollbar_present(textarea), box_vscrollbar_present(textarea)); } /** * Move to the start of the word containing the given character position, * or the start of the preceding word if already at the start of this one. * * \param text UTF-8 text string * \param poffset offset of caret within string (updated on exit) * \param pchars receives the number of characters skipped * \return true iff the start of a word was found before/at the string start */ static bool textinput_word_left(const char *text, size_t *poffset, size_t *pchars) { size_t offset = *poffset; bool success = false; size_t nchars = 0; /* Skip any spaces immediately prior to the offset */ while (offset > 0) { offset = utf8_prev(text, offset); nchars++; if (!isspace(text[offset])) break; } /* Now skip all non-space characters */ while (offset > 0) { size_t prev = utf8_prev(text, offset); success = true; if (isspace(text[prev])) break; offset = prev; nchars++; } *poffset = offset; if (pchars) *pchars = nchars; return success; } /** * Move to the start of the first word following the given character position. * * \param text UTF-8 text string * \param len length of string in bytes * \param poffset offset of caret within string (updated on exit) * \param pchars receives the number of characters skipped * \return true iff the start of a word was found before the string end */ static bool textinput_word_right(const char *text, size_t len, size_t *poffset, size_t *pchars) { size_t offset = *poffset; bool success = false; size_t nchars = 0; /* Skip all non-space characters after the offset */ while (offset < len) { if (isspace(text[offset])) break; offset = utf8_next(text, len, offset); nchars++; } /* Now skip all space characters */ while (offset < len) { offset = utf8_next(text, len, offset); nchars++; if (offset < len && !isspace(text[offset])) { success = true; break; } } *poffset = offset; if (pchars) *pchars = nchars; return success; } /** * Adjust scroll offsets so that the caret is visible * * \param c html content where click ocurred * \param textarea textarea box * \return true if a change in scroll offsets has occurred */ static bool textinput_ensure_caret_visible(struct content *c, struct box *textarea) { html_content *html = (html_content *)c; int cx, cy; int scrollx, scrolly; assert(textarea->gadget); scrollx = scrollbar_get_offset(textarea->scroll_x); scrolly = scrollbar_get_offset(textarea->scroll_y); /* Calculate the caret coordinates */ cx = textarea->gadget->caret_pixel_offset + textarea->gadget->caret_text_box->x; cy = textarea->gadget->caret_text_box->y; /* Ensure they are visible */ if (textarea->scroll_x == NULL) { scrollx = 0; } else if (cx - scrollbar_get_offset(textarea->scroll_x) < 0) { scrollx = cx; } else if (cx > scrollbar_get_offset(textarea->scroll_x) + textarea->width) { scrollx = cx - textarea->width; } if (textarea->scroll_y == NULL) { scrolly = 0; } else if (cy - scrollbar_get_offset(textarea->scroll_y) < 0) { scrolly = cy; } else if (cy + textarea->gadget->caret_text_box->height > scrollbar_get_offset(textarea->scroll_y) + textarea->height) { scrolly = (cy + textarea->gadget->caret_text_box->height) - textarea->height; } if ((scrollx == scrollbar_get_offset(textarea->scroll_x)) && (scrolly == scrollbar_get_offset(textarea->scroll_y))) return false; if (textarea->scroll_x != NULL) { html->scrollbar = textarea->scroll_x; scrollbar_set(textarea->scroll_x, scrollx, false); html->scrollbar = NULL; } if (textarea->scroll_y != NULL) { html->scrollbar = textarea->scroll_x; scrollbar_set(textarea->scroll_y, scrolly, false); html->scrollbar = NULL; } return true; } /** * Paste a block of text into a textarea at the * current caret position. * * \param bw browser window * \param utf8 pointer to block of text * \param utf8_len length (bytes) of text block * \param last true iff this is the last chunk (update screen too) * \param p1 pointer to textarea * \param p2 html content with the text area box * \return true iff successful */ bool textinput_textarea_paste_text(struct browser_window *bw, const char *utf8, unsigned utf8_len, bool last, void *p1, void *p2) { struct box *textarea = p1; struct content *c = p2; struct box *inline_container = textarea->gadget->caret_inline_container; struct box *text_box = textarea->gadget->caret_text_box; size_t char_offset = textarea->gadget->caret_box_offset; int pixel_offset = textarea->gadget->caret_pixel_offset; const char *ep = utf8 + utf8_len; const char *p = utf8; bool success = true; bool update = last; while (p < ep) { struct box *new_text; unsigned utf8_len; while (p < ep) { if (*p == '\n' || *p == '\r') break; p++; } utf8_len = p - utf8; if (!textinput_textbox_insert(c, text_box, char_offset, utf8, utf8_len)) return false; char_offset += utf8_len; if (p == ep) break; new_text = textinput_textarea_insert_break(c, text_box, char_offset); if (!new_text) { /* we still need to update the screen */ update = true; success = false; break; } /* place caret at start of new text box */ text_box = new_text; char_offset = 0; /* handle CR/LF and LF/CR terminations */ if ((*p == '\n' && p[1] == '\r') || (*p == '\r' && p[1] == '\n')) p++; utf8 = ++p; } // textarea->gadget->caret_inline_container = inline_container; textarea->gadget->caret_text_box = text_box; textarea->gadget->caret_box_offset = char_offset; if (update) { int box_x, box_y; plot_font_style_t fstyle; /* reflow textarea preserving width and height */ textinput_textarea_reflow(c, textarea, inline_container); /* reflowing may have broken our caret offset * this bit should hopefully continue to work if * textarea_reflow is fixed to update the caret itself */ char_offset = textarea->gadget->caret_box_offset; text_box = textarea->gadget->caret_text_box; while ((char_offset > text_box->length + SPACE_LEN(text_box)) && (text_box->next) && (text_box->next->type == BOX_TEXT)) { #ifdef TEXTINPUT_DEBUG LOG(("Caret out of range: Was %d in boxlen %d " "space %d", char_offset, text_box->length, SPACE_LEN(text_box))); #endif char_offset -= text_box->length + SPACE_LEN(text_box); text_box = text_box->next; } /* not sure if this will happen or not... * but won't stick an assert here as we can recover from it */ if (char_offset > text_box->length) { #ifdef TEXTINPUT_DEBUG LOG(("Caret moved beyond end of line: " "Was %d in boxlen %d", char_offset, text_box->length)); #endif char_offset = text_box->length; } textarea->gadget->caret_text_box = text_box; textarea->gadget->caret_box_offset = char_offset; font_plot_style_from_css(text_box->style, &fstyle); nsfont.font_width(&fstyle, text_box->text, char_offset, &pixel_offset); textarea->gadget->caret_pixel_offset = pixel_offset; box_coords(textarea, &box_x, &box_y); box_x += scrollbar_get_offset(textarea->scroll_x); box_y += scrollbar_get_offset(textarea->scroll_y); textinput_ensure_caret_visible(c, textarea); box_x -= scrollbar_get_offset(textarea->scroll_x); box_y -= scrollbar_get_offset(textarea->scroll_y); browser_window_place_caret(bw, box_x + inline_container->x + text_box->x + pixel_offset, box_y + inline_container->y + text_box->y, text_box->height, textinput_textarea_callback, textinput_textarea_paste_text, textinput_textarea_move_caret, textarea, c); html__redraw_a_box(c, textarea); } return success; } /** * Move caret to new position after reformatting * * \param bw browser window * \param p1 pointer textarea box * \param p2 html content with the text area box * \return none */ void textinput_textarea_move_caret(struct browser_window *bw, void *p1, void *p2) { struct box *textarea = p1; struct content *c = p2; struct box *inline_container = textarea->gadget->caret_inline_container; struct box *text_box = textarea->gadget->caret_text_box; size_t char_offset = textarea->gadget->caret_box_offset; int pixel_offset; int box_x, box_y; plot_font_style_t fstyle; font_plot_style_from_css(text_box->style, &fstyle); box_coords(textarea, &box_x, &box_y); box_x -= scrollbar_get_offset(textarea->scroll_x); box_y -= scrollbar_get_offset(textarea->scroll_y); nsfont.font_width(&fstyle, text_box->text, char_offset, &pixel_offset); browser_window_place_caret(bw, box_x + inline_container->x + text_box->x + pixel_offset, box_y + inline_container->y + text_box->y, text_box->height, textinput_textarea_callback, textinput_textarea_paste_text, textinput_textarea_move_caret, textarea, c); } /** * Update display to reflect modified input field * * \param bw browser window * \param input input field * \param form_offset * \param box_offset offset of caret within text box * \param to_textarea caret is to be moved to a textarea * \param redraw force redraw even if field hasn't scrolled */ static void textinput_input_update_display(struct content *c, struct box *input, unsigned box_offset, bool to_textarea, bool redraw) { struct box *text_box = input->children->children; unsigned pixel_offset; int box_x, box_y; int dx; plot_font_style_t fstyle; html_content *html = (html_content *)c; font_plot_style_from_css(text_box->style, &fstyle); if (redraw) nsfont.font_width(&fstyle, text_box->text, text_box->length, &text_box->width); box_coords(input, &box_x, &box_y); nsfont.font_width(&fstyle, text_box->text, box_offset, (int *) &pixel_offset); /* Shift text box horizontally, so caret is visible */ dx = text_box->x; text_box->x = 0; if (input->width < text_box->width && input->width / 2 < (int) pixel_offset) { /* Make caret appear in centre of text input */ text_box->x = input->width / 2 - pixel_offset; /* Clamp if we've shifted too far left */ if (text_box->x < input->width - text_box->width) text_box->x = input->width - text_box->width; } dx -= text_box->x; input->gadget->caret_pixel_offset = pixel_offset; if (to_textarea) { /* moving to textarea so need to set these up */ input->gadget->caret_inline_container = input->children; input->gadget->caret_text_box = text_box; } input->gadget->caret_box_offset = box_offset; browser_window_place_caret(html->bw, box_x + input->children->x + text_box->x + pixel_offset, box_y + input->children->y + text_box->y, text_box->height, /* use the appropriate callback */ to_textarea ? textinput_textarea_callback : textinput_input_callback, to_textarea ? textinput_textarea_paste_text : textinput_input_paste_text, to_textarea ? textinput_textarea_move_caret : textinput_input_move_caret, input, c); if (dx || redraw) html__redraw_a_box(c, input); } /** * Key press callback for text areas. * * \param bw The browser window containing the text area * \param key The ucs4 character codepoint * \param p1 The text area box * \param p2 The html content with the text area box * \return true if the keypress is dealt with, false otherwise. It can * return true even if it ran out of memory; this just means that * it would have claimed it if it could. */ bool textinput_textarea_callback(struct browser_window *bw, uint32_t key, void *p1, void *p2) { struct box *textarea = p1; struct content *c = p2; html_content *html = (html_content *)c; struct box *inline_container = textarea->gadget->caret_inline_container; struct box *text_box = textarea->gadget->caret_text_box; struct box *new_text; size_t char_offset = textarea->gadget->caret_box_offset; int pixel_offset = textarea->gadget->caret_pixel_offset; int box_x, box_y; char utf8[6]; unsigned int utf8_len; bool scrolled, reflow = false; bool selection_exists = html->sel.defined; plot_font_style_t fstyle; /* box_dump(textarea, 0); */ #ifdef TEXTINPUT_DEBUG LOG(("key %i at %i in '%.*s'", key, char_offset, (int) text_box->length, text_box->text)); #endif box_coords(textarea, &box_x, &box_y); box_x -= scrollbar_get_offset(textarea->scroll_x); box_y -= scrollbar_get_offset(textarea->scroll_y); if (!(key <= 0x001F || (0x007F <= key && key <= 0x009F))) { /* normal character insertion */ utf8_len = utf8_from_ucs4(key, utf8); if (!textinput_textbox_insert(c, text_box, char_offset, utf8, utf8_len)) return true; char_offset += utf8_len; reflow = true; } else switch (key) { case KEY_DELETE_LEFT: if (selection_exists) { /* Have a selection; delete it */ textinput_textbox_delete(c, text_box, 0, 0); } else if (char_offset == 0) { /* at the start of a text box */ struct box *prev; if (text_box->prev && text_box->prev->type == BOX_BR) { /* previous box is BR: remove it */ box_unlink_and_free(text_box->prev); } /* This needs to be after the BR removal, as that may * result in no previous box existing */ if (!text_box->prev) /* at very beginning of text area: ignore */ return true; /* delete space by merging with previous text box */ prev = text_box->prev; assert(prev->type == BOX_TEXT); assert(prev->text); char_offset = prev->length; /* caret at join */ if (!textinput_textbox_insert(c, prev, prev->length, text_box->text, text_box->length)) return true; box_unlink_and_free(text_box); /* place caret at join (see above) */ text_box = prev; } else { /* delete a character */ size_t prev_offset = char_offset; size_t new_offset = utf8_prev(text_box->text, char_offset); if (textinput_textbox_delete(c, text_box, new_offset, prev_offset - new_offset)) char_offset = new_offset; } reflow = true; break; case KEY_DELETE_LINE_START: { struct box *start_box = textinput_line_start(text_box); /* Clear the selection, if one exists */ if (selection_exists) selection_clear(&html->sel, false); textinput_textarea_cut(c, start_box, 0, text_box, char_offset, false); text_box = start_box; char_offset = 0; reflow = true; } break; case KEY_DELETE_LINE_END: { struct box *end_box = textinput_line_end(text_box); /* Clear the selection, if one exists */ if (selection_exists) selection_clear(&html->sel, false); if (end_box != text_box || char_offset < text_box->length + SPACE_LEN(text_box)) { /* there's something at the end of the line to delete */ textinput_textarea_cut(c, text_box, char_offset, end_box, end_box->length + SPACE_LEN(end_box), false); reflow = true; break; } } /* no break */ case KEY_DELETE_RIGHT: /* delete to right */ if (selection_exists) { /* Delete selection */ textinput_textbox_delete(c, text_box, 0, 0); } else if (char_offset >= text_box->length) { /* at the end of a text box */ struct box *next; if (text_box->next && text_box->next->type == BOX_BR) { /* next box is a BR: remove it */ box_unlink_and_free(text_box->next); } /* This test is after the BR removal, as that may * result in no subsequent box being present */ if (!text_box->next) /* at very end of text area: ignore */ return true; /* delete space by merging with next text box */ next = text_box->next; assert(next->type == BOX_TEXT); assert(next->text); if (!textinput_textbox_insert(c, text_box, text_box->length, next->text, next->length)) return true; box_unlink_and_free(next); /* leave caret at join */ } else { /* delete a character */ size_t next_offset = utf8_next(text_box->text, text_box->length, char_offset); textinput_textbox_delete(c, text_box, char_offset, next_offset - char_offset); } reflow = true; break; case KEY_NL: case KEY_CR: /* paragraph break */ if (selection_exists) { /* If we have a selection, then delete it, * so it's replaced by the break */ textinput_textbox_delete(c, text_box, 0, 0); } new_text = textinput_textarea_insert_break(c, text_box, char_offset); if (!new_text) return true; /* place caret at start of new text box */ text_box = new_text; char_offset = 0; reflow = true; break; case KEY_CUT_LINE: { struct box *start_box = textinput_line_start(text_box); struct box *end_box = textinput_line_end(text_box); /* Clear the selection, if one exists */ if (selection_exists) selection_clear(&html->sel, false); textinput_textarea_cut(c, start_box, 0, end_box, end_box->length, false); text_box = start_box; char_offset = 0; reflow = true; } break; case KEY_PASTE: gui_paste_from_clipboard(bw->window, box_x + inline_container->x + text_box->x + pixel_offset, box_y + inline_container->y + text_box->y); /* screen updated and caret repositioned already */ return true; case KEY_CUT_SELECTION: { size_t start_idx, end_idx; struct box *start_box = selection_get_start(&html->sel, &start_idx); struct box *end_box = selection_get_end(&html->sel, &end_idx); if (start_box && end_box) { selection_clear(&html->sel, false); textinput_textarea_cut(c, start_box, start_idx, end_box, end_idx, true); text_box = start_box; char_offset = start_idx; reflow = true; } } break; case KEY_RIGHT: if (selection_exists) { /* In selection, move caret to end */ text_box = selection_get_end(&html->sel, &char_offset); } else if (char_offset < text_box->length) { /* Within-box movement */ char_offset = utf8_next(text_box->text, text_box->length, char_offset); } else { /* Between-box movement */ if (!text_box->next) /* at end of text area: ignore */ return true; text_box = text_box->next; if (text_box->type == BOX_BR) text_box = text_box->next; char_offset = 0; } break; case KEY_LEFT: if (selection_exists) { /* In selection, move caret to start */ text_box = selection_get_start(&html->sel, &char_offset); } else if (char_offset > 0) { /* Within-box movement */ char_offset = utf8_prev(text_box->text, char_offset); } else { /* Between-box movement */ if (!text_box->prev) /* at start of text area: ignore */ return true; text_box = text_box->prev; if (text_box->type == BOX_BR) text_box = text_box->prev; char_offset = text_box->length; } break; case KEY_UP: selection_clear(&html->sel, true); textinput_textarea_click(c, BROWSER_MOUSE_CLICK_1, textarea, box_x, box_y, text_box->x + pixel_offset, inline_container->y + text_box->y - 1); return true; case KEY_DOWN: selection_clear(&html->sel, true); textinput_textarea_click(c, BROWSER_MOUSE_CLICK_1, textarea, box_x, box_y, text_box->x + pixel_offset, inline_container->y + text_box->y + text_box->height + 1); return true; case KEY_LINE_START: text_box = textinput_line_start(text_box); char_offset = 0; break; case KEY_LINE_END: text_box = textinput_line_end(text_box); char_offset = text_box->length; break; case KEY_TEXT_START: assert(text_box->parent); /* place caret at start of first box */ text_box = text_box->parent->children; char_offset = 0; break; case KEY_TEXT_END: assert(text_box->parent); /* place caret at end of last box */ text_box = text_box->parent->last; char_offset = text_box->length; break; case KEY_WORD_LEFT: { bool start_of_word; /* if there is a selection, caret should stay at beginning */ if (selection_exists) break; start_of_word = (char_offset <= 0 || isspace(text_box->text[char_offset - 1])); while (!textinput_word_left(text_box->text, &char_offset, NULL)) { struct box *prev = NULL; assert(char_offset == 0); if (start_of_word) { /* find the preceding non-BR box */ prev = text_box->prev; if (prev && prev->type == BOX_BR) prev = prev->prev; } if (!prev) { /* just stay at the start of this box */ break; } assert(prev->type == BOX_TEXT); text_box = prev; char_offset = prev->length; } } break; case KEY_WORD_RIGHT: { bool in_word; /* if there is a selection, caret should move to the end */ if (selection_exists) { text_box = selection_get_end(&html->sel, &char_offset); break; } in_word = (char_offset < text_box->length && !isspace(text_box->text[char_offset])); while (!textinput_word_right(text_box->text, text_box->length, &char_offset, NULL)) { struct box *next = text_box->next; /* find the next non-BR box */ if (next && next->type == BOX_BR) next = next->next; if (!next) { /* just stay at the end of this box */ char_offset = text_box->length; break; } assert(next->type == BOX_TEXT); text_box = next; char_offset = 0; if (in_word && text_box->length > 0 && !isspace(text_box->text[0])) { /* just stay at the start of this box */ break; } } } break; case KEY_PAGE_UP: { int nlines = (textarea->height / text_box->height) - 1; while (nlines-- > 0) text_box = textinput_line_above(text_box); if (char_offset > text_box->length) char_offset = text_box->length; } break; case KEY_PAGE_DOWN: { int nlines = (textarea->height / text_box->height) - 1; while (nlines-- > 0) text_box = textinput_line_below(text_box); /* vague attempt to keep the caret at the same horizontal * position, given that the code currently cannot support it * being beyond the end of a line */ if (char_offset > text_box->length) char_offset = text_box->length; } break; default: return false; } /* box_dump(textarea, 0); for (struct box *t = inline_container->children; t; t = t->next) { assert(t->type == BOX_TEXT); assert(t->text); assert(t->parent == inline_container); if (t->next) assert(t->next->prev == t); if (t->prev) assert(t->prev->next == t); if (!t->next) { assert(inline_container->last == t); break; } if (t->next->type == BOX_BR) { assert(t->next->next); t = t->next; } } */ if (reflow) textinput_textarea_reflow(c, textarea, inline_container); if (text_box->length + SPACE_LEN(text_box) <= char_offset) { if (text_box->next && text_box->next->type == BOX_TEXT) { /* the text box has been split when reflowing and the caret is in the second part */ char_offset -= (text_box->length + SPACE_LEN(text_box)); text_box = text_box->next; assert(text_box); assert(char_offset <= text_box->length); /* Scroll back to the left */ if (textarea->scroll_x != NULL) { box_x += scrollbar_get_offset( textarea->scroll_x); scrollbar_set(textarea->scroll_x, 0, false); } } else { assert(!text_box->next || (text_box->next && text_box->next->type == BOX_BR)); char_offset = text_box->length + SPACE_LEN(text_box); } } font_plot_style_from_css(text_box->style, &fstyle); nsfont.font_width(&fstyle, text_box->text, char_offset, &pixel_offset); selection_clear(&html->sel, true); textarea->gadget->caret_inline_container = inline_container; textarea->gadget->caret_text_box = text_box; textarea->gadget->caret_box_offset = char_offset; textarea->gadget->caret_pixel_offset = pixel_offset; box_x += scrollbar_get_offset(textarea->scroll_x); box_y += scrollbar_get_offset(textarea->scroll_y); scrolled = textinput_ensure_caret_visible(c, textarea); box_x -= scrollbar_get_offset(textarea->scroll_x); box_y -= scrollbar_get_offset(textarea->scroll_y); browser_window_place_caret(bw, box_x + inline_container->x + text_box->x + pixel_offset, box_y + inline_container->y + text_box->y, text_box->height, textinput_textarea_callback, textinput_textarea_paste_text, textinput_textarea_move_caret, textarea, c); if (scrolled || reflow) html__redraw_a_box(c, textarea); return true; } /** * Handle clicks in a text area by placing the caret. * * \param c html content where click occurred * \param mouse state of mouse buttons and modifier keys * \param textarea textarea box * \param box_x position of textarea in global document coordinates * \param box_y position of textarea in global document coordinates * \param x coordinate of click relative to textarea * \param y coordinate of click relative to textarea */ void textinput_textarea_click(struct content *c, browser_mouse_state mouse, struct box *textarea, int box_x, int box_y, int x, int y) { /* A textarea is an INLINE_BLOCK containing a single * INLINE_CONTAINER, which contains the text as runs of TEXT * separated by BR. There is at least one TEXT. The first and * last boxes are TEXT. Consecutive BR may not be present. These * constraints are satisfied by using a 0-length TEXT for blank * lines. */ int char_offset = 0, pixel_offset = 0; struct box *inline_container = textarea->children; struct box *text_box; bool scrolled; html_content *html = (html_content *)c; text_box = textinput_textarea_get_position(textarea, x, y, &char_offset, &pixel_offset); textarea->gadget->caret_inline_container = inline_container; textarea->gadget->caret_text_box = text_box; textarea->gadget->caret_box_offset = char_offset; textarea->gadget->caret_pixel_offset = pixel_offset; box_x += scrollbar_get_offset(textarea->scroll_x); box_y += scrollbar_get_offset(textarea->scroll_y); scrolled = textinput_ensure_caret_visible(c, textarea); box_x -= scrollbar_get_offset(textarea->scroll_x); box_y -= scrollbar_get_offset(textarea->scroll_y); browser_window_place_caret(html->bw, box_x + inline_container->x + text_box->x + pixel_offset, box_y + inline_container->y + text_box->y, text_box->height, textinput_textarea_callback, textinput_textarea_paste_text, textinput_textarea_move_caret, textarea, c); if (scrolled) html__redraw_a_box(c, textarea); } /** * Paste a block of text into an input field at the caret position. * * \param bw browser window * \param utf8 pointer to block of text * \param utf8_len length (bytes) of text block * \param last true iff this is the last chunk (update screen too) * \param p1 pointer to input box * \param p2 html content with the input box * \return true iff successful */ bool textinput_input_paste_text(struct browser_window *bw, const char *utf8, unsigned utf8_len, bool last, void *p1, void *p2) { struct box *input = p1; struct content *c = p2; struct box *text_box = input->children->children; size_t box_offset = input->gadget->caret_box_offset; unsigned int nchars = utf8_length(input->gadget->value); const char *ep = utf8 + utf8_len; const char *p = utf8; bool success = true; bool update = last; /* keep adding chars until we've run out or would exceed the maximum length of the field (in which we silently ignore all others) */ while (p < ep && nchars < input->gadget->maxlength) { char buf[80 + 6]; int nbytes = 0; /* how many more chars can we insert in one go? */ while (p < ep && nbytes < 80 && nchars < input->gadget->maxlength && *p != '\n' && *p != '\r') { unsigned len = utf8_next(p, ep - p, 0); if (*p == ' ') nbytes += utf8_from_ucs4(160, &buf[nbytes]); else { memcpy(&buf[nbytes], p, len); nbytes += len; } p += len; nchars++; } if (!textinput_textbox_insert(c, text_box, box_offset, buf, nbytes)) { /* we still need to update the screen */ update = true; success = false; break; } box_offset += nbytes; /* Keep caret_form_offset in sync -- textbox_insert uses this * to determine where to insert into the gadget's value */ input->gadget->caret_form_offset += nbytes; /* handle CR/LF and LF/CR terminations */ if (*p == '\n') { p++; if (*p == '\r') p++; } else if (*p == '\r') { p++; if (*p == '\n') p++; } } if (update) textinput_input_update_display(c, input, box_offset, false, true); return success; } /** * Move caret to new position after reformatting * * \param bw browser window * \param p1 pointer to text input box * \param p2 html content with the input box * \return none */ void textinput_input_move_caret(struct browser_window *bw, void *p1, void *p2) { struct box *input = (struct box *)p1; struct content *c = p2; struct box *text_box = input->children->children; unsigned int box_offset = input->gadget->caret_box_offset; int pixel_offset; int box_x, box_y; plot_font_style_t fstyle; font_plot_style_from_css(text_box->style, &fstyle); box_coords(input, &box_x, &box_y); nsfont.font_width(&fstyle, text_box->text, box_offset, &pixel_offset); browser_window_place_caret(bw, box_x + input->children->x + text_box->x + pixel_offset, box_y + input->children->y + text_box->y, text_box->height, textinput_input_callback, textinput_input_paste_text, textinput_input_move_caret, input, c); } /** * Key press callback for text or password input boxes. * * \param bw The browser window containing the input box * \param key The UCS4 character codepoint * \param p1 The input box * \param p2 The html content with the input box * \return true if the keypress is dealt with, false otherwise. It can * return true even if it ran out of memory; this just means that * it would have claimed it if it could. */ bool textinput_input_callback(struct browser_window *bw, uint32_t key, void *p1, void *p2) { struct box *input = (struct box *)p1; struct content *c = p2; html_content *html = (html_content *)c; struct box *text_box = input->children->children; size_t box_offset = input->gadget->caret_box_offset; size_t end_offset; int pixel_offset = input->gadget->caret_pixel_offset; int box_x, box_y; struct form* form = input->gadget->form; bool changed = false; char utf8[6]; unsigned int utf8_len; bool to_textarea = false; bool selection_exists = html->sel.defined; input->gadget->caret_form_offset = textinput_get_form_offset(input, text_box, box_offset); /* update the form offset */ input->gadget->caret_form_offset = textinput_get_form_offset(input, text_box, box_offset); selection_get_end(&html->sel, &end_offset); box_coords(input, &box_x, &box_y); /* normal character insertion */ if (!(key <= 0x001F || (0x007F <= key && key <= 0x009F))) { /* have we exceeded max length of input? */ utf8_len = utf8_length(input->gadget->value); if (utf8_len >= input->gadget->maxlength) return true; utf8_len = utf8_from_ucs4(key, utf8); if (!textinput_textbox_insert(c, text_box, box_offset, utf8, utf8_len)) return true; box_offset += utf8_len; changed = true; } else switch (key) { case KEY_DELETE_LEFT: { int prev_offset, new_offset; if (selection_exists) { textinput_textbox_delete(c, text_box, 0, 0); } else { /* Can't delete left from text box start */ if (box_offset == 0) return true; prev_offset = box_offset; new_offset = utf8_prev(text_box->text, box_offset); if (textinput_textbox_delete(c, text_box, new_offset, prev_offset - new_offset)) box_offset = new_offset; } changed = true; } break; case KEY_DELETE_RIGHT: { unsigned next_offset; if (selection_exists) { textinput_textbox_delete(c, text_box, 0, 0); } else { /* Can't delete right from text box end */ if (box_offset >= text_box->length) return true; /* Go to the next valid UTF-8 character */ next_offset = utf8_next(text_box->text, text_box->length, box_offset); textinput_textbox_delete(c, text_box, box_offset, next_offset - box_offset); } changed = true; } break; case KEY_TAB: { struct form_control *next_input; /* Find next text entry field that is actually * displayed (i.e. has an associated box) */ for (next_input = input->gadget->next; next_input && ((next_input->type != GADGET_TEXTBOX && next_input->type != GADGET_TEXTAREA && next_input->type != GADGET_PASSWORD) || !next_input->box); next_input = next_input->next) ; if (!next_input) return true; input = next_input->box; box_offset = 0; to_textarea = next_input->type == GADGET_TEXTAREA; } break; case KEY_NL: case KEY_CR: /* Return/Enter hit */ selection_clear(&html->sel, true); if (form) form_submit(bw->current_content, bw, form, 0); return true; case KEY_SHIFT_TAB: { struct form_control *prev_input; /* Find previous text entry field that is actually * displayed (i.e. has an associated box) */ for (prev_input = input->gadget->prev; prev_input && ((prev_input->type != GADGET_TEXTBOX && prev_input->type != GADGET_TEXTAREA && prev_input->type != GADGET_PASSWORD) || !prev_input->box); prev_input = prev_input->prev) ; if (!prev_input) return true; input = prev_input->box; box_offset = 0; to_textarea = prev_input->type == GADGET_TEXTAREA; } break; case KEY_CUT_LINE: /* Clear the selection, if one exists */ if (selection_exists) selection_clear(&html->sel, false); textinput_textarea_cut(c, text_box, 0, text_box, text_box->length, false); box_offset = 0; changed = true; break; case KEY_PASTE: gui_paste_from_clipboard(bw->window, box_x + input->children->x + text_box->x + pixel_offset, box_y + input->children->y + text_box->y); /* screen updated and caret repositioned already */ return true; case KEY_CUT_SELECTION: { size_t start_idx, end_idx; struct box *start_box = selection_get_start(&html->sel, &start_idx); struct box *end_box = selection_get_end(&html->sel, &end_idx); if (start_box && end_box) { selection_clear(&html->sel, false); textinput_textarea_cut(c, start_box, start_idx, end_box, end_idx, true); box_offset = start_idx; changed = true; } } break; case KEY_RIGHT: if (selection_exists) { box_offset = end_offset; break; } if (box_offset < text_box->length) { /* Go to the next valid UTF-8 character */ box_offset = utf8_next(text_box->text, text_box->length, box_offset); } break; case KEY_LEFT: /* If there is a selection, caret should remain at start */ if (selection_exists) break; /* Go to the previous valid UTF-8 character */ box_offset = utf8_prev(text_box->text, box_offset); break; case KEY_LINE_START: box_offset = 0; break; case KEY_LINE_END: box_offset = text_box->length; break; case KEY_WORD_LEFT: /* If there is a selection, caret should remain at start */ if (selection_exists) break; if (!textinput_word_left(text_box->text, &box_offset, NULL)) box_offset = 0; break; case KEY_WORD_RIGHT: if (selection_exists) { box_offset = end_offset; break; } if (!textinput_word_right(text_box->text, text_box->length, &box_offset, NULL)) box_offset = text_box->length; break; case KEY_DELETE_LINE_START: if (selection_exists) selection_clear(&html->sel, true); if (box_offset == 0) return true; textinput_textarea_cut(c, text_box, 0, text_box, box_offset, false); box_offset = 0; changed = true; break; case KEY_DELETE_LINE_END: if (selection_exists) selection_clear(&html->sel, true); if (box_offset >= text_box->length) return true; textinput_textarea_cut(c, text_box, box_offset, text_box, text_box->length, false); changed = true; break; default: return false; } selection_clear(&html->sel, true); textinput_input_update_display(c, input, box_offset, to_textarea, changed); return true; } /** * Handle clicks in a text or password input box by placing the caret. * * \param bw browser window where click occurred * \param input input box * \param box_x position of input in global document coordinates * \param box_y position of input in global document coordinates * \param x coordinate of click relative to input * \param y coordinate of click relative to input */ void textinput_input_click(struct content *c, struct box *input, int box_x, int box_y, int x, int y) { size_t char_offset = 0; int pixel_offset = 0, dx = 0; struct box *text_box = input->children->children; plot_font_style_t fstyle; html_content *html = (html_content *)c; font_plot_style_from_css(text_box->style, &fstyle); nsfont.font_position_in_string(&fstyle, text_box->text, text_box->length, x - text_box->x, &char_offset, &pixel_offset); assert(char_offset <= text_box->length); /* Shift the text box horizontally to ensure that the * caret position is visible, and ideally centred */ text_box->x = 0; if ((input->width < text_box->width) && (input->width / 2 < pixel_offset)) { dx = text_box->x; /* Move left so caret is centred */ text_box->x = input->width / 2 - pixel_offset; /* Clamp, so text box's right hand edge coincides * with the input's right hand edge */ if (text_box->x < input->width - text_box->width) text_box->x = input->width - text_box->width; dx -= text_box->x; } input->gadget->caret_box_offset = char_offset; input->gadget->caret_form_offset = textinput_get_form_offset(input, text_box, char_offset); input->gadget->caret_pixel_offset = pixel_offset; browser_window_place_caret(html->bw, box_x + input->children->x + text_box->x + pixel_offset, box_y + input->children->y + text_box->y, text_box->height, textinput_input_callback, textinput_input_paste_text, textinput_input_move_caret, input, c); if (dx) html__redraw_a_box(c, input); }