-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathTextArea.pde
More file actions
344 lines (303 loc) · 11.2 KB
/
TextArea.pde
File metadata and controls
344 lines (303 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
private static final class LineRecord implements Comparable<LineRecord> { // we parse and draw text one line after another
public final int number; // line number
public final int offset, end; // positions of the line in the whole text
public final int x, y; // drawing positions
public LineRecord(final int number, final int offset, final int end, final int x, final int y) {
this.number = number;
this.offset = offset;
this.end = end;
this.x = x;
this.y = y;
}
@Override public int compareTo(final LineRecord o) {
return number - o.number;
}
}
private static final class LineOffsetComparator implements Comparator<LineRecord> { // order lines by offset
@Override public int compare(final LineRecord l1, final LineRecord l2) {
return l1.offset - l2.offset;
}
}
private static final class LineYComparator implements Comparator<LineRecord> { // order lines by y
@Override public int compare(final LineRecord l1, final LineRecord l2) {
return l1.y - l2.y;
}
}
static final class TextPosition {
public int offset; // position in the whole text
public int row; // line no
public int toward; // indicates the relation between a point and the offset. 0 means not set, 1 means the point being to the right of the offset, and -1 means left.
public TextPosition(final int offset, final int row) {
this.offset = offset;
this.row = row;
}
public TextPosition(final int offset, final int row, final int toward) {
this.offset = offset;
this.row = row;
this.toward = toward;
}
}
final class TextArea extends Observable {
public final int x, y, width, height; // dimensions of the text area
public String text = "";
public color textColor = 0, backgroundColor = 255;
public int marginTop = 0, marginLeft = 0, marginBottom = 0, marginRight = 0;
public float lineSpacing = 1.0;
public color selectionBackgroudColor = #50A6C2, selectionFrontColor = 255;
private int selectionStart, selectionEnd; // selectionStart is enforced to be less than selectionEnd
private final ArrayList<LineRecord> lines = new ArrayList<LineRecord>(); // lines
private int fontHeight, lineHeight, textWidth, textRight, textBottom; // text relative positions in this the area; there is no textTop as it is merely marginTop; similar for textLeft
public TextArea(final int x, final int y, final int width, final int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
public int getFontHeight() {
return fontHeight;
}
@Override public void setChanged() {
super.setChanged();
}
public void redraw() {
lines.clear();
setSelection(0, 0);
}
public Point getInnerPointByPoint(final int x, final int y) { // map a point in the coordinates of this text area to the point in the coordinates of the window
return new Point(x - this.x, y - this.y);
}
public Point getPointByInnerPoint(final Point p) {
return new Point(p.x + x, p.y + y);
}
public int getNumberOfLines() {
return lines.size();
}
public int getLineOffset(final int row) {
return lines.get(row).offset;
}
public int getLineEnd(final int row) {
return lines.get(row).end;
}
public int getLineMedian(final int row) {
return lines.get(row).y + Math.round(fontHeight / 2.0);
}
public int getLineTop(final int row) {
return lines.get(row).y;
}
public int getLineBottom(final int row) {
return lines.get(row).y + fontHeight;
}
// inner means relative to the text area rather than the window, display or screen
public Point getInnerPointByTextOffset(final int offset) {
LineRecord line;
int row = Collections.binarySearch(lines, new LineRecord(0, offset, 0, 0, 0), new LineOffsetComparator());
// see java doc for the meaning for the returned value by binarySearch
if (row < 0) {
line = lines.get(-row - 2);
} else if (row > 0) {
line = lines.get(row - 1);
if (line.end != offset) {
line = lines.get(row);
}
} else {
line = lines.get(row);
}
return new Point(line.x + Math.round(textWidth(text.substring(line.offset, offset))), line.y + Math.round(fontHeight / 2.0));
}
public Point getInnerPointByTextPosition(final TextPosition tp) {
return getInnerPointByTextPosition(tp.offset, tp.row);
}
public Point getInnerPointByTextPosition(final int offset, final int row) {
final LineRecord line = lines.get(row);
return new Point(line.x + Math.round(textWidth(text.substring(line.offset, offset))), line.y + Math.round(fontHeight / 2.0));
}
public TextPosition getTextPositionByInnerPoint(final Point p) {
clampIntoTextArea(p);
// get the target line first
int row = Collections.binarySearch(lines, new LineRecord(0, 0, 0, 0, p.y), new LineYComparator());
if (row < 0) {
row = -row - 2;
}
LineRecord line = lines.get(row);
if (row != lines.size() - 1) {
final LineRecord nextLine = lines.get(row + 1);
if (p.y >= Math.round((nextLine.y + line.y + fontHeight) / 2.0)) {
line = nextLine;
++row;
}
}
// then calculate the character in the target line
final int lineWidth = Math.round(textWidth(text.substring(line.offset, line.end)));
// start the search not from the begin but a appoximate character
// for example, if the x coordinate is at 1/3 of the line's widht, then start searching roughly from the character at 1/3 column of the text of the line
int offset = line.offset + Math.round((float)(p.x - line.x) / lineWidth * (line.end - line.offset));
offset = clampInt(offset, line.offset, line.end);
int x0 = line.x + Math.round(textWidth(text.substring(line.offset, offset))); // this is where offset is
int x1 = x0;
if (p.x < x0) { // may search backward
if (offset <= line.offset) {
return new TextPosition(offset, row, 1);
}
do {
--offset;
x1 = x0;
x0 = line.x + Math.round(textWidth(text.substring(line.offset, offset)));
}
while (p.x < x0 && offset > line.offset);
if (abs(p.x - x0) < abs(x1 - p.x)) {
return new TextPosition(offset, row, 1);
}
return new TextPosition(offset+1, row, -1);
}
if (offset >= line.end) {
return new TextPosition(offset, row, -1);
}
do { // or forward
++offset;
x1 = x0;
x0 = line.x + Math.round(textWidth(text.substring(line.offset, offset)));
}
while (p.x >= x0 && offset < line.end);
if (abs(x0 - p.x) <= abs(p.x - x1)) {
new TextPosition(offset, row, -1);
}
return new TextPosition(offset-1, row, 1);
}
public int getSelectionStart() {
return selectionStart;
}
public int getSelectionEnd() {
return selectionEnd;
}
public int getRowByTextOffset(final int offset) {
final int row = Collections.binarySearch(lines, new LineRecord(0, offset, 0, 0, 0), new LineOffsetComparator());
return row < 0 ? -row - 2 : row;
}
public boolean hasSelection() {
return selectionStart < selectionEnd;
}
public void setSelection(int start, int end) { // start is enforced to be less than end
final boolean oldHasSelection = hasSelection();
final int oldSelStart = selectionStart;
final int oldSelEnd = selectionEnd;
start = clampSelection(start);
end = clampSelection(end);
if (start == end) {
selectionStart = selectionEnd = 0;
} else if (start > end) {
final int tmp = end;
end = start;
start = tmp;
}
selectionStart = start;
selectionEnd = end;
if (hasSelection() == oldHasSelection
&& (!oldHasSelection || oldSelStart == selectionStart && oldSelEnd == selectionEnd)) {
return;
}
setChanged();
}
public void draw() {
pushStyle();
noStroke();
rectMode(CORNER);
fill(backgroundColor);
rect(0, 0, width, height);
textAlign(LEFT, TOP);
fill(textColor);
prepareText();
// draw lines
// the way of drawing selection is to draw background (rectangles) first then the fonts with the seleciton color
for (final LineRecord line : lines) {
final String lineText = text.substring(line.offset, line.end);
text(lineText, line.x, line.y);
if (selectionStart < selectionEnd && selectionStart < line.end && selectionEnd > line.offset) {
String selectedText = null;
int selectionX;
if (line.offset <= selectionStart) {
selectionX = line.x + Math.round(textWidth(text.substring(line.offset, selectionStart)));
if (line.end > selectionEnd) {
selectedText = text.substring(selectionStart, selectionEnd);
} else {
selectedText = text.substring(selectionStart, line.end);
}
} else {
selectionX = line.x;
if (line.end > selectionEnd) {
selectedText = text.substring(line.offset, selectionEnd);
} else {
selectedText = text.substring(line.offset, line.end);
}
}
if (selectedText != null && !selectedText.isEmpty()) {
fill(selectionBackgroudColor);
rect(selectionX, line.y, textWidth(selectedText), fontHeight);
fill(selectionFrontColor);
text(selectedText, selectionX, line.y);
fill(textColor);
}
}
}
// draw the bottom margin
fill(backgroundColor);
rect(0, textBottom, width, height - textBottom);
popStyle();
}
// parse the text into lines
private void prepareText() {
if (lines.size() != 0 || text.isEmpty()) {
return;
}
fontHeight = Math.round(textAscent() + textDescent());
lineHeight = Math.round(fontHeight * lineSpacing);
textRight = width - marginRight;
textWidth = textRight - marginLeft;
textBottom = height - marginBottom;
final int textLength = text.length();
int posY = marginTop;
int lineStart = 0;
float lineWidth = 0.0;
for (int i = 0; i < textLength; ++i) {
final char c = text.charAt(i);
final float cWidth = textWidth(c);
if (lineStart < i && lineWidth + cWidth >= textWidth) { // change line when the line is full
lines.add(new LineRecord(lines.size(), lineStart, i, marginLeft, posY));
lineStart = i;
lineWidth = cWidth;
posY += lineHeight;
if (posY >= textBottom) {
break;
}
} else {
lineWidth += cWidth;
if (c == '\n') { // change line at line feed
final int lineEnd = i + 1;
lines.add(new LineRecord(lines.size(), lineStart, lineEnd, marginLeft, posY));
lineStart = lineEnd;
lineWidth = 0;
posY += lineHeight;
if (posY >= textBottom) {
break;
}
}
}
}
if (posY < textBottom) {
int lastLineEnd = 0;
if (!lines.isEmpty()) {
lastLineEnd = lines.get(lines.size() - 1).end;
}
lines.add(new LineRecord(lines.size(), lastLineEnd, textLength, marginLeft, posY));
}
}
// do not let the point go outside the textarea
private void clampIntoTextArea(final Point p) {
p.setLocation(clampInt(p.x, marginLeft, textRight), clampInt(p.y, marginTop, textBottom));
}
private int clampSelection(final int pos) {
if (pos < 0) {
return 0;
}
return pos > text.length() ? text.length() : pos;
}
}