/*
 * Copyright 2006 John-Mark Bell <jmb@netsurf-browser.org>
 * Copyright 2009 Paul Blokus <paul_pl@users.sourceforge.net>
 *
 * 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 <http://www.gnu.org/licenses/>.
 */

/**
 * \file
 *
 * Single/Multi-line UTF-8 text area implementation.
 */

#include <stdint.h>
#include <string.h>

#include "utils/log.h"
#include "utils/utf8.h"
#include "utils/utils.h"
#include "netsurf/browser_window.h"
#include "netsurf/plotters.h"
#include "netsurf/mouse.h"
#include "netsurf/keypress.h"
#include "netsurf/clipboard.h"
#include "netsurf/layout.h"
#include "css/utils.h"

#include "desktop/textarea.h"
#include "desktop/scrollbar.h"
#include "desktop/gui_internal.h"

#define CARET_COLOR 0x0000FF
#define TA_ALLOC_STEP 512

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 */
	int width;			/**< Width in pixels of line */
};
struct textarea_drag {
	textarea_drag_type type;
	union {
		struct scrollbar* scrollbar;
	} data;
};

struct textarea_utf8 {
	char *data;		/**< UTF-8 text */
	unsigned int alloc;	/**< Size of allocated text */
	unsigned int len;	/**< Length of text, in bytes */
	unsigned int utf8_len;	/**< Length of text, in characters without
				 *   trailing NULL */
};

struct textarea_undo_detail {
	unsigned int b_start;	/**< Offset to detail's text in undo buffer */
	unsigned int b_end;	/**< End of text (exclusive) */
	unsigned int b_limit;	/**< End of detail space (exclusive) */

	unsigned int b_text_start;	/**< Start of textarea text. */
	unsigned int b_text_end;	/**< End of textarea text (exclusive) */
};

struct textarea_undo {
	unsigned int details_alloc;		/**< Details allocated for */
	unsigned int next_detail;		/**< Next detail pos */
	unsigned int last_detail;		/**< Last detail used */
	struct textarea_undo_detail *details;	/**< Array of undo details */

	struct textarea_utf8 text;
};

struct textarea {

	int scroll_x, scroll_y;		/**< scroll offsets for the textarea */

	struct scrollbar *bar_x;	/**< Horizontal scroll. */
	struct scrollbar *bar_y;	/**< Vertical scroll. */

	unsigned int flags;		/**< Textarea flags */
	int vis_width;			/**< Visible width, in pixels */
	int vis_height;			/**< Visible height, in pixels */

	int pad_top;		/**< Top padding, inside border, in pixels */
	int pad_right;		/**< Right padding, inside border, in pixels */
	int pad_bottom;		/**< Bottom padding, inside border, in pixels */
	int pad_left;		/**< Left padding, inside border, in pixels */

	int border_width;	/**< Border width, in pixels */
	colour border_col;	/**< Border colour */

	int text_y_offset;		/**< Vertical dist to 1st line top */
	int text_y_offset_baseline;	/**< Vertical dist to 1st baseline */

	plot_font_style_t fstyle;	/**< Text style, inc. textarea bg col */
	plot_font_style_t sel_fstyle;	/**< Selected text style */
	int line_height;		/**< Line height obtained from style */

	struct textarea_utf8 text;	/**< Textarea text content */
#define PASSWORD_REPLACEMENT "\xe2\x80\xa2"
#define PASSWORD_REPLACEMENT_W (sizeof(PASSWORD_REPLACEMENT) - 1)
	struct textarea_utf8 password;	/**< Text for obscured display */

	struct textarea_utf8 *show;	/**< Points at .text or .password */

	struct {
		int line;		/**< Line caret is on */
		int byte_off;		/**< Character index of caret on line */
	} caret_pos;

	int caret_x;			/**< cached X coordinate of the caret */
	int caret_y;			/**< cached Y coordinate of the caret */

	int sel_start;	/**< Character index of sel start (inclusive) */
	int sel_end;	/**< Character index of sel end (exclusive) */

	int h_extent;			/**< Width of content in px */
	int v_extent;			/**< Height of content in px */

	int line_count;			/**< Count of lines */

#define LINE_CHUNK_SIZE 32
	struct line_info *lines;	/**< Line info array */
	unsigned int lines_alloc_size;	/**< Number of LINE_CHUNK_SIZEs */

	/** Callback function for messages to client */
	textarea_client_callback callback;

	void *data;		/**< Client data for callback */

	int drag_start;		/**< Byte offset of drag start (in ta->show) */
	struct textarea_drag drag_info;	/**< Drag information */

	struct textarea_undo undo; /**< Undo/redo information */
};



/**
 * 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
 */
static void textarea_normalise_text(struct textarea *ta,
		unsigned int b_start, unsigned int b_len)
{
	bool multi = (ta->flags & TEXTAREA_MULTILINE) ? true : false;
	struct textarea_msg msg;
	unsigned int index;

	/* Remove CR characters. If it's a CRLF pair delete the CR, or replace
	 * CR with LF otherwise.
	 */
	for (index = 0; index < b_len; index++) {
		if (ta->text.data[b_start + index] == '\r') {
			if (b_start + index + 1 <= ta->text.len &&
					ta->text.data[b_start + index + 1] ==
							'\n') {
				ta->text.len--;
				ta->text.utf8_len--;
				memmove(ta->text.data + b_start + index,
						ta->text.data + b_start +
								index + 1,
						ta->text.len - b_start - index);
			}
			else
				ta->text.data[b_start + index] = '\n';
		}
		/* Replace newlines with spaces if this is a single line
		 * textarea.
		 */
		if (!multi && (ta->text.data[b_start + index] == '\n'))
			ta->text.data[b_start + index] = ' ';
	}

	/* Build text modified message */
	msg.ta = ta;
	msg.type = TEXTAREA_MSG_TEXT_MODIFIED;
	msg.data.modified.text = ta->text.data;
	msg.data.modified.len = ta->text.len;

	/* Pass message to client */
	ta->callback(ta->data, &msg);
}


/**
 * Reset the selection (no redraw)
 *
 * \param ta Text area
 */
static inline void textarea_reset_selection(struct textarea *ta)
{
	ta->sel_start = ta->sel_end = -1;
}


/**
 * Get the caret's position
 *
 * \param ta Text area
 * \return 0-based byte offset of caret location, or -1 on error
 */
static int textarea_get_caret(struct textarea *ta)
{
	/* Ensure caret isn't hidden */
	if (ta->caret_pos.byte_off < 0)
		textarea_set_caret(ta, 0);

	/* If the text is a trailing NULL only */
	if (ta->text.utf8_len == 0)
		return 0;

	/* If caret beyond text */
	if (ta->caret_pos.line >= ta->line_count)
		return ta->show->len - 1;

	/* Byte offset of line, plus byte offset of caret on line */
	return ta->lines[ta->caret_pos.line].b_start + ta->caret_pos.byte_off;
}


/**
 * 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
 */
static bool textarea_scroll_visible(struct textarea *ta)
{
	int x0, x1, y0, y1; /* area we want caret inside */
	int x, y; /* caret pos */
	int xs = ta->scroll_x;
	int ys = ta->scroll_y;
	int vis;
	int scrollbar_width;
	bool scrolled = false;

	if (ta->caret_pos.byte_off < 0)
		return false;

	scrollbar_width = (ta->bar_y == NULL) ? 0 : SCROLLBAR_WIDTH;
	x0 = ta->border_width + ta->pad_left;
	x1 = ta->vis_width - (ta->border_width + ta->pad_right);

	/* Adjust scroll pos for reduced extents */
	vis = ta->vis_width - 2 * ta->border_width - scrollbar_width;
	if (ta->h_extent - xs < vis)
		xs -= vis - (ta->h_extent - xs);

	/* Get caret pos on screen */
	x = ta->caret_x - xs;

	/* scroll as required */
	if (x < x0)
		xs += (x - x0);
	else if (x > x1)
		xs += (x - x1);

	if (ta->bar_x == NULL && ta->scroll_x != 0 &&
			ta->flags & TEXTAREA_MULTILINE) {
		/* Scrollbar removed, set to zero */
		ta->scroll_x = 0;
		scrolled = true;

	} else if (xs != ta->scroll_x) {
		/* Scrolled, set new pos. */
		if (ta->bar_x != NULL) {
			scrollbar_set(ta->bar_x, xs, false);
			xs = scrollbar_get_offset(ta->bar_x);
			if (xs != ta->scroll_x) {
				ta->scroll_x = xs;
				scrolled = true;
			}

		} else if (!(ta->flags & TEXTAREA_MULTILINE)) {
			ta->scroll_x = xs;
			scrolled = true;
		}
	}

	/* check and change vertical scroll */
	if (ta->flags & TEXTAREA_MULTILINE) {
		scrollbar_width = (ta->bar_x == NULL) ? 0 : SCROLLBAR_WIDTH;
		y0 = 0;
		y1 = ta->vis_height - 2 * ta->border_width -
				ta->pad_top - ta->pad_bottom;

		/* Adjust scroll pos for reduced extents */
		vis = ta->vis_height - 2 * ta->border_width - scrollbar_width;
		if (ta->v_extent - ys < vis)
			ys -= vis - (ta->v_extent - ys);

		/* Get caret pos on screen */
		y = ta->caret_y - ys;

		/* scroll as required */
		if (y < y0)
			ys += (y - y0);
		else if (y + ta->line_height > y1)
			ys += (y + ta->line_height - y1);

		if (ys != ta->scroll_y && ta->bar_y != NULL) {
			/* Scrolled, set new pos. */
			scrollbar_set(ta->bar_y, ys, false);
			ys = scrollbar_get_offset(ta->bar_y);
			if (ys != ta->scroll_y) {
				ta->scroll_y = ys;
				scrolled = true;
			}

		} else if (ta->bar_y == NULL && ta->scroll_y != 0) {
			/* Scrollbar removed, set to zero */
			ta->scroll_y = 0;
			scrolled = true;
		}
	}

	return scrolled;
}


/**
 * Set the caret position
 *
 * \param ta		Text area
 * \param caret_b	Byte offset to caret
 * \return true iff caret placement caused a scroll
 */
