1// This file is part of SmallBASIC
2//
3// Copyright(C) 2001-2014 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 <string.h>
10#include <ctype.h>
11#include <stdarg.h>
12#include <stdio.h>
13#include <stdlib.h>
14#include <wchar.h>
15#include <math.h>
16
17#include "ui/ansiwidget.h"
18#include "ui/inputs.h"
19#include "ui/utils.h"
20
21/* class AnsiWidget
22
23 Displays ANSI escape codes.
24
25 Escape sequences start with the characters ESC (ASCII 27d / 1Bh / 033o )
26 and [ (left bracket). This sequence is called CSI for
27 "Control Sequence Introducer".
28
29 For more information about ANSI code see:
30 http://en.wikipedia.org/wiki/ANSI_escape_code
31 http://www.uv.tietgen.dk/staff/mlha/PC/Soft/Prog/BAS/VB/Function.html
32 http://bjh21.me.uk/all-escapes/all-escapes.txt
33
34 Supported control codes:
35 \t tab (20 px)
36 \a beep
37 \r return
38 \n next line
39 \xC clear screen (new page)
40 \e[K clear to end of line
41 \e[0m reset all attributes to their defaults
42 \e[1m set bold on
43 \e[4m set underline on
44 \e[7m reverse video
45 \e[21m set bold off
46 \e[24m set underline off
47 \e[27m set reverse off
48*/
49
50#define BUTTON_PADDING 10
51#define OVER_SCROLL 100
52#define H_SCROLL_SIZE 10
53#define SWIPE_MAX_TIMER 3000
54#define SWIPE_DELAY_STEP 200
55#define SWIPE_MAX_DURATION 300
56#define SWIPE_MIN_DISTANCE 60
57#define FONT_FACTOR 30
58
59extern "C" void dev_beep(void);
60
61AnsiWidget::AnsiWidget(int width, int height) :
62 _back(nullptr),
63 _front(nullptr),
64 _focus(nullptr),
65 _activeButton(nullptr),
66 _hoverInput(nullptr),
67 _width(width),
68 _height(height),
69 _xTouch(-1),
70 _yTouch(-1),
71 _xMove(-1),
72 _yMove(-1),
73 _touchTime(0),
74 _swipeExit(false),
75 _autoflush(true) {
76 for (int i = 0; i < MAX_SCREENS; i++) {
77 _screens[i] = nullptr;
78 }
79 _fontSize = MIN(width, height) / FONT_FACTOR;
80 trace("width: %d height: %d fontSize:%d", _width, height, _fontSize);
81}
82
83void AnsiWidget::clearScreen() {
84 _hoverInput = nullptr;
85 _activeButton = nullptr;
86 _back->clear();
87}
88
89bool AnsiWidget::construct() {
90 bool result = false;
91 _back = new GraphicScreen(_width, _height, _fontSize);
92 if (_back && _back->construct()) {
93 _screens[0] = _front = _back;
94 clearScreen();
95 result = true;
96 }
97 return result;
98}
99
100// widget clean up
101AnsiWidget::~AnsiWidget() {
102 logEntered();
103 for (int i = 0; i < MAX_SCREENS; i++) {
104 delete _screens[i];
105 }
106}
107
108Screen *AnsiWidget::createScreen(int screenId) {
109 Screen *result = _screens[screenId];
110 if (result == nullptr) {
111 if (screenId == TEXT_SCREEN || screenId == MENU_SCREEN) {
112 result = new TextScreen(_width, _height, _fontSize);
113 } else {
114 result = new GraphicScreen(_width, _height, _fontSize);
115 }
116 if (result && result->construct()) {
117 _screens[screenId] = result;
118 result->drawInto();
119 result->clear();
120 } else {
121 trace("Failed to create screen %d", screenId);
122 }
123 }
124 return result;
125}
126
127void AnsiWidget::addImage(ImageDisplay &image) {
128 _back->addImage(image);
129 flush(false, false, MAX_PENDING_GRAPHICS);
130}
131
132void AnsiWidget::drawArc(int xc, int yc, double r, double start, double end, double aspect) {
133 _back->drawArc(xc, yc, r, start, end, aspect);
134 flush(false, false, MAX_PENDING_GRAPHICS);
135}
136
137void AnsiWidget::drawEllipse(int xc, int yc, int rx, int ry, int fill) {
138 _back->drawEllipse(xc, yc, rx, ry, fill);
139 flush(false, false, MAX_PENDING_GRAPHICS);
140}
141
142void AnsiWidget::drawImage(ImageDisplay &image) {
143 _back->drawImage(image);
144 flush(false, false, MAX_PENDING_GRAPHICS);
145}
146
147// draw a line onto the offscreen buffer
148void AnsiWidget::drawLine(int x1, int y1, int x2, int y2) {
149 _back->drawLine(x1, y1, x2, y2);
150 flush(false, false, MAX_PENDING_GRAPHICS);
151}
152
153// draw a rectangle onto the offscreen buffer
154void AnsiWidget::drawRect(int x1, int y1, int x2, int y2) {
155 _back->drawRect(x1, y1, x2, y2);
156 flush(false, false, MAX_PENDING_GRAPHICS);
157}
158
159// draw a filled rectangle onto the offscreen buffer
160void AnsiWidget::drawRectFilled(int x1, int y1, int x2, int y2) {
161 _back->drawRectFilled(x1, y1, x2, y2);
162 flush(false, false, MAX_PENDING_GRAPHICS);
163}
164
165// display any pending images changed
166void AnsiWidget::flush(bool force, bool vscroll, int maxPending) {
167 if (_front != nullptr && _autoflush) {
168 bool update = false;
169 if (force) {
170 update = _front->_dirty;
171 } else if (_front->_dirty) {
172 update = (maGetMilliSecondCount() - _front->_dirty >= maxPending);
173 }
174 if (update) {
175 _front->drawBase(vscroll);
176 }
177 }
178}
179
180int AnsiWidget::getScreenId(bool back) {
181 int result = 0;
182 for (int i = 0; i < MAX_SCREENS; i++) {
183 if (_screens[i] == (back ? _back : _front)) {
184 result = i;
185 break;
186 }
187 }
188 return result;
189}
190
191// prints the contents of the given string onto the backbuffer
192void AnsiWidget::print(const char *str) {
193 int len = (str == nullptr ? 0 : strlen(str));
194 if (len) {
195 _back->drawInto();
196
197 int lineHeight = textHeight();
198 const char *p = (char *)str;
199
200 while (*p) {
201 switch (*p) {
202 case '\a': // beep
203 dev_beep();
204 break;
205 case '\t':
206 _back->calcTab();
207 break;
208 case '\003': // end of text
209 flush(true);
210 break;
211 case '\xC': // form feed
212 clearScreen();
213 break;
214 case '\033': // ESC ctrl chars
215 handleEscape(p, lineHeight);
216 break;
217 case '\034':
218 // file separator
219 break;
220 case '\n': // new line
221 _back->newLine(lineHeight);
222 break;
223 case '\r': // return
224 _back->_curX = INITXY; // erasing the line will clear any previous text
225 break;
226 default:
227 p += _back->print(p, lineHeight) - 1; // allow for p++
228 break;
229 };
230
231 if (*p == '\0') {
232 break;
233 }
234 p++;
235 }
236
237 // cleanup
238 flush(false);
239 }
240}
241
242// redraws and flushes the front screen
243void AnsiWidget::redraw() {
244 _front->drawInto();
245 flushNow();
246}
247
248// reinit for new program run
249void AnsiWidget::reset() {
250 _back = _front = _screens[USER_SCREEN1];
251 _back->reset(_fontSize);
252 _back->clear();
253
254 // reset user screens
255 delete _screens[USER_SCREEN2];
256 delete _screens[TEXT_SCREEN];
257 _screens[USER_SCREEN2] = nullptr;
258 _screens[TEXT_SCREEN] = nullptr;
259
260 maFontSetCurrent(_back->_font);
261 redraw();
262}
263
264// update the widget to new dimensions
265void AnsiWidget::resize(int newWidth, int newHeight) {
266 int lineHeight = textHeight();
267 for (int i = 0; i < MAX_SCREENS; i++) {
268 Screen *screen = _screens[i];
269 if (screen) {
270 screen->resize(newWidth, newHeight, _width, _height, lineHeight);
271 if (screen == _front) {
272 screen->drawBase(false);
273 }
274 }
275 }
276 _width = newWidth;
277 _height = newHeight;
278}
279
280void AnsiWidget::removeHover() {
281 if (_hoverInput) {
282 int dx = _front->_x;
283 int dy = _front->_y - _front->_scrollY;
284 _hoverInput->drawHover(dx, dy, false);
285 _hoverInput = nullptr;
286 }
287}
288
289void AnsiWidget::removeInputs() {
290 List_each(FormInput *, it, _back->_inputs) {
291 FormInput *widget = *it;
292 if (widget == _activeButton) {
293 _activeButton = nullptr;
294 } else if (widget == _hoverInput) {
295 _hoverInput = nullptr;
296 }
297 }
298 _back->removeInputs();
299}
300
301bool AnsiWidget::scroll(bool up, bool page) {
302 int h = page ? _front->_height - _front->_charHeight : _front->_charHeight;
303 int vscroll = _front->_scrollY + (up ? - h : h);
304 int maxVScroll = (_front->_curY - _front->_height) + (2 * _fontSize);
305 bool result;
306
307 if (page) {
308 if (vscroll < 0 && _front->_scrollY > 0) {
309 vscroll = 0;
310 } else if (vscroll >= maxVScroll) {
311 vscroll = maxVScroll - _front->_charHeight;
312 }
313 }
314
315 if (vscroll >= 0 && vscroll < maxVScroll) {
316 _front->drawInto();
317 _front->_scrollY = vscroll;
318 flush(true, true);
319 result = true;
320 } else {
321 result = false;
322 }
323 return result;
324}
325
326// sets the current drawing color
327void AnsiWidget::setColor(long fg) {
328 _back->setColor(fg);
329}
330
331// sets the font
332void AnsiWidget::setFont(int size, bool bold, bool italic) {
333 _back->setGraphicsRendition('m', bold ? 1 : 21, 0);
334 _back->setGraphicsRendition('m', italic ? 3 : 23, 0);
335 _back->updateFont(size);
336}
337
338// sets the text font size
339void AnsiWidget::setFontSize(int fontSize) {
340 this->_fontSize = fontSize;
341 for (int i = 0; i < MAX_SCREENS; i++) {
342 if (_screens[i] != nullptr) {
343 _screens[i]->reset(fontSize);
344 }
345 }
346 redraw();
347}
348
349// sets the pixel to the given color at the given xy location
350void AnsiWidget::setPixel(int x, int y, int c) {
351 _back->setPixel(x, y, c);
352 flush(false, false, MAX_PENDING_GRAPHICS);
353}
354
355void AnsiWidget::setStatus(const char *label) {
356 _back->_label = label;
357 _back->setDirty();
358}
359
360// sets the current text drawing color
361void AnsiWidget::setTextColor(long fg, long bg) {
362 _back->setTextColor(fg, bg);
363}
364
365void AnsiWidget::setXY(int x, int y) {
366 if (x != _back->_curX || y != _back->_curY) {
367 int lineHeight = textHeight();
368 int newLines = (y - _back->_curY) / lineHeight;
369 while (newLines-- > 0) {
370 _back->newLine(lineHeight);
371 }
372 _back->_curX = (x == 0) ? INITXY : x;
373 _back->_curY = y;
374 flush(false, false, MAX_PENDING_GRAPHICS);
375 }
376}
377
378void AnsiWidget::handleMenu(bool up) {
379 _activeButton = _front->getNextMenu(_activeButton, up);
380}
381
382void AnsiWidget::insetMenuScreen(int x, int y, int w, int h) {
383 if (_back == _screens[MENU_SCREEN]) {
384 _back = _screens[USER_SCREEN1];
385 }
386 TextScreen *menuScreen = (TextScreen *)createScreen(MENU_SCREEN);
387 menuScreen->_x = x;
388 menuScreen->_y = y;
389 menuScreen->_width = w;
390 menuScreen->_height = h;
391 menuScreen->setOver(_front);
392 _front = _back = menuScreen;
393 _front->_dirty = true;
394}
395
396void AnsiWidget::insetTextScreen(int x, int y, int w, int h) {
397 if (_back == _screens[TEXT_SCREEN]) {
398 _back = _screens[USER_SCREEN1];
399 }
400 TextScreen *textScreen = (TextScreen *)createScreen(TEXT_SCREEN);
401 textScreen->inset(x, y, w, h, _front);
402 _front = _back = textScreen;
403 _front->_dirty = true;
404 flush(true);
405}
406
407// handler for pointer touch events
408bool AnsiWidget::pointerTouchEvent(MAEvent &event) {
409 bool result = false;
410 // hit test buttons on the front screen
411 if (setActiveButton(event, _front)) {
412 _focus = _front;
413 } else {
414 // hit test buttons on remaining screens
415 for (int i = 0; i < MAX_SCREENS; i++) {
416 if (_screens[i] != nullptr && _screens[i] != _front) {
417 if (setActiveButton(event, _screens[i])) {
418 _focus = _screens[i];
419 break;
420 }
421 }
422 }
423 }
424 // paint the pressed button
425 if (_activeButton != nullptr) {
426 _activeButton->clicked(event.point.x, event.point.y, true);
427 drawActiveButton();
428 }
429 // setup vars for page scrolling
430 if (_front->overlaps(event.point.x, event.point.y)) {
431 _touchTime = maGetMilliSecondCount();
432 _xTouch = _xMove = event.point.x;
433 _yTouch = _yMove = event.point.y;
434 result = true;
435 }
436 return result;
437}
438
439// handler for pointer move events
440bool AnsiWidget::pointerMoveEvent(MAEvent &event) {
441 bool result = false;
442 if (_front == _screens[MENU_SCREEN]) {
443 _activeButton = _front->getMenu(_activeButton, event.point.x, event.point.y);
444 } else if (_activeButton != nullptr) {
445 bool redraw = false;
446 bool pressed = _activeButton->selected(event.point, _focus->_x,
447 _focus->_y - _focus->_scrollY, redraw);
448 if (redraw || (pressed != _activeButton->_pressed)) {
449 _activeButton->_pressed = pressed;
450 drawActiveButton();
451 result = true;
452 }
453 } else if (!_swipeExit && _xMove != -1 && _yMove != -1 &&
454 _front->overlaps(event.point.x, event.point.y)) {
455 int hscroll = _front->_scrollX + (_xMove - event.point.x);
456 int vscroll = _front->_scrollY + (_yMove - event.point.y);
457 int maxHScroll = MAX(0, _front->getMaxHScroll());
458 int maxVScroll = (_front->_curY - _front->_height) + (2 * _fontSize);
459 if (hscroll < 0) {
460 hscroll = 0;
461 } else if (hscroll > maxHScroll) {
462 hscroll = maxHScroll;
463 }
464 if (vscroll < 0) {
465 vscroll = 0;
466 }
467 if ((abs(hscroll - _front->_scrollX) > H_SCROLL_SIZE
468 && maxHScroll > 0
469 && hscroll <= maxHScroll) ||
470 (vscroll != _front->_scrollY && maxVScroll > 0 &&
471 vscroll < maxVScroll + OVER_SCROLL)) {
472 _front->drawInto();
473 _front->_scrollX = hscroll;
474 _front->_scrollY = vscroll;
475 _xMove = event.point.x;
476 _yMove = event.point.y;
477 flush(true, true);
478 result = true;
479 }
480 } else {
481 result = drawHoverLink(event);
482 }
483 return result;
484}
485
486// handler for pointer release events
487void AnsiWidget::pointerReleaseEvent(MAEvent &event) {
488 if (_activeButton != nullptr && _front == _screens[MENU_SCREEN]) {
489 _activeButton->clicked(event.point.x, event.point.y, false);
490 } else if (_activeButton != nullptr && _activeButton->_pressed) {
491 _activeButton->_pressed = false;
492 drawActiveButton();
493 _activeButton->clicked(event.point.x, event.point.y, false);
494 } else if (_swipeExit) {
495 _swipeExit = false;
496 } else {
497 int maxScroll = (_front->_curY - _front->_height) + (2 * _fontSize);
498 if (_yMove != -1 && maxScroll > 0) {
499 _front->drawInto();
500
501 // swipe test - min distance and not max duration
502 int deltaX = _xTouch - event.point.x;
503 int deltaY = _yTouch - event.point.y;
504 int distance = (int) fabs(sqrt(deltaX * deltaX + deltaY * deltaY));
505 int now = maGetMilliSecondCount();
506 if (distance >= SWIPE_MIN_DISTANCE && (now - _touchTime) < SWIPE_MAX_DURATION) {
507 bool moveDown = (deltaY >= SWIPE_MIN_DISTANCE);
508 doSwipe(now, moveDown, distance, maxScroll);
509 } else if (_front->_scrollY > maxScroll) {
510 _front->_scrollY = maxScroll;
511 }
512 // ensure the scrollbar is removed
513 _front->_dirty = true;
514 flush(true);
515 _touchTime = 0;
516 }
517 }
518
519 if (_hoverInput) {
520 int dx = _front->_x;
521 int dy = _front->_y - _front->_scrollY;
522 _hoverInput->drawHover(dx, dy, false);
523 _hoverInput = nullptr;
524 }
525
526 _xTouch = _xMove = -1;
527 _yTouch = _yMove = -1;
528 _activeButton = nullptr;
529 _focus = nullptr;
530}
531
532// handles the characters following the \e[ sequence. Returns whether a further call
533// is required to complete the process.
534bool AnsiWidget::doEscape(const char *&p, int textHeight) {
535 int escValue = 0;
536
537 while (isdigit(*p)) {
538 escValue = (escValue * 10) + (*p - '0');
539 p++;
540 }
541
542 if (_back->setGraphicsRendition(*p, escValue, textHeight)) {
543 _back->updateFont();
544 }
545
546 bool result = false;
547 if (*p == ';') {
548 result = true;
549 // advance to next rendition
550 p++;
551 }
552 return result;
553}
554
555// swipe handler for pointerReleaseEvent()
556void AnsiWidget::doSwipe(int start, bool moveDown, int distance, int maxScroll) {
557 MAEvent event;
558 int elapsed = 0;
559 int vscroll = _front->_scrollY;
560 int scrollSize = distance / 3;
561 int swipeStep = SWIPE_DELAY_STEP;
562 while (elapsed < SWIPE_MAX_TIMER) {
563 if (maGetEvent(&event) && event.type == EVENT_TYPE_POINTER_RELEASED) {
564 // ignore the next move and release events
565 _swipeExit = true;
566 break;
567 }
568 elapsed += (maGetMilliSecondCount() - start);
569 if (elapsed > swipeStep && scrollSize > 1) {
570 // step down to a lesser scroll amount
571 scrollSize -= 1;
572 swipeStep += SWIPE_DELAY_STEP;
573 }
574 if (scrollSize == 1) {
575 maWait(20);
576 }
577 vscroll += moveDown ? scrollSize : -scrollSize;
578 if (vscroll < 0) {
579 vscroll = 0;
580 } else if (vscroll > maxScroll) {
581 vscroll = maxScroll;
582 }
583 if (vscroll != _front->_scrollY) {
584 _front->_dirty = true; // forced
585 _front->_scrollY = vscroll;
586 flush(true, true);
587 } else {
588 break;
589 }
590 }
591
592 // pause before removing the scrollbar
593 maWait(500);
594}
595
596// draws the focus screen's active button
597void AnsiWidget::drawActiveButton() {
598#if defined(_SDL) || defined(_FLTK)
599 if (_focus != nullptr && !_activeButton->hasHover()) {
600 MAHandle currentHandle = maSetDrawTarget(HANDLE_SCREEN);
601 _focus->drawShape(_activeButton);
602 _focus->drawLabel();
603 if (_activeButton->isFullScreen()) {
604 _focus->drawMenu();
605 }
606 maUpdateScreen();
607 maSetDrawTarget(currentHandle);
608 }
609#else
610 if (_activeButton->hasHover()) {
611 int dx = _front->_x;
612 int dy = _front->_y - _front->_scrollY;
613 _activeButton->drawHover(dx, dy, _activeButton->_pressed);
614 } else if (_focus != nullptr) {
615 MAHandle currentHandle = maSetDrawTarget(HANDLE_SCREEN);
616 _focus->drawShape(_activeButton);
617 _focus->drawLabel();
618 if (_activeButton->isFullScreen()) {
619 _focus->drawMenu();
620 }
621 maUpdateScreen();
622 maSetDrawTarget(currentHandle);
623 }
624#endif
625}
626
627bool AnsiWidget::drawHoverLink(MAEvent &event) {
628#if defined(_SDL) || defined(_FLTK)
629 if (_front != _screens[MENU_SCREEN]) {
630 int dx = _front->_x;
631 int dy = _front->_y - _front->_scrollY;
632 FormInput *active = nullptr;
633 if (_front->overlaps(event.point.x, event.point.y)) {
634 List_each(FormInput *, it, _front->_inputs) {
635 FormInput *widget = *it;
636 if (widget->hasHover() &&
637 widget->overlaps(event.point, dx, dy)) {
638 active = widget;
639 break;
640 }
641 }
642 }
643 if (active && active != _hoverInput) {
644 if (_hoverInput) {
645 // remove old hover
646 _hoverInput->drawHover(dx, dy, false);
647 }
648 // display new hover
649 _hoverInput = active;
650 _hoverInput->drawHover(dx, dy, true);
651 } else if (!active && _hoverInput) {
652 // no new hover, erase old hover
653 _hoverInput->drawHover(dx, dy, false);
654 _hoverInput = nullptr;
655 }
656 }
657#endif
658 return _hoverInput != nullptr;
659}
660
661// print() helper
662void AnsiWidget::handleEscape(const char *&p, int lineHeight) {
663 switch (*(p + 1)) {
664 case '[':
665 p += 2;
666 while (doEscape(p, lineHeight)) {
667 // continue
668 }
669 break;
670 case 'm':
671 // scroll to the top (M = scroll up one line)
672 p += 2;
673 _back->_scrollY = 0;
674 break;
675 case '<':
676 // select back screen
677 switch (*(p + 2)) {
678 case '1':
679 p += 3;
680 selectBackScreen(USER_SCREEN1);
681 break;
682 case '2':
683 p += 3;
684 selectBackScreen(USER_SCREEN2);
685 break;
686 }
687 break;
688 case '>':
689 // select front screen
690 switch (*(p + 2)) {
691 case '1':
692 p += 3;
693 selectFrontScreen(USER_SCREEN1);
694 break;
695 case '2':
696 p += 3;
697 selectFrontScreen(USER_SCREEN2);
698 break;
699 }
700 break;
701 default:
702 break;
703 }
704}
705
706// returns whether the event is over the given screen
707bool AnsiWidget::setActiveButton(MAEvent &event, Screen *screen) {
708 bool result = false;
709 if (_front != _screens[MENU_SCREEN] &&
710 screen->overlaps(event.point.x, event.point.y)) {
711 List_each(FormInput *, it, screen->_inputs) {
712 FormInput *widget = *it;
713 bool redraw = false;
714 if (widget->selected(event.point, screen->_x,
715 screen->_y - screen->_scrollY, redraw)) {
716 _activeButton = widget;
717 _activeButton->_pressed = true;
718 break;
719 }
720 if (redraw) {
721 _front->_dirty = true;
722 flush(true);
723 }
724 }
725 // screen overlaps event - avoid search in other screens
726 result = true;
727 }
728 return result;
729}
730
731void AnsiWidget::selectBackScreen(int screenId) {
732 _hoverInput = nullptr;
733 _back = createScreen(screenId);
734 _back->selectFont();
735}
736
737void AnsiWidget::selectFrontScreen(int screenId) {
738 _front = createScreen(screenId);
739 _front->_dirty = true;
740 flush(true);
741}
742
743int AnsiWidget::selectScreen(int screenId, bool forceFlush) {
744 int result = getScreenId(true);
745 selectBackScreen(screenId);
746 _front = _back;
747 _front->_dirty = true;
748 flush(forceFlush);
749 return result;
750}
751