mirror of
https://github.com/netsurf-browser/netsurf
synced 2024-12-15 08:42:39 +03:00
3214 lines
81 KiB
C
3214 lines
81 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/utils.h"
|
|
|
|
#include "desktop/mouse.h"
|
|
#include "desktop/textarea.h"
|
|
#include "desktop/textinput.h"
|
|
#include "desktop/plotters.h"
|
|
#include "desktop/scrollbar.h"
|
|
#include "desktop/gui_factory.h"
|
|
#include "render/font.h"
|
|
#include "utils/log.h"
|
|
#include "utils/utf8.h"
|
|
#include "utils/utils.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);
|
|
}
|
|
|
|
|
|
/**
|
|
* 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;
|
|
|
|
nsfont.font_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
|
|
* \parm 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 */
|
|
nsfont.font_width(&ta->fstyle, ta->show->data,
|
|
ta->show->len - 1, &x);
|
|
|
|
/* Get width of retained text */
|
|
if (b_off != ta->lines[0].b_length) {
|
|
nsfont.font_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
|
|
* \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;
|
|
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 =
|
|
malloc(LINE_CHUNK_SIZE * sizeof(struct line_info));
|
|
if (ta->lines == NULL) {
|
|
LOG(("malloc failed"));
|
|
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;
|
|
|
|
/* Have to start on line before where the changes are in case an
|
|
* added space makes the text before the space on a soft-wrapped line
|
|
* fit on the line above */
|
|
if (start != 0)
|
|
start--;
|
|
|
|
do {
|
|
/* Set line count to start point */
|
|
if (restart)
|
|
start = 0;
|
|
|
|
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 initial length and text offset */
|
|
if (line == 0) {
|
|
len = ta->text.len - 1;
|
|
text = ta->text.data;
|
|
} else {
|
|
unsigned int i;
|
|
len = ta->text.len - 1 - ta->lines[line].b_start;
|
|
text = ta->text.data + 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 */
|
|
nsfont.font_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)))
|
|
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 wraped, 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)))
|
|
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;
|
|
|
|
/* Don't need to redraw above changes, so update redraw request rect*/
|
|
if (ta->lines[start].b_start + ta->lines[start].b_length < 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;
|
|
|
|
nsfont.font_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 */
|
|
nsfont.font_position_in_string(&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 += (vis_height * 3 + 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(FDIV((FMUL(FLTTOFIX(1.3),
|
|
FMUL(nscss_screen_dpi, INTTOFIX((setup->text.size))))),
|
|
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) {
|
|
nsfont.font_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;
|
|
|
|
caret = ta->sel_end;
|
|
textarea_clear_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 KEY_SELECT_ALL:
|
|
textarea_select(ta, 0, ta->show->len - 1, true);
|
|
return true;
|
|
case 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 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;
|
|
|
|
caret = ta->sel_end;
|
|
textarea_clear_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 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;
|
|
|
|
caret = ta->sel_end;
|
|
textarea_clear_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 KEY_CR:
|
|
case 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;
|
|
|
|
caret = ta->sel_end;
|
|
textarea_clear_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 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;
|
|
|
|
caret = ta->sel_end;
|
|
textarea_clear_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 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;
|
|
|
|
caret = ta->sel_end;
|
|
caret += byte_delta;
|
|
textarea_clear_selection(ta);
|
|
}
|
|
break;
|
|
case KEY_ESCAPE:
|
|
/* Fall through to KEY_CLEAR_SELECTION */
|
|
case KEY_CLEAR_SELECTION:
|
|
return textarea_clear_selection(ta);
|
|
case 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 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 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 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 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 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 KEY_LINE_START:
|
|
if (readonly)
|
|
break;
|
|
caret -= ta->caret_pos.byte_off;
|
|
if (ta->sel_start != -1) {
|
|
textarea_clear_selection(ta);
|
|
}
|
|
break;
|
|
case 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 KEY_TEXT_START:
|
|
if (readonly)
|
|
break;
|
|
caret = 0;
|
|
if (ta->sel_start != -1) {
|
|
textarea_clear_selection(ta);
|
|
}
|
|
break;
|
|
case KEY_TEXT_END:
|
|
if (readonly)
|
|
break;
|
|
caret = ta->show->len - 1;
|
|
if (ta->sel_start != -1) {
|
|
textarea_clear_selection(ta);
|
|
}
|
|
break;
|
|
case 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 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 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;
|
|
caret = ta->sel_end;
|
|
textarea_clear_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 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;
|
|
caret = ta->sel_end;
|
|
textarea_clear_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 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;
|
|
caret = ta->sel_end;
|
|
textarea_clear_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 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 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 */
|
|
ta->sel_start = ta->sel_end = -1;
|
|
|
|
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;
|
|
}
|