static bool textarea_set_caret_internal(struct textarea *ta, int caret_b)
{
	unsigned int b_off;
	int i;
	int index;
	int x, y;
	int x0, y0, x1, y1;
	int width, height;
	struct textarea_msg msg;
	bool scrolled = false;

	if (caret_b != -1 && caret_b > (signed)(ta->show->len - 1))
		caret_b = ta->show->len - 1;

	/* Delete the old caret */
	if (ta->caret_pos.byte_off != -1 &&
			ta->flags & TEXTAREA_INTERNAL_CARET) {
		x0 = ta->caret_x - ta->scroll_x;
		y0 = ta->caret_y - ta->scroll_y;
		width = 2;
		height = ta->line_height;

		msg.ta = ta;
		msg.type = TEXTAREA_MSG_REDRAW_REQUEST;
		msg.data.redraw.x0 = x0;
		msg.data.redraw.y0 = y0;
		msg.data.redraw.x1 = x0 + width;
		msg.data.redraw.y1 = y0 + height;

		/* Ensure it is hidden */
		ta->caret_pos.byte_off = -1;

		ta->callback(ta->data, &msg);
	}

	/* check if the caret has to be drawn at all */
	if (caret_b != -1) {
		/* Find byte offset of caret position */
		b_off = caret_b;

		/* 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;

		/* Set new caret pos */
		ta->caret_pos.line = i;
		ta->caret_pos.byte_off = b_off - ta->lines[i].b_start;

		/* Finally, redraw the caret */
		index = textarea_get_caret(ta);

		/* find byte offset of caret position */
		b_off = index;

		guit->layout->width(&ta->fstyle,
				ta->show->data +
				ta->lines[ta->caret_pos.line].b_start,
				b_off - ta->lines[ta->caret_pos.line].b_start,
				&x);

		x += ta->border_width + ta->pad_left;
		ta->caret_x = x;
		y = ta->line_height * ta->caret_pos.line + ta->text_y_offset;
		ta->caret_y = y;

		scrolled = textarea_scroll_visible(ta);

		if (!scrolled && ta->flags & TEXTAREA_INTERNAL_CARET) {
			/* Didn't scroll, just moved caret.
			 * Caret is internal caret, redraw it */
			x -= ta->scroll_x;
			y -= ta->scroll_y;
			x0 = max(x - 1, ta->border_width);
			y0 = max(y, 0);
			x1 = min(x + 1, ta->vis_width - ta->border_width);
			y1 = min(y + ta->line_height,
					ta->vis_height);

			width = x1 - x0;
			height = y1 - y0;

			if (width > 0 && height > 0) {
				msg.ta = ta;
				msg.type = TEXTAREA_MSG_REDRAW_REQUEST;
				msg.data.redraw.x0 = x0;
				msg.data.redraw.y0 = y0;
				msg.data.redraw.x1 = x0 + width;
				msg.data.redraw.y1 = y0 + height;

				ta->callback(ta->data, &msg);
			}
		} else if (scrolled && !(ta->flags & TEXTAREA_MULTILINE)) {
			/* Textarea scrolled, whole area needs redraw */
			/* With multi-line textareas, the scrollbar
			 * callback will have requested redraw. */
			msg.ta = ta;
			msg.type = TEXTAREA_MSG_REDRAW_REQUEST;
			msg.data.redraw.x0 = 0;
			msg.data.redraw.y0 = 0;
			msg.data.redraw.x1 = ta->vis_width;
			msg.data.redraw.y1 = ta->vis_height;

			ta->callback(ta->data, &msg);
		}

		if (!(ta->flags & TEXTAREA_INTERNAL_CARET)) {
			/* Tell client where caret should be placed */
			struct rect cr = {
				.x0 = ta->border_width,
				.y0 = ta->border_width,
				.x1 = ta->vis_width - ta->border_width -
						((ta->bar_y == NULL) ?
						0 : SCROLLBAR_WIDTH),
				.y1 = ta->vis_height - ta->border_width -
						((ta->bar_x == NULL) ?
						0 : SCROLLBAR_WIDTH)
			};
			msg.ta = ta;
			msg.type = TEXTAREA_MSG_CARET_UPDATE;
			msg.data.caret.type = TEXTAREA_CARET_SET_POS;
			msg.data.caret.pos.x = x - ta->scroll_x;
			msg.data.caret.pos.y = y - ta->scroll_y;
			msg.data.caret.pos.height = ta->line_height;
			msg.data.caret.pos.clip = &cr;

			ta->callback(ta->data, &msg);
		}

	} else if (!(ta->flags & TEXTAREA_INTERNAL_CARET)) {
		/* Caret hidden, and client is responsible: tell client */
		msg.ta = ta;
		msg.type = TEXTAREA_MSG_CARET_UPDATE;
		msg.data.caret.type = TEXTAREA_CARET_HIDE;

		ta->callback(ta->data, &msg);
	}

	return scrolled;
}


/**
 * Selects a character range in the textarea and redraws it
 *
 * \param ta		Text area
 * \param b_start	First character (inclusive) byte offset
 * \param b_end		Last character (exclusive) byte offset
 * \param force_redraw  Redraw whether selection changed or not
 * \return 		true on success false otherwise
 */
static bool textarea_select(struct textarea *ta, int b_start, int b_end,
		bool force_redraw)
{
	int swap;
	bool pre_existing_selection = (ta->sel_start != -1);
	struct textarea_msg msg;

	if (b_start == b_end) {
		textarea_clear_selection(ta);
		return true;
	}

	/* Ensure start is the beginning of the selection */
	if (b_start > b_end) {
		swap = b_start;
		b_start = b_end;
		b_end = swap;
	}

	if (ta->sel_start == b_start && ta->sel_end == b_end &&
			!force_redraw)
		return true;

	msg.ta = ta;
	msg.type = TEXTAREA_MSG_REDRAW_REQUEST;
	msg.data.redraw.x0 = ta->border_width;
	msg.data.redraw.x1 = ta->vis_width - ta->border_width -
			((ta->bar_y == NULL) ? 0 : SCROLLBAR_WIDTH);

	if (force_redraw) {
		/* Asked to redraw everything */
		msg.data.redraw.y0 = ta->border_width;
		msg.data.redraw.y1 = ta->vis_height - ta->border_width -
				((ta->bar_x == NULL) ? 0 : SCROLLBAR_WIDTH);
	} else {
		/* Try to minimise redraw region */
		unsigned int b_low, b_high;
		int line_start = 0, line_end = 0;

		if (!pre_existing_selection) {
			/* There's a new selection */
			b_low = b_start;
			b_high = b_end;

		} else if (ta->sel_start != b_start && ta->sel_end != b_end) {
			/* Both ends of the selection have moved */
			b_low = (ta->sel_start < b_start) ?
					ta->sel_start : b_start;
			b_high = (ta->sel_end > b_end) ?
					ta->sel_end : b_end;

		} else if (ta->sel_start != b_start) {
			/* Selection start changed */
			if ((signed)ta->sel_start < b_start) {
				b_low = ta->sel_start;
				b_high = b_start;
			} else {
				b_low = b_start;
				b_high = ta->sel_start;
			}

		} else {
			/* Selection end changed */
			if ((signed)ta->sel_end < b_end) {
				b_low = ta->sel_end;
				b_high = b_end;
			} else {
				b_low = b_end;
				b_high = ta->sel_end;
			}
		}

		/* Find redraw start/end lines */
		for (line_end = 0; line_end < ta->line_count - 1; line_end++)
			if (ta->lines[line_end + 1].b_start > b_low) {
				line_start = line_end;
				break;
			}
		for (; line_end < ta->line_count - 1; line_end++)
			if (ta->lines[line_end + 1].b_start > b_high)
				break;

		/* Set vertical redraw range */
		msg.data.redraw.y0 = max(ta->border_width,
				ta->line_height * line_start +
				ta->text_y_offset - ta->scroll_y);
		msg.data.redraw.y1 = min(ta->vis_height - ta->border_width -
				((ta->bar_x == NULL) ? 0 : SCROLLBAR_WIDTH),
				ta->line_height * line_end + ta->text_y_offset +
				ta->line_height - ta->scroll_y);
	}

	ta->callback(ta->data, &msg);

	ta->sel_start = b_start;
	ta->sel_end = b_end;

	if (!pre_existing_selection && ta->sel_start != -1) {
		/* Didn't have a selection before, but do now */
		msg.type = TEXTAREA_MSG_SELECTION_REPORT;

		msg.data.selection.have_selection = true;
		msg.data.selection.read_only = (ta->flags & TEXTAREA_READONLY);

		ta->callback(ta->data, &msg);

		if (!(ta->flags & TEXTAREA_INTERNAL_CARET)) {
			/* Caret hidden, and client is responsible */
			msg.type = TEXTAREA_MSG_CARET_UPDATE;
			msg.data.caret.type = TEXTAREA_CARET_HIDE;

			ta->callback(ta->data, &msg);
		}
	}

	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 textarea * ta)
{
	int caret_pos;
	size_t sel_start, sel_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;
	}

	if (ta->show->len == 0) {
		return false;
	}

	/* Compute byte offset of caret position */
	for (sel_start = (caret_pos > 0 ? caret_pos - 1 : caret_pos);
			sel_start > 0; sel_start--) {
		/* Cache the character offset of the last separator */
		if (strchr(sep, ta->show->data[sel_start]) != NULL) {
			/* Found start,
			 * add one to start to skip over separator */
			sel_start++;
			break;
		}
	}

	/* Search for next separator, if any */
	for (sel_end = caret_pos; sel_end < ta->show->len - 1; sel_end++) {
		if (strchr(sep, ta->show->data[sel_end]) != NULL) {
			break;
		}
	}

	if (sel_start < sel_end) {
		textarea_select(ta, sel_start, sel_end, false);
		return true;
	}

	return false;
}


/**
 * Selects paragraph, at current caret position.
 *
 * \param ta  textarea widget
 * \return True on success, false otherwise
 */
static bool textarea_select_paragraph(struct textarea * ta)
{
	int caret_pos;
	size_t sel_start, sel_end;

	caret_pos = textarea_get_caret(ta);
	if (caret_pos < 0) {
		return false;
	}

	/* Work back from caret, looking for a place to start selection */
	for (sel_start = (caret_pos > 0 ? caret_pos - 1 : caret_pos);
			sel_start > 0; sel_start--) {
		/* Set selection start as character after any new line found */
		if (ta->show->data[sel_start] == '\n') {
			/* Add one to start to skip over separator */
			sel_start++;
			break;
		}
	}

	/* Search for end of selection */
	for (sel_end = caret_pos; sel_end < ta->show->len - 1; sel_end++) {
		if (ta->show->data[sel_end] == '\n') {
			break;
		}
	}

	if (sel_start < sel_end) {
		textarea_select(ta, sel_start, sel_end, false);
		return true;
	}

	return false;
}


/**
 * Callback for scrollbar widget.
 */
static void textarea_scrollbar_callback(void *client_data,
		struct scrollbar_msg_data *scrollbar_data)
{
	struct textarea *ta = client_data;
	struct textarea_msg msg;

	switch(scrollbar_data->msg) {
	case SCROLLBAR_MSG_MOVED:
		/* Scrolled; redraw everything */
		ta->scroll_x = scrollbar_get_offset(ta->bar_x);
		ta->scroll_y = scrollbar_get_offset(ta->bar_y);

		msg.ta = ta;
		msg.type = TEXTAREA_MSG_REDRAW_REQUEST;
		msg.data.redraw.x0 = 0;
		msg.data.redraw.y0 = 0;
		msg.data.redraw.x1 = ta->vis_width;
		msg.data.redraw.y1 = ta->vis_height;

		ta->callback(ta->data, &msg);

		if (!(ta->flags & TEXTAREA_INTERNAL_CARET) &&
				ta->sel_start < 0 &&
				ta->caret_pos.byte_off >= 0) {
			/* Tell client where caret should be placed */
			int x = ta->caret_x - ta->scroll_x;
			int y = ta->caret_y - ta->scroll_y;
			int h = ta->line_height;
			struct rect cr = {
				.x0 = ta->border_width,
				.y0 = ta->border_width,
				.x1 = ta->vis_width - ta->border_width -
						((ta->bar_y == NULL) ?
						0 : SCROLLBAR_WIDTH),
				.y1 = ta->vis_height - ta->border_width -
						((ta->bar_x == NULL) ?
						0 : SCROLLBAR_WIDTH)
			};

			msg.ta = ta;
			msg.type = TEXTAREA_MSG_CARET_UPDATE;

			if ((x >= cr.x0 && x < cr.x1) &&
					(y + h >= cr.y0 && y < cr.y1)) {
				/* Caret inside textarea */
				msg.data.caret.type = TEXTAREA_CARET_SET_POS;
				msg.data.caret.pos.x = x;
				msg.data.caret.pos.y = y;
				msg.data.caret.pos.height = h;
				msg.data.caret.pos.clip = &cr;
			} else {
				/* Caret fully outside textarea */
				msg.data.caret.type = TEXTAREA_CARET_HIDE;
			}

			ta->callback(ta->data, &msg);
		}
		break;

	case SCROLLBAR_MSG_SCROLL_START:
		ta->drag_info.type = TEXTAREA_DRAG_SCROLLBAR;
		ta->drag_info.data.scrollbar = scrollbar_data->scrollbar;

		msg.ta = ta;
		msg.type = TEXTAREA_MSG_DRAG_REPORT;
		msg.data.drag = ta->drag_info.type;

		/* Tell client we're handling a drag */
		ta->callback(ta->data, &msg);
		break;

	case SCROLLBAR_MSG_SCROLL_FINISHED:
		ta->drag_info.type = TEXTAREA_DRAG_NONE;

		msg.ta = ta;
		msg.type = TEXTAREA_MSG_DRAG_REPORT;
		msg.data.drag = ta->drag_info.type;

		/* Tell client we finished handling the drag */
		ta->callback(ta->data, &msg);
		break;

	default:
		break;
	}
}



/**
 * Reflow a single line textarea
 *
 * \param ta 	Textarea widget to reflow
 * \param b_off	0-based byte offset in ta->show's text to start of modification
 * \param r	Modified/reduced to area where redraw is required
 * \return true on success false otherwise
 */
