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