1// This file is part of SmallBASIC
2//
3// Copyright(C) 2001-2019 Chris Warren-Smith.
4//
5// This program is distributed under the terms of the GPL v2.0 or later
6// Download the GNU Public License (GPL) from www.gnu.org
7//
8
9#include <config.h>
10#include <FL/Fl_Rect.H>
11#include "platform/fltk/TtyWidget.h"
12
13static void scrollbar_callback(Fl_Widget *scrollBar, void *widget) {
14 ((Fl_Group *)widget)->redraw();
15}
16
17//
18// TtyWidget constructor
19//
20TtyWidget::TtyWidget(int x, int y, int w, int h, int numRows) :
21 Fl_Group(x, y, w, h, 0) {
22
23 // initialize the buffer
24 buffer = new TtyRow[numRows];
25 rows = numRows;
26 cols = width = 0;
27 head = tail = 0;
28 markX = markY = pointX = pointY = 0;
29
30 setfont(FL_COURIER, DEF_FONT_SIZE);
31 scrollLock = false;
32
33 begin();
34 // vertical scrollbar scrolls in row units
35 vscrollbar = new Fl_Scrollbar(w - SCROLL_W, y, x + SCROLL_W, h);
36 vscrollbar->type(FL_VERTICAL);
37 vscrollbar->user_data(this);
38 vscrollbar->callback(scrollbar_callback);
39
40 // horizontal scrollbar scrolls in pixel units
41 hscrollbar = new Fl_Scrollbar(w - HSCROLL_W - SCROLL_W, 1, HSCROLL_W, SCROLL_H);
42 hscrollbar->type(FL_HORIZONTAL);
43 hscrollbar->user_data(this);
44 hscrollbar->callback(scrollbar_callback);
45
46 end();
47 resize(x, y, w, h);
48}
49
50TtyWidget::~TtyWidget() {
51 delete [] buffer;
52}
53
54//
55// draw the text
56//
57void TtyWidget::draw() {
58 // get the text drawing rectangle
59 Fl_Rect rc = Fl_Rect(x(), y(), w(), h());
60 if (vscrollbar->visible()) {
61 rc.w(rc.w() - vscrollbar->w());
62 }
63 // prepare escape state variables
64 bool bold = false;
65 bool italic = false;
66 bool underline = false;
67 bool invert = false;
68
69 // calculate rows to display
70 int pageRows = getPageRows();
71 int textRows = getTextRows();
72 int vscroll = vscrollbar->value();
73 int hscroll = hscrollbar->value();
74 int numRows = textRows < pageRows ? textRows : pageRows;
75 int firstRow = tail + vscroll; // from start plus scroll offset
76
77 // setup the background colour
78 fl_color(color());
79 fl_rectf(rc.x(), rc.y(), rc.w(), rc.h());
80 fl_push_clip(rc.x(), rc.y(), rc.w(), rc.h());
81 fl_color(fl_color_average(labelcolor(), color(), .33f));
82 fl_rectf(rc.x(), rc.y(), rc.w(), 1);
83 fl_color(labelcolor());
84 fl_font(labelfont(), labelsize());
85
86 int pageWidth = 0;
87 for (int row = firstRow, rows = 0, y = rc.y() + lineHeight; rows < numRows; row++, rows++, y += lineHeight) {
88 TtyRow *line = getLine(row); // next logical row
89 TtyTextSeg *seg = line->head;
90 int x = rc.x() + 2 - hscroll;
91 while (seg != NULL) {
92 if (seg->escape(&bold, &italic, &underline, &invert)) {
93 setfont(bold, italic);
94 }
95 drawSelection(seg, NULL, row, x, y);
96 int width = seg->width();
97 if (seg->str) {
98 if (invert) {
99 fl_color(labelcolor());
100 fl_rectf(x, (y - lineHeight) + fl_descent(), width, lineHeight);
101 fl_color(color());
102 fl_draw(seg->str, x, y);
103 fl_color(labelcolor());
104 } else {
105 fl_draw(seg->str, x, y);
106 }
107 }
108 if (underline) {
109 fl_line(x, y + 1, x + width, y + 1);
110 }
111 x += width;
112 seg = seg->next;
113 }
114 int rowWidth = line->width();
115 if (rowWidth > pageWidth) {
116 pageWidth = rowWidth;
117 }
118 }
119
120 // draw scrollbar controls
121 if (pageWidth > w()) {
122 draw_child(*hscrollbar);
123 }
124 fl_pop_clip();
125 draw_child(*vscrollbar);
126}
127
128//
129// draw the background for selected text
130//
131void TtyWidget::drawSelection(TtyTextSeg *seg, strlib::String *s, int row, int x, int y) {
132 if (markX != pointX || markY != pointY) {
133 Fl_Rect rc(0, y - fl_height(), 0, lineHeight);
134 int r1 = markY;
135 int r2 = pointY;
136 int x1 = markX;
137 int x2 = pointX;
138
139 if (r1 > r2) {
140 r1 = pointY;
141 r2 = markY;
142 x1 = pointX;
143 x2 = markX;
144 }
145 if (r1 == r2 && x1 > x2) {
146 x1 = pointX;
147 x2 = markX;
148 }
149 if (row > r1 && row < r2) {
150 // entire row
151 rc.x(x);
152 rc.w(seg->width());
153 if (s) {
154 s->append(seg->str);
155 }
156 } else if (row == r1 && (r2 > r1 || x < x2)) {
157 // top selection row
158 int i = 0;
159 int len = seg->numChars();
160
161 // find start of selection
162 while (x < x1 && i < len) {
163 x += fl_width(seg->str + (i++), 1);
164 }
165 rc.x(x);
166
167 // select rest of line when r2>r1
168 while ((r2 > r1 || x < x2) && i < len) {
169 if (s) {
170 s->append(seg->str[i]);
171 }
172 x += fl_width(seg->str + (i++), 1);
173 }
174 rc.w(x - rc.x());
175 } else if (row == r2) {
176 // bottom selection row
177 rc.x(x);
178
179 // select rest of line when r2>r1
180 int i = 0;
181 int len = seg->numChars();
182 while (x < x2 && i < len) {
183 if (s) {
184 s->append(seg->str[i]);
185 }
186 x += fl_width(seg->str + (i++), 1);
187 }
188 rc.w(x - rc.x());
189 }
190
191 if (!s && (rc.w() || rc.h())) {
192 fl_color(selection_color());
193 fl_rectf(rc.x(), rc.y(), rc.w(), rc.h());
194 fl_color(labelcolor());
195 }
196 }
197}
198
199//
200// process mouse messages
201//
202int TtyWidget::handle(int e) {
203 static bool leftButtonDown = false;
204 switch (e) {
205 case FL_PUSH:
206 if ((!vscrollbar->visible() || !Fl::event_inside(vscrollbar)) &&
207 (!hscrollbar->visible() || !Fl::event_inside(hscrollbar))) {
208 bool selected = (markX != pointX || markY != pointY);
209 if (selected && Fl::event_button() == FL_RIGHT_MOUSE) {
210 // right click to copy selection
211 copySelection();
212 }
213 markX = pointX = Fl::event_x();
214 markY = pointY = rowEvent();
215 if (selected) {
216 // draw end selection
217 damage(DAMAGE_HIGHLIGHT);
218 }
219 leftButtonDown = true;
220 return 1; // become belowmouse to receive RELEASE event
221 }
222 break;
223
224 case FL_DRAG:
225 case FL_MOVE:
226 if (leftButtonDown) {
227 pointX = Fl::event_x();
228 pointY = rowEvent();
229 damage(DAMAGE_HIGHLIGHT);
230 if (vscrollbar->visible()) {
231 // drag to scroll up or down
232 int value = vscrollbar->value();
233 if (Fl::event_y() < 0 && value > 0) {
234 vscrollbar->value(value - 1);
235 } else if ((Fl::event_y() > h()) && (value + getPageRows() < getTextRows())) {
236 vscrollbar->value(value + 1);
237 }
238 }
239 }
240 return 1;
241
242 case FL_RELEASE:
243 leftButtonDown = false;
244 return 1;
245
246 case FL_MOUSEWHEEL:
247 if (vscrollbar->visible()) {
248 return vscrollbar->handle(e);
249 }
250 break;
251 }
252
253 return Fl_Group::handle(e);
254}
255
256//
257// update scrollbar positions
258//
259void TtyWidget::resize(int x, int y, int w, int h) {
260 Fl_Group::resize(x, y, w, h);
261
262 int pageRows = getPageRows();
263 int textRows = getTextRows();
264 int hscrollX = w - HSCROLL_W;
265 int hscrollW = w - 4;
266
267 if (textRows > pageRows && h > SCROLL_W) {
268 vscrollbar->set_visible();
269 int value = vscrollbar->value();
270 if (value + pageRows > textRows) {
271 // prevent value from extending beyond the buffer range
272 value = textRows - pageRows;
273 }
274 vscrollbar->resize(x + w - SCROLL_W, y, SCROLL_W, h);
275 vscrollbar->value(value, pageRows, 0, textRows);
276 hscrollX -= SCROLL_W;
277 hscrollW -= SCROLL_W;
278 } else {
279 vscrollbar->clear_visible();
280 vscrollbar->value(0);
281 }
282 if (width > hscrollW) {
283 hscrollbar->set_visible();
284 hscrollbar->resize(hscrollX, y, HSCROLL_W, SCROLL_H);
285 hscrollbar->value(hscrollbar->value(), hscrollW, 0, width);
286 } else {
287 hscrollbar->clear_visible();
288 hscrollbar->value(0);
289 }
290}
291
292//
293// copy selected text to the clipboard
294//
295bool TtyWidget::copySelection() {
296 int hscroll = hscrollbar->value();
297 bool bold = false;
298 bool italic = false;
299 bool underline = false;
300 bool invert = false;
301 int r1 = markY;
302 int r2 = pointY;
303
304 if (r1 > r2) {
305 r1 = pointY;
306 r2 = markY;
307 }
308
309 strlib::String selection;
310
311 for (int row = r1; row <= r2; row++) {
312 TtyRow *line = getLine(row); // next logical row
313 TtyTextSeg *seg = line->head;
314 int x = 2 - hscroll;
315 strlib::String rowText;
316 while (seg != NULL) {
317 if (seg->escape(&bold, &italic, &underline, &invert)) {
318 setfont(bold, italic);
319 }
320 drawSelection(seg, &rowText, row, x, 0);
321 x += seg->width();
322 seg = seg->next;
323 }
324 if (rowText.length()) {
325 selection.append(rowText);
326 selection.append("\n");
327 }
328 }
329
330 bool result = selection.length() > 0;
331 if (result) {
332 const char *copy = selection.c_str();
333 Fl::copy(copy, strlen(copy), true);
334 }
335 return result;
336}
337
338//
339// clear the screen
340//
341void TtyWidget::clearScreen() {
342 head = tail = 0;
343 cols = width = 0;
344 markX = markY = pointX = pointY = 0;
345 getLine(0)->clear();
346 vscrollbar->value(0);
347 vscrollbar->hide();
348 hscrollbar->value(0);
349 hscrollbar->hide();
350 redraw();
351}
352
353//
354// process incoming text
355//
356void TtyWidget::print(const char *str) {
357 int strLength = strlen(str);
358 TtyRow *line = getLine(head); // pointer to current line
359
360 // need the current font set to calculate text widths
361 fl_font(labelfont(), labelsize());
362
363 // scan the text, handle any special characters, and display the rest.
364 for (int i = 0; i < strLength; i++) {
365 // check for telnet IAC codes
366 switch (str[i]) {
367 case '\r': // return
368 // move to the start of the line
369 break;
370
371 case '\a':
372 // beep!
373 break;
374
375 case '\n': // new line
376 // scroll by moving logical last line
377 if (getTextRows() == rows) {
378 tail = (tail + 1 >= rows) ? 0 : tail + 1;
379 }
380 head = (head + 1 >= rows) ? 0 : head + 1;
381
382 // clear the new line
383 line = getLine(head);
384 line->clear();
385 break;
386
387 case '\b': // backspace
388 break;
389
390 case '\t':
391 line->tab();
392 break;
393
394 case '\xC':
395 clearScreen();
396 break;
397
398 default:
399 i += processLine(line, &str[i]);
400 } // end case
401 }
402
403 if (!scrollLock) {
404 vscrollbar->value(getTextRows() - getPageRows());
405 }
406
407 // schedule a layout and redraw
408 resize(x(), y(), w(), h());
409 redraw();
410}
411
412//
413// return a pointer to the specified line of the display.
414//
415TtyRow *TtyWidget::getLine(int pos) {
416 if (pos < 0) {
417 pos += rows;
418 }
419 if (pos > rows - 1) {
420 pos -= rows;
421 }
422
423 return &buffer[pos];
424}
425
426//
427// interpret ANSI escape codes in linePtr and return number of chars consumed
428//
429int TtyWidget::processLine(TtyRow *line, const char *linePtr) {
430 TtyTextSeg *segment = new TtyTextSeg();
431 line->append(segment);
432
433 const char *linePtrStart = linePtr;
434
435 // Determine if we are at an end-of-line or an escape
436 if (*linePtr == '\033') {
437 linePtr++;
438
439 bool escaped = false;
440 if (*linePtr == '[') {
441 escaped = true;
442 linePtr++;
443 }
444
445 if (escaped) {
446 int param = 0;
447 while (*linePtr != '\0' && escaped) {
448 // Walk the escape sequence
449 switch (*linePtr) {
450 case 'm':
451 escaped = false; // fall through
452
453 case ';': // Parameter seperator
454 setGraphicsRendition(segment, param);
455 param = 0;
456 break;
457
458 case '0':
459 case '1':
460 case '2':
461 case '3':
462 case '4':
463 case '5':
464 case '6':
465 case '7':
466 case '8':
467 case '9':
468 // Numeric OK; continue till delimeter or illegal char
469 param = (param * 10) + (*linePtr - '0');
470 break;
471
472 default: // Illegal character - reset
473 segment->flags = 0;
474 escaped = false;
475 break;
476 }
477 linePtr += 1;
478 } // while escaped and not null
479 }
480 }
481
482 const char *linePtrNext = linePtr;
483
484 // Walk the line of text until an escape char or end-of-line
485 // is encountered.
486 while (*linePtr > 31) {
487 linePtr++;
488 }
489
490 // Print the next (possible) line of text
491 if (*linePtrNext != '\0' && linePtrNext != linePtr) {
492 segment->setText(linePtrNext, (linePtr - linePtrNext));
493
494 // save max rows encountered
495 int lineWidth = line->width();
496 if (lineWidth > width) {
497 width = lineWidth;
498 }
499
500 int lineChars = line->numChars();
501 if (lineChars > cols) {
502 cols = lineChars;
503 }
504 }
505 // return the number of eaten chars (less 1)
506 return linePtr == linePtrStart ? 0 : (linePtr - linePtrStart) - 1;
507}
508
509//
510// performs the ANSI text SGI function.
511//
512void TtyWidget::setGraphicsRendition(TtyTextSeg *segment, int c) {
513 switch (c) {
514 case 0:
515 segment->reset();
516 break;
517
518 case 1: // Bold on
519 segment->set(TtyTextSeg::BOLD, true);
520 break;
521
522 case 2: // Faint on
523 segment->set(TtyTextSeg::BOLD, false);
524 break;
525
526 case 3: // Italic on
527 segment->set(TtyTextSeg::ITALIC, true);
528 break;
529
530 case 4: // Underscrore
531 segment->set(TtyTextSeg::UNDERLINE, true);
532 break;
533
534 case 7: // reverse video on
535 segment->set(TtyTextSeg::INVERT, true);
536 break;
537
538 case 21: // set bold off
539 segment->set(TtyTextSeg::BOLD, false);
540 break;
541
542 case 23:
543 segment->set(TtyTextSeg::ITALIC, false);
544 break;
545
546 case 24: // set underline off
547 segment->set(TtyTextSeg::UNDERLINE, false);
548 break;
549
550 case 27: // reverse video off
551 segment->set(TtyTextSeg::INVERT, false);
552 break;
553
554 case 30: // Black
555 segment->color = FL_BLACK;
556 break;
557
558 case 31: // Red
559 segment->color = FL_RED;
560 break;
561
562 case 32: // Green
563 segment->color = FL_GREEN;
564 break;
565
566 case 33: // Yellow
567 segment->color = FL_YELLOW;
568 break;
569
570 case 34: // Blue
571 segment->color = FL_BLUE;
572 break;
573
574 case 35: // Magenta
575 segment->color = FL_MAGENTA;
576 break;
577
578 case 36: // Cyan
579 segment->color = FL_CYAN;
580 break;
581
582 case 37: // White
583 segment->color = FL_WHITE;
584 break;
585 }
586}
587
588//
589// update the current drawing font
590//
591void TtyWidget::setfont(bool bold, bool italic) {
592 Fl_Font font = labelfont();
593 if (bold) {
594 font += FL_BOLD;
595 }
596 if (italic) {
597 font += FL_ITALIC;
598 }
599 fl_font(font, labelsize());
600}
601
602//
603// update the current drawing font and remember the face/size
604//
605void TtyWidget::setfont(Fl_Font font, int size) {
606 if (font) {
607 labelfont(font);
608 }
609 if (size) {
610 labelsize(size);
611 }
612 fl_font(labelfont(), labelsize());
613 lineHeight = fl_height() + fl_descent();
614}
615