static bool textarea_reflow_singleline(struct textarea *ta, size_t b_off,
		struct rect *r)
{
	int x;
	int shift;
	int retained_width = 0;
	int w = ta->vis_width - 2 * ta->border_width -
			ta->pad_left - ta->pad_right;

	assert(!(ta->flags & TEXTAREA_MULTILINE));

	if (ta->lines == NULL) {
		ta->lines =
			malloc(LINE_CHUNK_SIZE * sizeof(struct line_info));
		if (ta->lines == NULL) {
			LOG("malloc failed");
			return false;
		}
		ta->lines_alloc_size = LINE_CHUNK_SIZE;

		ta->lines[0].b_start = 0;
		ta->lines[0].b_length = 0;
		ta->lines[0].width = 0;
	}

	if (ta->flags & TEXTAREA_PASSWORD &&
			ta->text.utf8_len != ta->password.utf8_len) {
		/* Make password-obscured text have same number of
		 * characters as underlying text */
		unsigned int c, b;
		int diff = ta->text.utf8_len - ta->password.utf8_len;
		unsigned int rep_len = PASSWORD_REPLACEMENT_W;
		unsigned int b_len = ta->text.utf8_len * rep_len + 1;

		if (diff > 0 && b_len > ta->password.alloc) {
			/* Increase password alloaction */
			char *temp = realloc(ta->password.data,
					b_len + TA_ALLOC_STEP);
			if (temp == NULL) {
				LOG("realloc failed");
				return false;
			}

			ta->password.data = temp;
			ta->password.alloc = b_len + TA_ALLOC_STEP;
		}

		b_len--;
		for (c = 0; c < b_len; c += rep_len) {
			for (b = 0; b < rep_len; b++) {
				ta->password.data[c + b] =
						PASSWORD_REPLACEMENT[b];
			}
		}
		ta->password.data[b_len] = '\0';
		ta->password.len = b_len + 1;
		ta->password.utf8_len = ta->text.utf8_len;
	}

	/* Measure new width */
	guit->layout->width(&ta->fstyle, ta->show->data,
			ta->show->len - 1, &x);

	/* Get width of retained text */
	if (b_off != ta->lines[0].b_length) {
		guit->layout->width(&ta->fstyle, ta->show->data,
				b_off, &retained_width);
	} else {
		retained_width = ta->lines[0].width;
	}

	shift = ta->border_width + ta->pad_left - ta->scroll_x;

	r->x0 = max(r->x0, retained_width + shift - 1);
	r->x1 = min(r->x1, max(x, ta->lines[0].width) + shift + 1);

	ta->lines[0].b_start = 0;
	ta->lines[0].b_length = ta->show->len - 1;
	ta->lines[0].width = x;

	if (x > w)
		w = x;

	ta->h_extent = w + ta->pad_left + ta->pad_right;
	ta->line_count = 1;

	return true;
}



/**
 * Reflow a multiline textarea from the given line onwards
 *
 * \param ta		Textarea to reflow
 * \param b_start	0-based byte offset in ta->text to start of modification
 * \param b_length	Byte length of change in textarea text
 * \param r		Modified/reduced to area where redraw is required
 * \return true on success false otherwise
 */
static bool textarea_reflow_multiline(struct textarea *ta,
		const size_t b_start, const int b_length, struct rect *r)
{
	char *text;
	unsigned int len;
	unsigned int start;
	size_t b_off;
	size_t b_start_line_end;
	int x;
	char *space, *para_end;
	unsigned int line; /* line count */
	unsigned int scroll_lines;
	int avail_width;
	int h_extent; /* horizontal extent */
	int v_extent; /* vertical extent */
	bool restart = false;
	bool skip_line = false;

	assert(ta->flags & TEXTAREA_MULTILINE);

	if (ta->lines == NULL) {
		ta->lines = calloc(sizeof(struct line_info), LINE_CHUNK_SIZE);
		if (ta->lines == NULL) {
			LOG("Failed to allocate memory for textarea lines");
			return false;
		}
		ta->lines_alloc_size = LINE_CHUNK_SIZE;
	}

	/* Get line of start of changes */
	for (start = 0; (signed) start < ta->line_count - 1; start++)
		if (ta->lines[start + 1].b_start > b_start)
			break;

	/* Find max number of lines before vertical scrollbar is required */
	scroll_lines = (ta->vis_height - 2 * ta->border_width -
				ta->pad_top - ta->pad_bottom) /
				ta->line_height;

	/* Start on the line before the first change, in case the
	 * modification on this line alters what fits on the line
	 * above.  For example adding a space or deleting text on
	 * a soft-wrapped line */
	if (start != 0)
		start--;

	/* Record original end pos of start line */
	b_start_line_end = ta->lines[start].b_start + ta->lines[start].b_length;

	/* During layout we may decide we need to restart again from the
	 * textarea's first line. */
	do {
		/* If a vertical scrollbar has been added or removed, we need
		 * to restart from the first line in the textarea. */
		if (restart)
			start = 0;

		/* Set current line to the starting line */
		line = start;

		/* Find available width */
		avail_width = ta->vis_width - 2 * ta->border_width -
				ta->pad_left - ta->pad_right;
		if (avail_width < 0)
			avail_width = 0;
		h_extent = avail_width;

		/* Set up length of remaining text and offset to current point
		 * in text.  Initially set it to start of textarea */
		len = ta->text.len - 1;
		text = ta->text.data;

		if (line != 0) {
			/* Not starting at the beginning of the textarea, so
			 * jump forward, and make sure the horizontal extents
			 * accommodate the width of the skipped lines. */
			unsigned int i;
			len -= ta->lines[line].b_start;
			text += ta->lines[line].b_start;

			for (i = 0; i < line; i++) {
				if (ta->lines[i].width > h_extent) {
					h_extent = ta->lines[i].width;
				}
			}
		}

		if (ta->text.len == 1) {
			/* Handle empty textarea */
			assert(ta->text.data[0] == '\0');
			ta->lines[line].b_start = 0;
			ta->lines[line].b_length = 0;
			ta->lines[line++].width = 0;
			ta->line_count = 1;
		}

		restart = false;
		for (; len > 0; len -= b_off, text += b_off) {
			/* Find end of paragraph */
			for (para_end = text; para_end < text + len;
					para_end++) {
				if (*para_end == '\n')
					break;
			}

			/* Wrap current line in paragraph */
			guit->layout->split(&ta->fstyle, text, para_end - text,
					avail_width, &b_off, &x);
			/* b_off now marks space, or end of paragraph */

			if (x > h_extent) {
				h_extent = x;
			}
			if (x > avail_width && ta->bar_x == NULL) {
				/* We need to insert a horizontal scrollbar */
				int w = ta->vis_width - 2 * ta->border_width;
				if (scrollbar_create(true, w, w, w,
						ta, textarea_scrollbar_callback,
						     &(ta->bar_x)) != NSERROR_OK) {
					return false;
				}
				if (ta->bar_y != NULL)
					scrollbar_make_pair(ta->bar_x,
							ta->bar_y);
				ta->pad_bottom += SCROLLBAR_WIDTH;

				/* Find new max visible lines */
				scroll_lines = (ta->vis_height -
						2 * ta->border_width -
						ta->pad_top - ta->pad_bottom) /
						ta->line_height;
			}

			/* Ensure enough storage for lines data */
			if (line > ta->lines_alloc_size - 2) {
				/* Up to two lines my be added in a pass */
				struct line_info *temp = realloc(ta->lines,
						(line + 2 + LINE_CHUNK_SIZE) *
						sizeof(struct line_info));
				if (temp == NULL) {
					LOG("realloc failed");
					return false;
				}

				ta->lines = temp;
				ta->lines_alloc_size = line + 2 +
						LINE_CHUNK_SIZE;
			}

			if (para_end == text + b_off && *para_end == '\n') {
				/* Not found any spaces to wrap at, and we
				 * have a newline char */
				ta->lines[line].b_start = text - ta->text.data;
				ta->lines[line].b_length = para_end - text;
				ta->lines[line++].width = x;

				/* Jump newline */
				b_off++;

				if (len - b_off == 0) {
					/* reached end of input;
					 * add last line */
					ta->lines[line].b_start = text +
							b_off - ta->text.data;
					ta->lines[line].b_length = 0;
					ta->lines[line++].width = x;
				}

				if (line > scroll_lines && ta->bar_y == NULL)
					break;

				continue;

			} else if (len - b_off > 0) {
				/* soft wrapped, 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].b_start = text - ta->text.data;
			ta->lines[line].b_length = b_off;
			ta->lines[line++].width = x;

			if (line > scroll_lines && ta->bar_y == NULL)
				break;
		}

		if (h_extent <= avail_width && ta->bar_x != NULL) {
			/* We need to remove a horizontal scrollbar */
			scrollbar_destroy(ta->bar_x);
			ta->bar_x = NULL;
			ta->pad_bottom -= SCROLLBAR_WIDTH;

			/* Find new max visible lines */
			scroll_lines = (ta->vis_height - 2 * ta->border_width -
					ta->pad_top - ta->pad_bottom) /
					ta->line_height;
		}

		if (line > scroll_lines && ta->bar_y == NULL) {
			/* Add vertical scrollbar */
			int h = ta->vis_height - 2 * ta->border_width;
			if (scrollbar_create(false, h, h, h,
					     ta, textarea_scrollbar_callback,
					     &(ta->bar_y)) != NSERROR_OK) {
				return false;
			}
			if (ta->bar_x != NULL)
				scrollbar_make_pair(ta->bar_x,
						ta->bar_y);
			ta->pad_right += SCROLLBAR_WIDTH;
			restart = true;

		} else if (line <= scroll_lines && ta->bar_y != NULL) {
			/* Remove vertical scrollbar */
			scrollbar_destroy(ta->bar_y);
			ta->bar_y = NULL;
			ta->pad_right -= SCROLLBAR_WIDTH;
			restart = true;
		}
	} while (restart);

	h_extent += ta->pad_left + ta->pad_right -
			(ta->bar_y != NULL ? SCROLLBAR_WIDTH : 0);
	v_extent = line * ta->line_height + ta->pad_top +
				ta->pad_bottom -
				(ta->bar_x != NULL ? SCROLLBAR_WIDTH : 0);

	if (ta->bar_x != NULL) {
		/* Set horizontal scrollbar extents */
		int w = ta->vis_width - 2 * ta->border_width -
				(ta->bar_y != NULL ? SCROLLBAR_WIDTH : 0);
		scrollbar_set_extents(ta->bar_x, w, w, h_extent);
	}

	if (ta->bar_y != NULL) {
		/* Set vertical scrollbar extents */
		int h = ta->vis_height - 2 * ta->border_width;
		scrollbar_set_extents(ta->bar_y, h,
				h - (ta->bar_x != NULL ? SCROLLBAR_WIDTH : 0),
				v_extent);
	}

	ta->h_extent = h_extent;
	ta->v_extent = v_extent;
	ta->line_count = line;

	/* Update start line end byte pos, if it's increased */
	if (ta->lines[start].b_start + ta->lines[start].b_length >
			b_start_line_end) {
		b_start_line_end = ta->lines[start].b_start +
				ta->lines[start].b_length;
	}

	/* Don't need to redraw above changes, so update redraw request rect */
	if (b_start_line_end < b_start && restart == false) {
		/* Start line is unchanged */
		start++;
		skip_line = true;
	}

	r->y0 = max(r->y0, (signed)(ta->line_height * start +
			ta->text_y_offset - ta->scroll_y));

	/* Reduce redraw region to single line if possible */
	if ((skip_line || start == 0) &&
			ta->lines[start].b_start + ta->lines[start].b_length >=
			b_start + b_length) {
		size_t b_line_end = ta->lines[start].b_start +
				ta->lines[start].b_length;
		text = ta->text.data + b_line_end;
		if (*text == '\0' || *text == '\n') {
			r->y1 = min(r->y1, (signed)
					(ta->line_height * (start + 1) +
					ta->text_y_offset - ta->scroll_y));
			if (b_start > ta->lines[start].b_start &&
					b_start <= b_line_end) {
				/* Remove unchanged text at start of line
				 * from redraw region */
				int retained_width = 0;
				size_t retain_end = b_start -
						ta->lines[start].b_start;
				text = ta->text.data + ta->lines[start].b_start;

				guit->layout->width(&ta->fstyle, text,
						retain_end, &retained_width);

				r->x0 = max(r->x0,
						retained_width +
						ta->border_width +
						ta->pad_left -
						ta->scroll_x - 1);
			}
		}
	}

	return true;
}


/**
 * get byte offset from the beginning of the text for some coordinates
 *
 * \param ta		textarea widget
 * \param x		X coordinate
 * \param y		Y coordinate
 * \param visible	true iff (x,y) is wrt visiable area, false for global
 * \return byte offset
 */
static size_t textarea_get_b_off_xy(struct textarea *ta, int x, int y,
		bool visible)
{
	size_t bpos; /* Byte position in utf8 string */
	int line;

	if (!ta->line_count) {
		return 0;
	}

	x = x - ta->border_width - ta->pad_left +
			(visible ? ta->scroll_x : 0);
	y = y - ta->border_width - ta->pad_top +
			(visible ? ta->scroll_y : 0);

	if (x < 0)
		x = 0;

	line = y / ta->line_height;

	if (ta->line_count - 1 < line)
		line = ta->line_count - 1;
	if (line < 0)
		line = 0;

	/* Get byte position */
	guit->layout->position(&ta->fstyle,
			ta->show->data + ta->lines[line].b_start,
			ta->lines[line].b_length, x, &bpos, &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 (ta->flags & TEXTAREA_MULTILINE && ta->lines[line].b_length > 1 &&
			bpos == (unsigned)ta->lines[line].b_length &&
			ta->show->data[ta->lines[line].b_start +
					ta->lines[line].b_length - 1] == ' ')
		bpos--;

	/* Set the return byte offset */
	return bpos + ta->lines[line].b_start;
}


/**
 * Set the caret's position
 *
 * \param ta		textarea widget
 * \param x		X coordinate
 * \param y		Y coordinate
 * \param visible	true iff (x,y) is wrt visiable area, false for global
 * \return true iff caret placement caused a scroll
 */
static bool textarea_set_caret_xy(struct textarea *ta, int x, int y,
		bool visible)
{
	unsigned int b_off = textarea_get_b_off_xy(ta, x, y, visible);

	return textarea_set_caret_internal(ta, b_off);
}


/**
 * Insert text into the textarea
 *
 * \param ta		Textarea widget
 * \param text		UTF-8 text to insert
 * \param b_off		0-based byte offset in ta->show's text to insert at
 * \param b_len		Byte length of UTF-8 text
 * \param byte_delta	Updated to change in byte count in textarea (ta->show)
 * \param r		Modified/reduced to area where redraw is required
 * \return false on memory exhaustion, true otherwise
 *
 * Note: b_off must be for ta->show
 */
static bool textarea_insert_text(struct textarea *ta, const char *text,
		size_t b_off, size_t b_len, int *byte_delta, struct rect *r)
{
	int char_delta;
	const size_t show_b_off = b_off;

	if (ta->flags & TEXTAREA_READONLY)
		return true;

	/* If password field, we must convert from ta->password byte offset to
	 * ta->text byte offset */
	if (ta->flags & TEXTAREA_PASSWORD) {
		size_t c_off;

		c_off = utf8_bounded_length(ta->password.data, b_off);
		b_off = utf8_bounded_byte_length(ta->text.data,
				ta->text.len - 1, c_off);
	}

	/* Find insertion point */
	if (b_off > ta->text.len - 1)
		b_off = ta->text.len - 1;

	if (b_len + ta->text.len >= ta->text.alloc) {
		char *temp = realloc(ta->text.data, b_len + ta->text.len +
				TA_ALLOC_STEP);
		if (temp == NULL) {
			LOG("realloc failed");
			return false;
		}

		ta->text.data = temp;
		ta->text.alloc = b_len + ta->text.len + TA_ALLOC_STEP;
	}

	/* Shift text following up */
	memmove(ta->text.data + b_off + b_len, ta->text.data + b_off,
			ta->text.len - b_off);
	/* Insert new text */
	memcpy(ta->text.data + b_off, text, b_len);

	char_delta = ta->text.utf8_len;
	*byte_delta = ta->text.len;

	/* Update lengths, and normalise */
	ta->text.len += b_len;
	ta->text.utf8_len += utf8_bounded_length(text, b_len);
	textarea_normalise_text(ta, b_off, b_len);

	/* Get byte delta */
	if (ta->flags & TEXTAREA_PASSWORD) {
		char_delta = ta->text.utf8_len - char_delta;
		*byte_delta = char_delta * PASSWORD_REPLACEMENT_W;
	} else {
		*byte_delta = ta->text.len - *byte_delta;
	}

	/* See to reflow */
	if (ta->flags & TEXTAREA_MULTILINE) {
		if (!textarea_reflow_multiline(ta, show_b_off, b_len, r))
			return false;
	} else {
		if (!textarea_reflow_singleline(ta, show_b_off, r))
			return false;
	}

	return true;
}


/**
 * Helper for replace_text function converts character offset to byte offset
 *
 * text		utf8 textarea text object
 * start	start character offset
 * end		end character offset
 * b_start	updated to byte offset of start in text
 * b_end	updated to byte offset of end in text
 */
static inline void textarea_char_to_byte_offset(struct textarea_utf8 *text,
		unsigned int start, unsigned int end,
		size_t *b_start, size_t *b_end)
{
	size_t diff = end - start;
	/* find byte offset of replace start */
	for (*b_start = 0; start-- > 0;
			*b_start = utf8_next(text->data, text->len - 1,
					*b_start))
		; /* do nothing */

	/* find byte length of replaced text */
	for (*b_end = *b_start; diff-- > 0;
			*b_end = utf8_next(text->data, text->len - 1, *b_end))
		; /* do nothing */
}


/**
 * Perform actual text replacment in a textarea
 *
 * \param ta		Textarea widget
 * \param b_start	Start byte index of replaced section (inclusive)
 * \param b_end		End byte index of replaced section (exclusive)
 * \param rep		Replacement UTF-8 text to insert
 * \param rep_len	Byte length of replacement UTF-8 text
 * \param add_to_clipboard	True iff replaced text to be added to clipboard
 * \param byte_delta	Updated to change in byte count in textarea (ta->show)
 * \param r		Updated to area where redraw is required
 * \return false on memory exhaustion, true otherwise
 *
 * Note, b_start and b_end must be the byte offsets in ta->show, so in the
 * password textarea case, they are for ta->password.
 */
static bool 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)
{
	int char_delta;
	const size_t show_b_off = b_start;
	*byte_delta = 0;

	if ((ta->flags & TEXTAREA_READONLY) &&
			 !(rep == NULL && rep_len == 0 && add_to_clipboard))
		/* Can't edit if readonly, and we're not just copying */
		return true;

	if (b_start > ta->show->len - 1)
		b_start = ta->show->len - 1;
	if (b_end > ta->show->len - 1)
		b_end = ta->show->len - 1;

	/* Set up initial redraw rect */
	r->x0 = ta->border_width;
	r->y0 = ta->border_width;
	r->x1 = ta->vis_width - ta->border_width -
			((ta->bar_y == NULL) ? 0 : SCROLLBAR_WIDTH);
	r->y1 = ta->vis_height - ta->border_width -
			((ta->bar_x == NULL) ? 0 : SCROLLBAR_WIDTH);

	/* Early exit if just inserting */
	if (b_start == b_end && rep != NULL)
		return textarea_insert_text(ta, rep, b_start, rep_len,
				byte_delta, r);

	if (b_start > b_end)
		return false;

	/* Place CUTs on clipboard */
	if (add_to_clipboard) {
		guit->clipboard->set(ta->show->data + b_start, b_end - b_start,
				NULL, 0);
	}

	if (rep == NULL) {
		/* No replacement text */
		return true;
	}

	/* If password field, we must convert from ta->password byte offset to
	 * ta->text byte offset */
	if (ta->flags & TEXTAREA_PASSWORD) {
		size_t c_start, c_end;

		c_start = utf8_bounded_length(ta->password.data, b_start);
		c_end = c_start;
		c_end += utf8_bounded_length(ta->password.data + b_start,
				b_end - b_start);
		textarea_char_to_byte_offset(&ta->text, c_start, c_end,
				&b_start, &b_end);
	}

	/* Ensure textarea's text buffer is large enough */
	if (rep_len + ta->text.len - (b_end - b_start) >= ta->text.alloc) {
		char *temp = realloc(ta->text.data,
				rep_len + ta->text.len - (b_end - b_start) +
					TA_ALLOC_STEP);
		if (temp == NULL) {
			LOG("realloc failed");
			return false;
		}

		ta->text.data = temp;
		ta->text.alloc = rep_len + ta->text.len - (b_end - b_start) +
				TA_ALLOC_STEP;
	}

	/* Shift text following to new position */
	memmove(ta->text.data + b_start + rep_len, ta->text.data + b_end,
			ta->text.len - b_end);

	/* Insert new text */
	memcpy(ta->text.data + b_start, rep, rep_len);

	char_delta = ta->text.utf8_len;
	*byte_delta = ta->text.len;

	/* Update lengths, and normalise */
	ta->text.len += (int)rep_len - (b_end - b_start);
	ta->text.utf8_len = utf8_length(ta->text.data);
	textarea_normalise_text(ta, b_start, rep_len);

	/* Get byte delta */
	if (ta->flags & TEXTAREA_PASSWORD) {
		char_delta = ta->text.utf8_len - char_delta;
		*byte_delta = char_delta * PASSWORD_REPLACEMENT_W;
	} else {
		*byte_delta = ta->text.len - *byte_delta;
	}

	/* See to reflow */
	if (ta->flags & TEXTAREA_MULTILINE) {
		if (!textarea_reflow_multiline(ta, b_start, *byte_delta, r))
			return false;
	} else {
		if (!textarea_reflow_singleline(ta, show_b_off, r))
			return false;
	}

	return true;
}


/**
 * Update undo buffer by adding any text to be replaced, and allocating
 * space as appropriate.
 *
 * \param ta		Textarea widget
 * \param b_start	Start byte index of replaced section (inclusive)
 * \param b_end		End byte index of replaced section (exclusive)
 * \param rep_len	Byte length of replacement UTF-8 text
 * \return false on memory exhaustion, true otherwise
 */
static bool textarea_copy_to_undo_buffer(struct textarea *ta,
		size_t b_start, size_t b_end, size_t rep_len)
{
	struct textarea_undo *undo;
	size_t b_offset;
	unsigned int len = b_end - b_start;

	undo = &ta->undo;

	if (undo->next_detail == 0)
		b_offset = 0;
	else
		b_offset = undo->details[undo->next_detail - 1].b_start +
			   undo->details[undo->next_detail - 1].b_limit;

	len = len > rep_len ? len : rep_len;

	if (b_offset + len >= undo->text.alloc) {
		/* Need more memory for undo buffer */
		char *temp = realloc(undo->text.data,
				b_offset + len + TA_ALLOC_STEP);
		if (temp == NULL) {
			LOG("realloc failed");
			return false;
		}

		undo->text.data = temp;
		undo->text.alloc = b_offset + len + TA_ALLOC_STEP;
	}

	if (undo->next_detail >= undo->details_alloc) {
		/* Need more memory for undo details */
		struct textarea_undo_detail *temp = realloc(undo->details,
				(undo->next_detail + 128) *
				sizeof(struct textarea_undo_detail));
		if (temp == NULL) {
			LOG("realloc failed");
			return false;
		}

		undo->details = temp;
		undo->details_alloc = undo->next_detail + 128;
	}

	/* Put text into buffer */
	memcpy(undo->text.data + b_offset, ta->text.data + b_start,
			b_end - b_start);

	/* Update next_detail */
	undo->details[undo->next_detail].b_start = b_offset;
	undo->details[undo->next_detail].b_end = b_offset + b_end - b_start;
	undo->details[undo->next_detail].b_limit = len;

	undo->details[undo->next_detail].b_text_start = b_start;

	return true;
}


/**
 * Replace text in a textarea, updating undo buffer.
 *
 * \param ta		Textarea widget
 * \param b_start	Start byte index of replaced section (inclusive)
 * \param b_end		End byte index of replaced section (exclusive)
 * \param rep		Replacement UTF-8 text to insert
 * \param rep_len	Byte length of replacement UTF-8 text
 * \param add_to_clipboard	True iff replaced text to be added to clipboard
 * \param byte_delta	Updated to change in byte count in textarea (ta->show)
 * \param r		Updated to area where redraw is required
 * \return false on memory exhaustion, true otherwise
 *
 * Note, b_start and b_end must be the byte offsets in ta->show, so in the
 * password textarea case, they are for ta->password.
 */
static bool 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)
{
	if (!(b_start != b_end && rep == NULL && add_to_clipboard) &&
			!(ta->flags & TEXTAREA_PASSWORD)) {
		/* Not just copying to clipboard, and not a password field;
		 * Sort out undo buffer. */
		if (textarea_copy_to_undo_buffer(ta, b_start, b_end,
				rep_len) == false)
			return false;
	}

	/* Replace the text in the textarea, and reflow it */
	if (textarea_replace_text_internal(ta, b_start, b_end, rep, rep_len,
			add_to_clipboard, byte_delta, r) == false) {
		return false;
	}

	if (!(b_start != b_end && rep == NULL && add_to_clipboard) &&
			!(ta->flags & TEXTAREA_PASSWORD)) {
		/* Not just copying to clipboard, and not a password field;
		 * Update UNDO buffer */
		ta->undo.details[ta->undo.next_detail].b_text_end =
				b_end + *byte_delta;
		ta->undo.last_detail = ta->undo.next_detail;
		ta->undo.next_detail++;
	}

	return true;
}


/**
 * Undo or redo previous change.
 *
 * \param ta		Textarea widget
 * \param forward	Iff true, redo, else undo
 * \param caret		Updated to new caret pos in textarea (ta->show)
 * \param r		Updated to area where redraw is required
 * \return false if nothing to undo/redo, true otherwise
 */
static bool textarea_undo(struct textarea *ta, bool forward,
		unsigned int *caret, struct rect *r)
{
	unsigned int detail_n;
	struct textarea_undo_detail *detail;
	char *temp = NULL;
	unsigned int b_len;
	unsigned int b_text_len;
	int byte_delta;

	if (ta->flags & TEXTAREA_PASSWORD || ta->flags & TEXTAREA_READONLY)
		/* No undo/redo for password or readonly fields */
		return false;

	if (forward) {
		/* Redo */
		if (ta->undo.next_detail > ta->undo.last_detail)
			/* Nothing to redo */
			return false;

		detail_n = ta->undo.next_detail;
	} else {
		/* Undo */
		if (ta->undo.next_detail == 0)
			/* Nothing to undo */
			return false;

		detail_n = ta->undo.next_detail - 1;
	}

	detail = &(ta->undo.details[detail_n]);

	b_len = detail->b_end - detail->b_start;
	b_text_len = detail->b_text_end - detail->b_text_start;

	/* Take copy of any current textarea text that undo/redo will remove */
	if (detail->b_text_end > detail->b_text_start) {
		temp = malloc(b_text_len);
		if (temp == NULL) {
			/* TODO */
			return false;
		}

		memcpy(temp, ta->text.data + detail->b_text_start, b_text_len);
	}

	/* Replace textarea text with undo buffer text */
	textarea_replace_text_internal(ta,
			detail->b_text_start, detail->b_text_end,
			ta->undo.text.data + detail->b_start, b_len,
			false, &byte_delta, r);

	/* Update undo buffer for redo */
	if (temp != NULL)
		memcpy(ta->undo.text.data + detail->b_start, temp, b_text_len);

	detail->b_text_end = detail->b_text_start + b_len;
	detail->b_end = detail->b_start + b_text_len;

	*caret = detail->b_text_end;

	if (forward) {
		/* Redo */
		ta->undo.next_detail++;
	} else {
		/* Undo */
		ta->undo.next_detail--;
	}

	free(temp);

	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
 */
static bool textarea_drag_end(struct textarea *ta, browser_mouse_state mouse,
		int x, int y)
{
	size_t b_end;
	struct textarea_msg msg;

	assert(ta->drag_info.type != TEXTAREA_DRAG_NONE);

	switch (ta->drag_info.type) {
	case TEXTAREA_DRAG_SCROLLBAR:
		if (ta->drag_info.data.scrollbar == ta->bar_x) {
			x -= ta->border_width;
			y -= ta->vis_height - ta->border_width -
					SCROLLBAR_WIDTH;
		} else {
			x -= ta->vis_width - ta->border_width -
					SCROLLBAR_WIDTH;
			y -= ta->border_width;
		}
		scrollbar_mouse_drag_end(ta->drag_info.data.scrollbar,
				mouse, x, y);
		assert(ta->drag_info.type == TEXTAREA_DRAG_NONE);

		/* Return, since drag end already reported to textarea client */
		return true;

	case TEXTAREA_DRAG_SELECTION:
		ta->drag_info.type = TEXTAREA_DRAG_NONE;

		b_end = textarea_get_b_off_xy(ta, x, y, true);

		if (!textarea_select(ta, ta->drag_start, b_end, false))
			return false;

		break;

	default:
		return false;
	}

	/* Report drag end to client, if not already reported */
	assert(ta->drag_info.type == TEXTAREA_DRAG_NONE);

	msg.ta = ta;
	msg.type = TEXTAREA_MSG_DRAG_REPORT;
	msg.data.drag = ta->drag_info.type;

	ta->callback(ta->data, &msg);

	return true;
}


/**
 * Setup text offsets after height / border / padding change
 *
 * \param ta	Textarea widget
 */
static void textarea_setup_text_offsets(struct textarea *ta)
{
	int text_y_offset, text_y_offset_baseline;

	text_y_offset = text_y_offset_baseline = ta->border_width;
	if (ta->flags & TEXTAREA_MULTILINE) {
		/* Multiline textarea */
		text_y_offset += ta->pad_top;
		text_y_offset_baseline +=
				(ta->line_height * 3 + 2) / 4 + ta->pad_top;
	} else {
		/* Single line text area; text is vertically centered */
		int vis_height = ta->vis_height - 2 * ta->border_width;
		text_y_offset += (vis_height - ta->line_height + 1) / 2;
		text_y_offset_baseline +=
				(2 * vis_height + ta->line_height + 2) / 4;
	}

	ta->text_y_offset = text_y_offset;
	ta->text_y_offset_baseline = text_y_offset_baseline;
}



/* exported interface, documented in textarea.h */
struct textarea *textarea_create(const textarea_flags flags,
		const textarea_setup *setup,
		textarea_client_callback callback, void *data)
{
	struct textarea *ret;
	struct rect r = {0, 0, 0, 0};

	/* Sanity check flags */
	assert(!(flags & TEXTAREA_MULTILINE &&
			flags & TEXTAREA_PASSWORD));

	if (callback == NULL) {
		LOG("no callback provided");
		return NULL;
	}

	ret = malloc(sizeof(struct textarea));
	if (ret == NULL) {
		LOG("malloc failed");
		return NULL;
	}

	ret->callback = callback;
	ret->data = data;

	ret->flags = flags;
	ret->vis_width = setup->width;
	ret->vis_height = setup->height;

	ret->pad_top = setup->pad_top;
	ret->pad_right = setup->pad_right;
	ret->pad_bottom = setup->pad_bottom;
	ret->pad_left = setup->pad_left;

	ret->border_width = setup->border_width;
	ret->border_col = setup->border_col;

	ret->fstyle = setup->text;

	ret->sel_fstyle = setup->text;
	ret->sel_fstyle.foreground = setup->selected_text;
	ret->sel_fstyle.background = setup->selected_bg;

	ret->scroll_x = 0;
	ret->scroll_y = 0;
	ret->bar_x = NULL;
	ret->bar_y = NULL;
	ret->h_extent = setup->width;
	ret->v_extent = setup->height;
	ret->drag_start = 0;
	ret->drag_info.type = TEXTAREA_DRAG_NONE;

	ret->undo.details_alloc = 0;
	ret->undo.next_detail = 0;
	ret->undo.last_detail = 0;
	ret->undo.details = NULL;

	ret->undo.text.data = NULL;
	ret->undo.text.alloc = 0;
	ret->undo.text.len = 0;
	ret->undo.text.utf8_len = 0;


	ret->text.data = malloc(TA_ALLOC_STEP);
	if (ret->text.data == NULL) {
		LOG("malloc failed");
		free(ret);
		return NULL;
	}
	ret->text.data[0] = '\0';
	ret->text.alloc = TA_ALLOC_STEP;
	ret->text.len = 1;
	ret->text.utf8_len = 0;

	if (flags & TEXTAREA_PASSWORD) {
		ret->password.data = malloc(TA_ALLOC_STEP);
		if (ret->password.data == NULL) {
			LOG("malloc failed");
			free(ret->text.data);
			free(ret);
			return NULL;
		}
		ret->password.data[0] = '\0';
		ret->password.alloc = TA_ALLOC_STEP;
		ret->password.len = 1;
		ret->password.utf8_len = 0;

		ret->show = &ret->password;

	} else {
		ret->password.data = NULL;
		ret->password.alloc = 0;
		ret->password.len = 0;
		ret->password.utf8_len = 0;

		ret->show = &ret->text;
	}

	ret->line_height = FIXTOINT(FMUL(FLTTOFIX(1.3), FDIV(FMUL(
			nscss_screen_dpi, FDIV(INTTOFIX(setup->text.size),
			INTTOFIX(FONT_SIZE_SCALE))), F_72)));

	ret->caret_pos.line = ret->caret_pos.byte_off = -1;
	ret->caret_x = 0;
	ret->caret_y = 0;
	ret->sel_start = -1;
	ret->sel_end = -1;

	ret->line_count = 0;
	ret->lines = NULL;
	ret->lines_alloc_size = 0;

	textarea_setup_text_offsets(ret);

	if (flags & TEXTAREA_MULTILINE)
		 textarea_reflow_multiline(ret, 0, 0, &r);
	else
		 textarea_reflow_singleline(ret, 0, &r);

	return ret;
}


/* exported interface, documented in textarea.h */
void textarea_destroy(struct textarea *ta)
{
	if (ta->bar_x)
		scrollbar_destroy(ta->bar_x);
	if (ta->bar_y)
		scrollbar_destroy(ta->bar_y);

	if (ta->flags & TEXTAREA_PASSWORD)
		free(ta->password.data);

	free(ta->undo.text.data);
	free(ta->undo.details);

	free(ta->text.data);
	free(ta->lines);
	free(ta);
}


/* exported interface, documented in textarea.h */
bool textarea_set_text(struct textarea *ta, const char *text)
{
	unsigned int len = strlen(text) + 1;
	struct rect r = {0, 0, 0, 0};

	if (len >= ta->text.alloc) {
		char *temp = realloc(ta->text.data, len + TA_ALLOC_STEP);
		if (temp == NULL) {
			LOG("realloc failed");
			return false;
		}
		ta->text.data = temp;
		ta->text.alloc = len + TA_ALLOC_STEP;
	}

	memcpy(ta->text.data, text, len);
	ta->text.len = len;
	ta->text.utf8_len = utf8_length(ta->text.data);

	ta->undo.next_detail = 0;
	ta->undo.last_detail = 0;

	textarea_normalise_text(ta, 0, len);

	if (ta->flags & TEXTAREA_MULTILINE) {
		 if (!textarea_reflow_multiline(ta, 0, len - 1, &r))
		 	return false;
	} else {
		 if (!textarea_reflow_singleline(ta, 0, &r))
		 	return false;
	}

	return true;
}


/* exported interface, documented in textarea.h */
bool textarea_drop_text(struct textarea *ta, const char *text,
		size_t text_length)
{
	struct textarea_msg msg;
	struct rect r;	/**< Redraw rectangle */
	unsigned int caret_pos;
	int byte_delta;

	if (ta->flags & TEXTAREA_READONLY)
		return false;

	if (text == NULL)
		return false;

	caret_pos = textarea_get_caret(ta);

	if (ta->sel_start != -1) {
		if (!textarea_replace_text(ta, ta->sel_start, ta->sel_end,
				text, text_length, false, &byte_delta, &r))
			return false;

		caret_pos = ta->sel_end;
		ta->sel_start = ta->sel_end = -1;
	} else {
		if (!textarea_replace_text(ta, caret_pos, caret_pos,
				text, text_length, false, &byte_delta, &r))
			return false;
	}

	caret_pos += byte_delta;
	textarea_set_caret_internal(ta, caret_pos);

	msg.ta = ta;
	msg.type = TEXTAREA_MSG_REDRAW_REQUEST;
	msg.data.redraw.x0 = 0;
	msg.data.redraw.y0 = 0;
	msg.data.redraw.x1 = ta->vis_width;
	msg.data.redraw.y1 = ta->vis_height;

	ta->callback(ta->data, &msg);

	return true;
}


/* exported interface, documented in textarea.h */
int textarea_get_text(struct textarea *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.data, ta->text.len);

	return ta->text.len;
}


/* exported interface, documented in textarea.h */
bool textarea_set_caret(struct textarea *ta, int caret)
{
	int b_off;

	if (caret < 0) {
		textarea_set_caret_internal(ta, -1);
	} else if (caret == 0) {
		textarea_set_caret_internal(ta, 0);
	} else {
		b_off = utf8_bounded_byte_length(ta->show->data,
				ta->show->len - 1, caret);
		textarea_set_caret_internal(ta, b_off);
	}

	return true;
}


/* exported interface, documented in textarea.h */
void textarea_redraw(struct textarea *ta, int x, int y, colour bg, float scale,
		const struct rect *clip, const struct redraw_context *ctx)
{
	const struct plotter_table *plot = ctx->plot;
	int line0, line1, line, left, right, line_y;
	int text_y_offset, text_y_offset_baseline;
	unsigned int b_pos, b_len, b_len_part, b_end;
	unsigned int sel_start, sel_end;
	char *line_text;
	struct rect r, s;
	bool selected = false;
	plot_font_style_t fstyle;
	int fsize = ta->fstyle.size;
	int line_height = ta->line_height;
	plot_style_t plot_style_fill_bg = {
		.stroke_type = PLOT_OP_TYPE_NONE,
		.stroke_width = 0,
		.stroke_colour = NS_TRANSPARENT,
		.fill_type = PLOT_OP_TYPE_SOLID,
		.fill_colour = ta->border_col
	};

	r = *clip;

	/* Nothing to render if textarea is outside clip rectangle */
	if (r.x1 < x || r.y1 < y)
		return;
	if (scale == 1.0) {
		if (r.x0 > x + ta->vis_width || r.y0 > y + ta->vis_height)
			return;
	} else {
		if (r.x0 > x + ta->vis_width * scale ||
				r.y0 > y + ta->vis_height * scale)
			return;
	}

	if (ta->lines == NULL)
		/* Nothing to redraw */
		return;

	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 (scale == 1.0) {
		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;
	} else {
		if (r.x1 > x + ta->vis_width * scale)
			r.x1 = x + ta->vis_width * scale;
		if (r.y1 > y + ta->vis_height * scale)
			r.y1 = y + ta->vis_height * scale;
	}

	plot->clip(&r);
	if (ta->border_col != NS_TRANSPARENT &&
			ta->border_width > 0) {
		/* Plot border */
		plot->rectangle(x, y, x + ta->vis_width, y + ta->vis_height,
				&plot_style_fill_bg);
	}
	if (ta->fstyle.background != NS_TRANSPARENT) {
		/* Plot background */
		plot_style_fill_bg.fill_colour = ta->fstyle.background;
		plot->rectangle(x + ta->border_width, y + ta->border_width,
				x + ta->vis_width - ta->border_width,
				y + ta->vis_height - ta->border_width,
				&plot_style_fill_bg);
	}

	if (scale == 1.0) {
		if (r.x0 < x + ta->border_width)
			r.x0 = x + ta->border_width;
		if (r.x1 > x + ta->vis_width - ta->border_width)
			r.x1 = x + ta->vis_width - ta->border_width;
		if (r.y0 < y + ta->border_width)
			r.y0 = y + ta->border_width;
		if (r.y1 > y + ta->vis_height - ta->border_width -
				(ta->bar_x != NULL ? SCROLLBAR_WIDTH : 0))
			r.y1 = y + ta->vis_height - ta->border_width -
					(ta->bar_x != NULL ? SCROLLBAR_WIDTH :
							0);
	} else {
		if (r.x0 < x + ta->border_width * scale)
			r.x0 = x + ta->border_width * scale;
		if (r.x1 > x + (ta->vis_width - ta->border_width) * scale)
			r.x1 = x + (ta->vis_width - ta->border_width) * scale;
		if (r.y0 < y + ta->border_width * scale)
			r.y0 = y + ta->border_width * scale;
		if (r.y1 > y + (ta->vis_height - ta->border_width -
				(ta->bar_x != NULL ? SCROLLBAR_WIDTH : 0)) *
				scale)
			r.y1 = y + (ta->vis_height - ta->border_width -
					(ta->bar_x != NULL ? SCROLLBAR_WIDTH :
							0)) * scale;
	}

	if (line0 > 0)
		b_pos = ta->lines[line0].b_start;
	else
		b_pos = 0;

	text_y_offset = ta->text_y_offset;
	text_y_offset_baseline = ta->text_y_offset_baseline;

	if (scale != 1.0) {
		text_y_offset *= scale;
		text_y_offset_baseline *= scale;

		fsize *= scale;
		line_height *= scale;
	}

	plot_style_fill_bg.fill_colour = ta->sel_fstyle.background;

	for (line = line0; (line <= line1) &&
			(y + line * ta->line_height <= r.y1 + ta->scroll_y);
			line++) {
		if (ta->lines[line].b_length == 0) {
			b_pos++;
			continue;
		}

		/* reset clip rectangle */
		plot->clip(&r);

		b_len = ta->lines[line].b_length;

		b_end = 0;
		right = x + ta->border_width + ta->pad_left - ta->scroll_x;

		line_y = line * ta->line_height - ta->scroll_y;

		if (scale != 1.0) {
			line_y *= scale;
		}

		sel_start = ta->sel_start;
		sel_end = ta->sel_end;

		if (ta->sel_end == -1 || ta->sel_end == ta->sel_start ||
				sel_end < ta->lines[line].b_start ||
				sel_start > ta->lines[line].b_start +
						ta->lines[line].b_length) {
			/* Simple case; no selection on this line */
			fstyle = ta->fstyle;
			fstyle.size = fsize;

			plot->text(x + ta->border_width + ta->pad_left -
					ta->scroll_x,
					y + line_y + text_y_offset_baseline,
					ta->show->data +
							ta->lines[line].b_start,
					ta->lines[line].b_length, &fstyle);

			b_pos += b_len;

		} else do {
			/* get length of part of line */
			if (sel_end <= b_pos || sel_start > b_pos + b_len) {
				/* rest of line unselected */
				selected = false;
				b_len_part = b_len;
				fstyle = ta->fstyle;

			} else if (sel_start <= b_pos &&
					sel_end > b_pos + b_len) {
				/* rest of line selected */
				selected = true;
				b_len_part = b_len;
				fstyle = ta->sel_fstyle;

			} else if (sel_start > b_pos) {
				/* next part of line unselected */
				selected = false;
				b_len_part = sel_start - b_pos;
				fstyle = ta->fstyle;

			} else if (sel_end > b_pos) {
				/* next part of line selected */
				selected = true;
				b_len_part = sel_end - b_pos;
				fstyle = ta->sel_fstyle;

			} else {
				assert(0);
			}
			fstyle.size = fsize;

			line_text = &(ta->show->data[ta->lines[line].b_start]);

			/* find b_end for this part of the line */
			b_end += b_len_part;

			/* find clip left/right for this part of line */
			left = right;
			if (b_len_part != b_len) {
				guit->layout->width(&fstyle, line_text, b_end,
						&right);
			} else {
				right = ta->lines[line].width;
				if (scale != 1.0)
					right *= scale;
			}
			right += x + ta->border_width + ta->pad_left -
					ta->scroll_x;

			/* set clip rectangle for line part */
			s = r;

			if (s.x1 <= left || s.x0 > right) {
				/* Skip this span, it's outside the visible */
				b_pos += b_len_part;
				b_len -= b_len_part;
				continue;
			}

			/* Adjust clip rectangle to span limits */
			if (s.x0 < left)
				s.x0 = left;
			if (s.x1 > right)
				s.x1 = right;

			if (right <= left) {
				/* Skip this span, it's outside the visible */
				b_pos += b_len_part;
				b_len -= b_len_part;
				continue;
			}

			plot->clip(&s);

			if (selected) {
				/* draw selection fill */
				plot->rectangle(s.x0, y + line_y +
					text_y_offset,
					s.x1, y + line_y + line_height +
							text_y_offset,
					&plot_style_fill_bg);
			}

			/* draw text */
			plot->text(x + ta->border_width + ta->pad_left -
					ta->scroll_x,
					y + line_y + text_y_offset_baseline,
					ta->show->data +
							ta->lines[line].b_start,
					ta->lines[line].b_length, &fstyle);

			b_pos += b_len_part;
			b_len -= b_len_part;

		} while (b_pos < b_pos + b_len);

		/* if there is a newline between the lines, skip it */
		if (line < ta->line_count - 1 &&
				ta->lines[line + 1].b_start !=
						ta->lines[line].b_start +
						ta->lines[line].b_length)
			b_pos++;
	}

	if (ta->flags & TEXTAREA_INTERNAL_CARET &&
			(ta->sel_end == -1 || ta->sel_start == ta->sel_end) &&
			ta->caret_pos.byte_off >= 0) {
		/* No native caret, there is no selection, and caret visible */
		int caret_y = y - ta->scroll_y + ta->caret_y;

		plot->clip(&r);

		/* Render our own caret */
		plot->line(x - ta->scroll_x + ta->caret_x, caret_y,
				x - ta->scroll_x + ta->caret_x,
				caret_y + ta->line_height,
				&pstyle_stroke_caret);
	}

	plot->clip(clip);

	if (ta->bar_x != NULL)
		scrollbar_redraw(ta->bar_x,
				x / scale + ta->border_width,
				y / scale + ta->vis_height - ta->border_width -
						SCROLLBAR_WIDTH,
				clip, scale, ctx);

	if (ta->bar_y != NULL)
		scrollbar_redraw(ta->bar_y,
				x / scale + ta->vis_width - ta->border_width -
						SCROLLBAR_WIDTH,
				y / scale + ta->border_width,
				clip, scale, ctx);
}


/* exported interface, documented in textarea.h */
bool textarea_keypress(struct textarea *ta, uint32_t key)
{
	struct textarea_msg msg;
	struct rect r;	/**< Redraw rectangle */
	char utf8[6];
	unsigned int caret, length, b_off, b_len;
	int h_extent = ta->h_extent;
	int v_extent = ta->v_extent;
	int line;
	int byte_delta = 0;
	int x, y;
	bool redraw = false;
	bool readonly;
	bool bar_x = ta->bar_x;
	bool bar_y = ta->bar_y;

	/* Word separators */
	static const char *sep = " .\n";

	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 (ta->sel_start != -1) {
			if (!textarea_replace_text(ta,
					ta->sel_start, ta->sel_end, utf8,
					length, false, &byte_delta, &r))
				return false;

			redraw = true;
			caret = ta->sel_end;
			textarea_reset_selection(ta);
		} else {
			if (!textarea_replace_text(ta, caret, caret,
					utf8, length, false, &byte_delta, &r))
				return false;
			redraw = true;
		}
		caret += byte_delta;

	} else switch (key) {
		case NS_KEY_SELECT_ALL:
			textarea_select(ta, 0, ta->show->len - 1, true);
			return true;
		case NS_KEY_COPY_SELECTION:
			if (ta->sel_start != -1) {
				if (!textarea_replace_text(ta,
						ta->sel_start, ta->sel_end,
						NULL, 0, true, &byte_delta, &r))
					return false;
			}
			break;
		case NS_KEY_DELETE_LEFT:
			if (readonly)
				break;
			if (ta->sel_start != -1) {
				if (!textarea_replace_text(ta,
						ta->sel_start, ta->sel_end,
						"", 0, false, &byte_delta, &r))
					return false;

				redraw = true;
				caret = ta->sel_end;
				textarea_reset_selection(ta);
			} else if (caret > 0) {
				b_off = utf8_prev(ta->show->data, caret);
				if (!textarea_replace_text(ta, b_off, caret,
						"", 0, false, &byte_delta, &r))
					return false;
				redraw = true;
			}
			caret += byte_delta;
			break;
		case NS_KEY_DELETE_RIGHT:
			if (readonly)
				break;
			if (ta->sel_start != -1) {
				if (!textarea_replace_text(ta,
						ta->sel_start, ta->sel_end,
						"", 0, false, &byte_delta, &r))
					return false;

				redraw = true;
				caret = ta->sel_end;
				textarea_reset_selection(ta);
			} else if (caret < ta->show->len - 1) {
				b_off = utf8_next(ta->show->data,
						ta->show->len - 1, caret);
				if (!textarea_replace_text(ta, caret, b_off,
						"", 0, false, &byte_delta, &r))
					return false;
				caret = b_off;
				redraw = true;
			}
			caret += byte_delta;
			break;
		case NS_KEY_CR:
		case NS_KEY_NL:
			if (readonly)
				break;

			if (ta->sel_start != -1) {
				if (!textarea_replace_text(ta,
						ta->sel_start, ta->sel_end,
						"\n", 1, false,
						&byte_delta, &r))
					return false;

				redraw = true;
				caret = ta->sel_end;
				textarea_reset_selection(ta);
			} else {
				if (!textarea_replace_text(ta, caret, caret,
						"\n", 1, false,
						&byte_delta, &r))
					return false;
				redraw = true;
			}
			caret += byte_delta;
			break;
		case NS_KEY_PASTE:
		{
			char *clipboard = NULL;
			size_t clipboard_length;

			if (readonly)
				break;

			guit->clipboard->get(&clipboard, &clipboard_length);
			if (clipboard == NULL)
				return false;

			if (ta->sel_start != -1) {
				if (!textarea_replace_text(ta,
						ta->sel_start, ta->sel_end,
						clipboard, clipboard_length,
						false, &byte_delta, &r))
					return false;

				redraw = true;
				caret = ta->sel_end;
				textarea_reset_selection(ta);
			} else {
				if (!textarea_replace_text(ta,
						caret, caret,
						clipboard, clipboard_length,
						false, &byte_delta, &r))
					return false;
				redraw = true;
			}
			caret += byte_delta;

			free(clipboard);
		}
			break;
		case NS_KEY_CUT_SELECTION:
			if (readonly)
				break;
			if (ta->sel_start != -1) {
				if (!textarea_replace_text(ta,
						ta->sel_start, ta->sel_end,
						"", 0, true, &byte_delta, &r))
					return false;

				redraw = true;
				caret = ta->sel_end;
				caret += byte_delta;
				textarea_reset_selection(ta);
			}
			break;
		case NS_KEY_ESCAPE:
			/* Fall through to NS_KEY_CLEAR_SELECTION */
		case NS_KEY_CLEAR_SELECTION:
			return textarea_clear_selection(ta);
		case NS_KEY_LEFT:
			if (readonly)
				break;
			if (caret > 0)
				caret = utf8_prev(ta->show->data, caret);
			if (ta->sel_start != -1) {
				textarea_clear_selection(ta);
			}
			break;
		case NS_KEY_RIGHT:
			if (readonly)
				break;
			if (caret < ta->show->len - 1)
				caret = utf8_next(ta->show->data,
						ta->show->len - 1, caret);
			if (ta->sel_start != -1) {
				textarea_clear_selection(ta);
			}
			break;
		case NS_KEY_UP:
			if (readonly)
				break;
			if (ta->sel_start != -1) {
				textarea_clear_selection(ta);
			}
			if (!(ta->flags & TEXTAREA_MULTILINE))
				break;

			line--;
			if (line < 0)
				line = 0;
			if (line == ta->caret_pos.line)
				break;

			x = ta->caret_x;
			y = ta->text_y_offset_baseline + line * ta->line_height;
			textarea_set_caret_xy(ta, x, y, false);

			return true;
		case NS_KEY_DOWN:
			if (readonly)
				break;
			if (ta->sel_start != -1) {
				textarea_clear_selection(ta);
			}
			if (!(ta->flags & TEXTAREA_MULTILINE))
				break;

			line++;
			if (line > ta->line_count - 1)
				line = ta->line_count - 1;
			if (line == ta->caret_pos.line)
				break;

			x = ta->caret_x;
			y = ta->text_y_offset_baseline + line * ta->line_height;
			textarea_set_caret_xy(ta, x, y, false);

			return true;
		case NS_KEY_PAGE_UP:
			if (!(ta->flags & TEXTAREA_MULTILINE))
				break;
			y = ta->vis_height - 2 * ta->border_width -
					ta->pad_top - ta->pad_bottom -
					ta->line_height;
			textarea_scroll(ta, 0, -y);
			return true;
		case NS_KEY_PAGE_DOWN:
			if (!(ta->flags & TEXTAREA_MULTILINE))
				break;
			y = ta->vis_height - 2 * ta->border_width -
					ta->pad_top - ta->pad_bottom -
					ta->line_height;
			textarea_scroll(ta, 0, y);
			return true;
		case NS_KEY_LINE_START:
			if (readonly)
				break;
			caret -= ta->caret_pos.byte_off;
			if (ta->sel_start != -1) {
				textarea_clear_selection(ta);
			}
			break;
		case NS_KEY_LINE_END:
			if (readonly)
				break;

			caret = ta->lines[line].b_start +
					ta->lines[line].b_length;

			if (!(ta->flags & TEXTAREA_PASSWORD) &&
					caret > 0 &&
					ta->text.data[caret - 1] == ' ')
				caret--;
			if (ta->sel_start != -1) {
				textarea_clear_selection(ta);
			}
			break;
		case NS_KEY_TEXT_START:
			if (readonly)
				break;
			caret = 0;
			if (ta->sel_start != -1) {
				textarea_clear_selection(ta);
			}
			break;
		case NS_KEY_TEXT_END:
			if (readonly)
				break;
			caret = ta->show->len - 1;
			if (ta->sel_start != -1) {
				textarea_clear_selection(ta);
			}
			break;
		case NS_KEY_WORD_LEFT:
			if (readonly)
				break;
			if (caret == 0)
				break;
			caret--;
			while (strchr(sep, ta->show->data[caret]) != NULL &&
					caret > 0)
				caret--;
			for (; caret > 0; caret--) {
				if (strchr(sep, ta->show->data[caret])
						!= NULL) {
					caret++;
					break;
				}
			}
			if (ta->sel_start != -1) {
				textarea_clear_selection(ta);
			}
			break;
		case NS_KEY_WORD_RIGHT:
			if (readonly)
				break;
			if (strchr(sep, ta->show->data[caret]) != NULL &&
					caret < ta->show->len - 1) {
				while (strchr(sep, ta->show->data[caret]) !=
						NULL &&
						caret < ta->show->len - 1) {
					caret++;
				}
				break;
			}
			for (; caret < ta->show->len - 1; caret++) {
				if (strchr(sep, ta->show->data[caret]) != NULL)
					break;
			}
			while (strchr(sep, ta->show->data[caret]) != NULL &&
					caret < ta->show->len - 1)
				caret++;
			if (ta->sel_start != -1) {
				textarea_clear_selection(ta);
			}
			break;
		case NS_KEY_DELETE_LINE:
			if (readonly)
				break;
			if (ta->sel_start != -1) {
				if (!textarea_replace_text(ta,
						ta->sel_start, ta->sel_end,
						"", 0, false, &byte_delta, &r))
					return false;
				redraw = true;
				caret = ta->sel_end;
				textarea_reset_selection(ta);
			} else {
				if (ta->lines[line].b_length != 0) {
					/* Delete line */
					caret = ta->lines[line].b_start;
					b_len = ta->lines[line].b_length;
					if (!textarea_replace_text(ta, caret,
							caret + b_len, "", 0,
							false, &byte_delta, &r))
						return false;
					caret = caret + b_len;
				} else if (caret < ta->show->len - 1) {
					/* Delete blank line */
					if (!textarea_replace_text(ta,
							caret, caret + 1, "", 0,
							false, &byte_delta, &r))
						return false;
					caret++;
				}
				redraw = true;
			}
			caret += byte_delta;
			break;
		case NS_KEY_DELETE_LINE_END:
			if (readonly)
				break;
			if (ta->sel_start != -1) {
				if (!textarea_replace_text(ta,
						ta->sel_start, ta->sel_end,
						"", 0, false, &byte_delta, &r))
					return false;
				redraw = true;
				caret = ta->sel_end;
				textarea_reset_selection(ta);
			} else {
				b_len = ta->lines[line].b_length;
				b_off = ta->lines[line].b_start + b_len;
				if (!textarea_replace_text(ta, caret, b_off,
						"", 0, false, &byte_delta, &r))
					return false;
				caret = b_off;
				redraw = true;
			}
			caret += byte_delta;
			break;
		case NS_KEY_DELETE_LINE_START:
			if (readonly)
				break;
			if (ta->sel_start != -1) {
				if (!textarea_replace_text(ta,
						ta->sel_start, ta->sel_end,
						"", 0, false, &byte_delta, &r))
					return false;
				redraw = true;
				caret = ta->sel_end;
				textarea_reset_selection(ta);
			} else {
				if (!textarea_replace_text(ta,
						caret - ta->caret_pos.byte_off,
						caret, "", 0, false,
						&byte_delta, &r))
					return false;
				redraw = true;
			}
			caret += byte_delta;
			break;
		case NS_KEY_UNDO:
			if (!textarea_undo(ta, false, &caret, &r)) {
				/* We consume the UNDO, even if we can't act
				 * on it. */
				return true;
			}
			if (ta->sel_start != -1) {
				textarea_clear_selection(ta);
			}
			redraw = true;
			break;
		case NS_KEY_REDO:
			if (!textarea_undo(ta, true, &caret, &r)) {
				/* We consume the REDO, even if we can't act
				 * on it. */
				return true;
			}
			if (ta->sel_start != -1) {
				textarea_clear_selection(ta);
			}
			redraw = true;
			break;
		default:
			return false;
	}

	redraw &= ~textarea_set_caret_internal(ta, caret);

	/* TODO: redraw only the bit that changed */
	msg.ta = ta;
	msg.type = TEXTAREA_MSG_REDRAW_REQUEST;

	if (bar_x != (ta->bar_x != NULL) || bar_y != (ta->bar_y != NULL) ||
			h_extent != ta->h_extent || v_extent != ta->v_extent) {
		/* Must redraw since scrollbars have changed */
		msg.data.redraw.x0 = ta->border_width;
		msg.data.redraw.y0 = ta->border_width;
		msg.data.redraw.x1 = ta->vis_width - ta->border_width;
		msg.data.redraw.y1 = ta->vis_height - ta->border_width;
		ta->callback(ta->data, &msg);

	} else if (redraw) {
		msg.data.redraw = r;
		ta->callback(ta->data, &msg);
	}

	return true;
}


/* Handle textarea scrollbar mouse action
 * Helper for textarea_mouse_action()
 *
 * \param ta	Text area
 * \param mouse	the mouse state at action moment
 * \param x	X coordinate
 * \param y	Y coordinate
 * \return textarea mouse state
 */
static textarea_mouse_status textarea_mouse_scrollbar_action(
		struct textarea *ta, browser_mouse_state mouse, int x, int y)
{
	int sx, sy; /* xy coord offset for scrollbar */
	int sl; /* scrollbar length */

	assert(SCROLLBAR_MOUSE_USED == (1 << 0));
	assert(TEXTAREA_MOUSE_SCR_USED == (1 << 3));

	/* Existing scrollbar drag */
	if (ta->drag_info.type == TEXTAREA_DRAG_SCROLLBAR) {
		/* Scrollbar drag in progress; pass input to scrollbar */
		if (ta->drag_info.data.scrollbar == ta->bar_x) {
			x -= ta->border_width;
			y -= ta->vis_height - ta->border_width -
					SCROLLBAR_WIDTH;
		} else {
			x -= ta->vis_width - ta->border_width -
					SCROLLBAR_WIDTH;
			y -= ta->border_width;
		}
		return (scrollbar_mouse_action(ta->drag_info.data.scrollbar,
				mouse, x, y) << 3);
	}

	/* Horizontal scrollbar */
	if (ta->bar_x != NULL && ta->drag_info.type == TEXTAREA_DRAG_NONE) {
		/* No drag happening, but mouse input is over scrollbar;
		 * pass input to scrollbar */
		sx = x - ta->border_width;
		sy = y - (ta->vis_height - ta->border_width - SCROLLBAR_WIDTH);
		sl = ta->vis_width - 2 * ta->border_width -
				(ta->bar_y != NULL ? SCROLLBAR_WIDTH : 0);

		if (sx >= 0 && sy >= 0 && sx < sl && sy < SCROLLBAR_WIDTH) {
			return (scrollbar_mouse_action(ta->bar_x, mouse,
					sx, sy) << 3);
		}
	}

	/* Vertical scrollbar */
	if (ta->bar_y != NULL && ta->drag_info.type == TEXTAREA_DRAG_NONE) {
		/* No drag happening, but mouse input is over scrollbar;
		 * pass input to scrollbar */
		sx = x - (ta->vis_width - ta->border_width - SCROLLBAR_WIDTH);
		sy = y - ta->border_width;
		sl = ta->vis_height - 2 * ta->border_width;

		if (sx >= 0 && sy >= 0 && sx < SCROLLBAR_WIDTH && sy < sl) {
			return (scrollbar_mouse_action(ta->bar_y, mouse,
					sx, sy) << 3);
		}
	}

	return TEXTAREA_MOUSE_NONE;
}


/* exported interface, documented in textarea.h */
textarea_mouse_status textarea_mouse_action(struct textarea *ta,
		browser_mouse_state mouse, int x, int y)
{
	int b_start, b_end;
	unsigned int b_off;
	struct textarea_msg msg;
	textarea_mouse_status status = TEXTAREA_MOUSE_NONE;

	if (ta->drag_info.type != TEXTAREA_DRAG_NONE &&
			mouse == BROWSER_MOUSE_HOVER) {
		/* There is a drag that we must end */
		textarea_drag_end(ta, mouse, x, y);
	}

	/* Mouse action might be a scrollbar's responsibility */
	status = textarea_mouse_scrollbar_action(ta, mouse, x, y);
	if (status != TEXTAREA_MOUSE_NONE) {
		/* Mouse action was handled by a scrollbar */
		return status;
	}

	/* Might be outside textarea, and not dragging */
	if ((x >= ta->vis_width || y >= ta->vis_height) &&
			ta->drag_info.type == TEXTAREA_DRAG_NONE &&
			ta->flags & TEXTAREA_MULTILINE) {
		return status;
	}

	status |= TEXTAREA_MOUSE_EDITOR;

	/* Mouse action is textarea's responsibility */
	if (mouse & BROWSER_MOUSE_DOUBLE_CLICK) {
		/* Select word */
		textarea_set_caret_xy(ta, x, y, true);
		textarea_select_fragment(ta);
		status |= TEXTAREA_MOUSE_USED;

	} else if (mouse & BROWSER_MOUSE_TRIPLE_CLICK) {
		/* Select paragraph */
		textarea_set_caret_xy(ta, x, y, true);
		textarea_select_paragraph(ta);
		status |= TEXTAREA_MOUSE_USED;

	} else if (mouse & BROWSER_MOUSE_PRESS_1) {
		/* Place caret */
		b_off = textarea_get_b_off_xy(ta, x, y, true);
		ta->drag_start = b_off;

		textarea_set_caret_internal(ta, b_off);
		if (ta->sel_start != -1) {
			/* Clear selection */
			textarea_clear_selection(ta);
		}
		status |= TEXTAREA_MOUSE_USED;

	} else if (mouse & BROWSER_MOUSE_PRESS_2) {
		b_off = textarea_get_b_off_xy(ta, x, y, true);

		if (ta->sel_start != -1) {
			/* Adjust selection */
			b_start = (ta->sel_end - ta->sel_start) / 2 +
					ta->sel_start;
			b_start = ((unsigned)b_start > b_off) ?
					ta->sel_end : ta->sel_start;
			ta->drag_start = b_start;
			textarea_select(ta, b_start, b_off, false);
		} else {
			/* Select to caret */
			b_start = textarea_get_caret(ta);
			ta->drag_start = b_start;
			textarea_select(ta, b_start, b_off, false);
		}
		status |= TEXTAREA_MOUSE_USED;

	} else if (mouse & (BROWSER_MOUSE_DRAG_1 | BROWSER_MOUSE_DRAG_2)) {
		/* Selection start */
		b_off = textarea_get_b_off_xy(ta, x, y, true);
		b_start = ta->drag_start;
		b_end = b_off;
		ta->drag_info.type = TEXTAREA_DRAG_SELECTION;

		msg.ta = ta;
		msg.type = TEXTAREA_MSG_DRAG_REPORT;
		msg.data.drag = ta->drag_info.type;

		ta->callback(ta->data, &msg);

		textarea_select(ta, b_start, b_end, false);
		status |= TEXTAREA_MOUSE_USED;

	} else if (mouse &
			(BROWSER_MOUSE_HOLDING_1 | BROWSER_MOUSE_HOLDING_2) &&
			ta->drag_info.type == TEXTAREA_DRAG_SELECTION) {
		/* Selection track */
		int scrx = 0;
		int scry = 0;
		int w, h;
		bool need_redraw = false;

		b_off = textarea_get_b_off_xy(ta, x, y, true);
		b_start = ta->drag_start;
		b_end = b_off;

		w = ta->vis_width - ta->border_width -
				((ta->bar_y == NULL) ? 0 : SCROLLBAR_WIDTH);
		h = ta->vis_height - ta->border_width -
				((ta->bar_x == NULL) ? 0 : SCROLLBAR_WIDTH);

		/* selection auto-scroll */
		if (x < ta->border_width)
			scrx = (x - ta->border_width) / 4;
		else if (x > w)
			scrx = (x - w) / 4;

		if (y < ta->border_width)
			scry = (y - ta->border_width) / 4;
		else if (y > h)
			scry = (y - h) / 4;

		if (scrx || scry)
			need_redraw = textarea_scroll(ta, scrx, scry);

		textarea_select(ta, b_start, b_end, need_redraw);
		status |= TEXTAREA_MOUSE_USED;
	}

	if (ta->sel_start != -1) {
		/* Have selection */
		status |= TEXTAREA_MOUSE_SELECTION;
	}

	return status;
}


/* exported interface, documented in textarea.h */
bool textarea_clear_selection(struct textarea *ta)
{
	struct textarea_msg msg;
	int line_end, line_start = 0;

	if (ta->sel_start == -1)
		/* No selection to clear */
		return false;

	/* Find selection start & end lines */
	for (line_end = 0; line_end < ta->line_count - 1; line_end++)
		if (ta->lines[line_end + 1].b_start > (unsigned)ta->sel_start) {
			line_start = line_end;
			break;
		}
	for (; line_end < ta->line_count - 1; line_end++)
		if (ta->lines[line_end + 1].b_start > (unsigned)ta->sel_end)
			break;

	/* Clear selection and redraw */
	textarea_reset_selection(ta);

	msg.ta = ta;
	msg.type = TEXTAREA_MSG_REDRAW_REQUEST;
	msg.data.redraw.x0 = ta->border_width;
	msg.data.redraw.y0 = max(ta->border_width,
			ta->line_height * line_start +
			ta->text_y_offset - ta->scroll_y);
	msg.data.redraw.x1 = ta->vis_width - ta->border_width -
			((ta->bar_y == NULL) ? 0 : SCROLLBAR_WIDTH);
	msg.data.redraw.y1 = min(ta->vis_height - ta->border_width -
			((ta->bar_x == NULL) ? 0 : SCROLLBAR_WIDTH),
			ta->line_height * line_end + ta->text_y_offset +
			ta->line_height - ta->scroll_y);

	ta->callback(ta->data, &msg);

	/* No more selection */
	msg.type = TEXTAREA_MSG_SELECTION_REPORT;

	msg.data.selection.have_selection = false;
	msg.data.selection.read_only = (ta->flags & TEXTAREA_READONLY);

	ta->callback(ta->data, &msg);

	if (!(ta->flags & TEXTAREA_INTERNAL_CARET)) {
		/* Tell client where caret should be placed */
		struct rect cr = {
			.x0 = ta->border_width,
			.y0 = ta->border_width,
			.x1 = ta->vis_width - ta->border_width -
					((ta->bar_y == NULL) ?
					0 : SCROLLBAR_WIDTH),
			.y1 = ta->vis_height - ta->border_width -
					((ta->bar_x == NULL) ?
					0 : SCROLLBAR_WIDTH)
		};
		msg.ta = ta;
		msg.type = TEXTAREA_MSG_CARET_UPDATE;
		msg.data.caret.type = TEXTAREA_CARET_SET_POS;
		msg.data.caret.pos.x = ta->caret_x - ta->scroll_x;
		msg.data.caret.pos.y = ta->caret_y - ta->scroll_y;
		msg.data.caret.pos.height = ta->line_height;
		msg.data.caret.pos.clip = &cr;

		ta->callback(ta->data, &msg);
	}

	return true;
}


/* exported interface, documented in textarea.h */
char *textarea_get_selection(struct textarea *ta)
{
	char *ret;
	size_t b_start, b_end, b_len;

	if (ta->sel_start == -1)
		/* No selection get */
		return NULL;

	b_start = ta->sel_start;
	b_end = ta->sel_end;
	b_len = b_end - b_start;

	if (b_len == 0)
		/* No selection get */
		return NULL;

	ret = malloc(b_len + 1); /* Add space for '\0' */
	if (ret == NULL)
		/* Can't get selection; no memory */
		return NULL;

	memcpy(ret, ta->show->data + b_start, b_len);
	ret[b_len] = '\0';

	return ret;
}


/* exported interface, documented in textarea.h */
void textarea_get_dimensions(struct textarea *ta, int *width, int *height)
{
	if (width != NULL)
		*width = ta->vis_width;
	if (height != NULL)
		*height = ta->vis_height;
}


/* exported interface, documented in textarea.h */
void textarea_set_dimensions(struct textarea *ta, int width, int height)
{
	struct rect r = {0, 0, 0, 0};

	ta->vis_width = width;
	ta->vis_height = height;

	textarea_setup_text_offsets(ta);

	if (ta->flags & TEXTAREA_MULTILINE) {
		 textarea_reflow_multiline(ta, 0, ta->show->len -1, &r);
	} else {
		 textarea_reflow_singleline(ta, 0, &r);
	}
}


/* exported interface, documented in textarea.h */
void textarea_set_layout(struct textarea *ta, int width, int height,
		int top, int right, int bottom, int left)
{
	struct rect r = {0, 0, 0, 0};

	ta->vis_width = width;
	ta->vis_height = height;
	ta->pad_top = top;
	ta->pad_right = right + ((ta->bar_y == NULL) ? 0 : SCROLLBAR_WIDTH);
	ta->pad_bottom = bottom + ((ta->bar_x == NULL) ? 0 : SCROLLBAR_WIDTH);
	ta->pad_left = left;

	textarea_setup_text_offsets(ta);

	if (ta->flags & TEXTAREA_MULTILINE) {
		 textarea_reflow_multiline(ta, 0, ta->show->len -1, &r);
	} else {
		 textarea_reflow_singleline(ta, 0, &r);
	}
}


/* exported interface, documented in textarea.h */
bool textarea_scroll(struct textarea *ta, int scrx, int scry)
{
	bool handled_scroll = false;

	if (ta->flags & TEXTAREA_MULTILINE) {
		/* Multi line textareas have scrollbars to handle this */
		if (ta->bar_x != NULL && scrx != 0 &&
				scrollbar_scroll(ta->bar_x, scrx))
			handled_scroll = true;
		if (ta->bar_y != NULL && scry != 0 &&
				scrollbar_scroll(ta->bar_y, scry))
			handled_scroll = true;

	} else {
		/* Single line.  Can only scroll horizontally. */
		int xs = ta->scroll_x;

		/* Apply offset */
		xs += scrx;

		/* Clamp to limits */
		if (xs < 0)
			xs = 0;
		else if (xs > ta->h_extent - ta->vis_width - ta->border_width)
			xs = ta->h_extent - ta->vis_width - ta->border_width;

		if (xs != ta->scroll_x) {
			ta->scroll_x = xs;
			handled_scroll = true;
		}
	}

	return handled_scroll;
}
