/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program 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; either version 2 * of the License, or (at your option) any later version. * * This program 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "glk/window_text_buffer.h" #include "glk/conf.h" #include "glk/glk.h" #include "glk/screen.h" #include "glk/selection.h" #include "glk/unicode.h" namespace Glk { /** * * How many pixels we add to left/right margins */ #define SLOP (2 * GLI_SUBPIX) TextBufferWindow::TextBufferWindow(Windows *windows, uint rock) : TextWindow(windows, rock), _font(g_conf->_propInfo), _historyPos(0), _historyFirst(0), _historyPresent(0), _lastSeen(0), _scrollPos(0), _scrollMax(0), _scrollBack(SCROLLBACK), _width(-1), _height(-1), _inBuf(nullptr), _lineTerminators(nullptr), _echoLineInput(true), _ladjw(0), _radjw(0), _ladjn(0), _radjn(0), _numChars(0), _chars(nullptr), _attrs(nullptr), _spaced(0), _dashed(0), _copyBuf(0), _copyPos(0) { _type = wintype_TextBuffer; _history.resize(HISTORYLEN); _lines.resize(SCROLLBACK); _chars = _lines[0]._chars; _attrs = _lines[0]._attrs; Common::copy(&g_conf->_tStyles[0], &g_conf->_tStyles[style_NUMSTYLES], _styles); } TextBufferWindow::~TextBufferWindow() { if (_inBuf) { if (g_vm->gli_unregister_arr) (*g_vm->gli_unregister_arr)(_inBuf, _inMax, "&+#!Cn", _inArrayRock); _inBuf = nullptr; } delete[] _copyBuf; delete[] _lineTerminators; for (int i = 0; i < _scrollBack; i++) { if (_lines[i]._lPic) _lines[i]._lPic->decrement(); if (_lines[i]._rPic) _lines[i]._rPic->decrement(); } } void TextBufferWindow::rearrange(const Rect &box) { Window::rearrange(box); int newwid, newhgt; int rnd; newwid = MAX((box.width() - g_conf->_tMarginX * 2 - g_conf->_scrollWidth) / _font._cellW, 0); newhgt = MAX((box.height() - g_conf->_tMarginY * 2) / _font._cellH, 0); // align text with bottom rnd = newhgt * _font._cellH + g_conf->_tMarginY * 2; _yAdj = (box.height() - rnd); _bbox.top += (box.height() - rnd); if (newwid != _width) { _width = newwid; reflow(); } if (newhgt != _height) { // scroll up if we obscure new lines if (_lastSeen >= newhgt - 1) _scrollPos += (_height - newhgt); _height = newhgt; // keep window within 'valid' lines if (_scrollPos > _scrollMax - _height + 1) _scrollPos = _scrollMax - _height + 1; if (_scrollPos < 0) _scrollPos = 0; touchScroll(); // allocate copy buffer if (_copyBuf) delete[] _copyBuf; _copyBuf = new uint32[_height * TBLINELEN]; for (int i = 0; i < (_height * TBLINELEN); i++) _copyBuf[i] = 0; _copyPos = 0; } } void TextBufferWindow::reflow() { int inputbyte = -1; Attributes curattr, oldattr; int i, k, p, s; int x; if (_height < 4 || _width < 20) return; _lines[0]._len = _numChars; // allocate temp buffers Attributes *attrbuf = new Attributes[SCROLLBACK * TBLINELEN]; uint32 *charbuf = new uint32[SCROLLBACK * TBLINELEN]; int *alignbuf = new int[SCROLLBACK]; Picture **pictbuf = new Picture *[SCROLLBACK]; uint *hyperbuf = new uint[SCROLLBACK]; int *offsetbuf = new int[SCROLLBACK]; if (!attrbuf || !charbuf || !alignbuf || !pictbuf || !hyperbuf || !offsetbuf) { delete[] attrbuf; delete[] charbuf; delete[] alignbuf; delete[] pictbuf; delete[] hyperbuf; delete[] offsetbuf; return; } // copy text to temp buffers oldattr = _attr; curattr.clear(); x = 0; p = 0; s = _scrollMax < SCROLLBACK ? _scrollMax : SCROLLBACK - 1; for (k = s; k >= 0; k--) { if (k == 0 && _lineRequest) inputbyte = p + _inFence; if (_lines[k]._lPic) { offsetbuf[x] = p; alignbuf[x] = imagealign_MarginLeft; pictbuf[x] = _lines[k]._lPic; if (pictbuf[x]) pictbuf[x]->increment(); hyperbuf[x] = _lines[k]._lHyper; x++; } if (_lines[k]._rPic) { offsetbuf[x] = p; alignbuf[x] = imagealign_MarginRight; pictbuf[x] = _lines[k]._rPic; if (pictbuf[x]) pictbuf[x]->increment(); hyperbuf[x] = _lines[k]._rHyper; x++; } for (i = 0; i < _lines[k]._len; i++) { attrbuf[p] = curattr = _lines[k]._attrs[i]; charbuf[p] = _lines[k]._chars[i]; p++; } if (_lines[k]._newLine) { attrbuf[p] = curattr; charbuf[p] = '\n'; p++; } } offsetbuf[x] = -1; // clear window clear(); // and dump text back x = 0; for (i = 0; i < p; i++) { if (i == inputbyte) break; _attr = attrbuf[i]; if (offsetbuf[x] == i) { putPicture(pictbuf[x], alignbuf[x], hyperbuf[x]); x++; } putCharUni(charbuf[i]); } // terribly sorry about this... _lastSeen = 0; _scrollPos = 0; if (inputbyte != -1) { _inFence = _numChars; putTextUni(charbuf + inputbyte, p - inputbyte, _numChars, 0); _inCurs = _numChars; } // free temp buffers delete[] attrbuf; delete[] charbuf; delete[] alignbuf; delete[] pictbuf; delete[] hyperbuf; delete[] offsetbuf; _attr = oldattr; touchScroll(); } void TextBufferWindow::touchScroll() { g_vm->_selection->clearSelection(); _windows->repaint(_bbox); for (int i = 0; i < _scrollMax; i++) _lines[i]._dirty = true; } bool TextBufferWindow::putPicture(Picture *pic, uint align, uint linkval) { if (align == imagealign_MarginRight) { if (_lines[0]._rPic || _numChars) return false; _radjw = (pic->w + g_conf->_tMarginX) * GLI_SUBPIX; _radjn = (pic->h + _font._cellH - 1) / _font._cellH; _lines[0]._rPic = pic; _lines[0]._rm = _radjw; _lines[0]._rHyper = linkval; } else { if (align != imagealign_MarginLeft && _numChars) putCharUni('\n'); if (_lines[0]._lPic || _numChars) return false; _ladjw = (pic->w + g_conf->_tMarginX) * GLI_SUBPIX; _ladjn = (pic->h + _font._cellH - 1) / _font._cellH; _lines[0]._lPic = pic; _lines[0]._lm = _ladjw; _lines[0]._lHyper = linkval; if (align != imagealign_MarginLeft) flowBreak(); } return true; } uint TextBufferWindow::drawPicture(uint image, uint align, uint scaled, uint width, uint height) { Picture *pic; uint hyperlink; int error; pic = g_vm->_pictures->load(image); if (!pic) return false; if (!_imageLoaded) { g_vm->_pictures->increment(); _imageLoaded = true; } if (scaled) { Picture *tmp; tmp = g_vm->_pictures->scale(pic, width, height); pic = tmp; } hyperlink = _attr.hyper; pic->increment(); error = putPicture(pic, align, hyperlink); return error; } void TextBufferWindow::putText(const char *buf, int len, int pos, int oldlen) { int diff = len - oldlen; if (_numChars + diff >= TBLINELEN) return; if (diff != 0 && pos + oldlen < _numChars) { memmove(_chars + pos + len, _chars + pos + oldlen, (_numChars - (pos + oldlen)) * 4); memmove(_attrs + pos + len, _attrs + pos + oldlen, (_numChars - (pos + oldlen)) * sizeof(Attributes)); } if (len > 0) { for (int i = 0; i < len; i++) { _chars[pos + i] = buf[i]; _attrs[pos + i].set(style_Input); } } _numChars += diff; if (_inBuf) { if (_inCurs >= pos + oldlen) _inCurs += diff; else if (_inCurs >= pos) _inCurs = pos + len; } touch(0); } void TextBufferWindow::putTextUni(const uint32 *buf, int len, int pos, int oldlen) { int diff = len - oldlen; if (_numChars + diff >= TBLINELEN) return; if (diff != 0 && pos + oldlen < _numChars) { memmove(_chars + pos + len, _chars + pos + oldlen, (_numChars - (pos + oldlen)) * 4); memmove(_attrs + pos + len, _attrs + pos + oldlen, (_numChars - (pos + oldlen)) * sizeof(Attributes)); } if (len > 0) { int i; memmove(_chars + pos, buf, len * 4); for (i = 0; i < len; i++) _attrs[pos + i].set(style_Input); } _numChars += diff; if (_inBuf) { if (_inCurs >= pos + oldlen) _inCurs += diff; else if (_inCurs >= pos) _inCurs = pos + len; } touch(0); } void TextBufferWindow::touch(int line) { _lines[line]._dirty = true; g_vm->_selection->clearSelection(); int y = _bbox.top + g_conf->_tMarginY + (_height - line - 1) * _font._leading; _windows->repaint(Rect(_bbox.left, y - 2, _bbox.right, y + _font._leading + 2)); } uint TextBufferWindow::getSplit(uint size, bool vertical) const { return (vertical) ? size * _font._cellW : size * _font._cellH; } void TextBufferWindow::putCharUni(uint32 ch) { uint bchars[TBLINELEN]; Attributes battrs[TBLINELEN]; int pw; int bpoint; int saved; int i; int linelen; uint color; gli_tts_speak(&ch, 1); pw = (_bbox.right - _bbox.left - g_conf->_tMarginX * 2 - g_conf->_scrollWidth) * GLI_SUBPIX; pw = pw - 2 * SLOP - _radjw - _ladjw; color = Windows::_overrideBgSet ? g_conf->_windowColor : _bgColor; // oops ... overflow if (_numChars + 1 >= TBLINELEN) scrollOneLine(0); if (ch == '\n') { scrollOneLine(1); return; } if (_font._quotes) { // fails for 'tis a wonderful day in the '80s if (_font._quotes > 1 && ch == '\'') { if (_numChars == 0 || leftquote(_chars[_numChars - 1])) ch = UNI_LSQUO; } if (ch == '`') ch = UNI_LSQUO; if (ch == '\'') ch = UNI_RSQUO; if (ch == '"') { if (_numChars == 0 || leftquote(_chars[_numChars - 1])) ch = UNI_LDQUO; else ch = UNI_RDQUO; } } if (_font._dashes && _attr.style != style_Preformatted) { if (ch == '-') { _dashed++; if (_dashed == 2) { _numChars--; if (_font._dashes == 2) ch = UNI_NDASH; else ch = UNI_MDASH; } if (_dashed == 3) { _numChars--; ch = UNI_MDASH; _dashed = 0; } } else { _dashed = 0; } } if (_font._spaces && _attr.style != style_Preformatted && _styles[_attr.style].bg == color && !_styles[_attr.style].reverse) { // turn (period space space) into (period space) if (_font._spaces == 1) { if (ch == '.') _spaced = 1; else if (ch == ' ' && _spaced == 1) _spaced = 2; else if (ch == ' ' && _spaced == 2) { _spaced = 0; return; } else { _spaced = 0; } } // Turn (per sp x) into (per sp sp x) if (_font._spaces == 2) { if (ch == '.') _spaced = 1; else if (ch == ' ' && _spaced == 1) _spaced = 2; else if (ch != ' ' && _spaced == 2) { _spaced = 0; putCharUni(' '); } else { _spaced = 0; } } } _chars[_numChars] = ch; _attrs[_numChars] = _attr; _numChars++; // kill spaces at the end for line width calculation linelen = _numChars; while (linelen > 1 && _chars[linelen - 1] == ' ' && _styles[_attrs[linelen - 1].style].bg == color && !_styles[_attrs[linelen - 1].style].reverse) linelen--; if (calcWidth(_chars, _attrs, 0, linelen, -1) >= pw) { bpoint = _numChars; for (i = _numChars - 1; i > 0; i--) { if (_chars[i] == ' ') { bpoint = i + 1; // skip space break; } } saved = _numChars - bpoint; memcpy(bchars, _chars + bpoint, saved * 4); memcpy(battrs, _attrs + bpoint, saved * sizeof(Attributes)); _numChars = bpoint; scrollOneLine(0); memcpy(_chars, bchars, saved * 4); memcpy(_attrs, battrs, saved * sizeof(Attributes)); _numChars = saved; } touch(0); } bool TextBufferWindow::unputCharUni(uint32 ch) { if (_numChars > 0 && _chars[_numChars - 1] == ch) { _numChars--; touch(0); return true; } return false; } void TextBufferWindow::clear() { int i; _attr.fgset = Windows::_overrideFgSet; _attr.bgset = Windows::_overrideBgSet; _attr.fgcolor = Windows::_overrideFgSet ? Windows::_overrideFgVal : 0; _attr.bgcolor = Windows::_overrideBgSet ? Windows::_overrideBgVal : 0; _attr.reverse = false; _ladjw = _radjw = 0; _ladjn = _radjn = 0; _spaced = 0; _dashed = 0; _numChars = 0; for (i = 0; i < _scrollBack; i++) { _lines[i]._len = 0; if (_lines[i]._lPic) _lines[i]._lPic->decrement(); _lines[i]._lPic = nullptr; if (_lines[i]._rPic) _lines[i]._rPic->decrement(); _lines[i]._rPic = nullptr; _lines[i]._lHyper = 0; _lines[i]._rHyper = 0; _lines[i]._lm = 0; _lines[i]._rm = 0; _lines[i]._newLine = 0; _lines[i]._dirty = true; _lines[i]._repaint = false; } _lastSeen = 0; _scrollPos = 0; _scrollMax = 0; for (i = 0; i < _height; i++) touch(i); } void TextBufferWindow::click(const Point &newPos) { int gh = false; int gs = false; if (_lineRequest || _charRequest || _lineRequestUni || _charRequestUni || _moreRequest || _scrollRequest) _windows->setFocus(this); if (_hyperRequest) { uint linkval = g_vm->_selection->getHyperlink(newPos); if (linkval) { g_vm->_events->store(evtype_Hyperlink, this, linkval, 0); _hyperRequest = false; if (g_conf->_safeClicks) g_vm->_events->_forceClick = 1; gh = true; } } if (newPos.x > _bbox.right - g_conf->_scrollWidth) { if (newPos.y < _bbox.top + g_conf->_tMarginY + g_conf->_scrollWidth) acceptScroll(keycode_Up); else if (newPos.y > _bbox.bottom - g_conf->_tMarginY - g_conf->_scrollWidth) acceptScroll(keycode_Down); else if (newPos.y < (_bbox.top + _bbox.bottom) / 2) acceptScroll(keycode_PageUp); else acceptScroll(keycode_PageDown); gs = true; } if (!gh && !gs) { g_vm->_copySelect = true; g_vm->_selection->startSelection(newPos); } } void TextBufferWindow::requestLineEvent(char *buf, uint maxlen, uint initlen) { if (_charRequest || _lineRequest || _charRequestUni || _lineRequestUni) { warning("request_line_event: window already has keyboard request"); return; } _lineRequest = true; int pw; gli_tts_flush(); // because '>' prompt is ugly without extra space if (_numChars && _chars[_numChars - 1] == '>') putCharUni(' '); if (_numChars && _chars[_numChars - 1] == '?') putCharUni(' '); // make sure we have some space left for typing... pw = (_bbox.right - _bbox.left - g_conf->_tMarginX * 2) * GLI_SUBPIX; pw = pw - 2 * SLOP - _radjw + _ladjw; if (calcWidth(_chars, _attrs, 0, _numChars, -1) >= pw * 3 / 4) putCharUni('\n'); _inBuf = buf; _inMax = maxlen; _inFence = _numChars; _inCurs = _numChars; _origAttr = _attr; _attr.set(style_Input); _historyPos = _historyPresent; if (initlen) { touch(0); putText(buf, initlen, _inCurs, 0); } // WORKAROUND: Mark bottom line as dirty so caret will be drawn _lines[0]._dirty = true; _echoLineInput = _echoLineInputBase; if (_lineTerminatorsBase && _termCt) { _lineTerminators = new uint[_termCt + 1]; if (_lineTerminators) { memcpy(_lineTerminators, _lineTerminatorsBase, _termCt * sizeof(uint)); _lineTerminators[_termCt] = 0; } } if (g_vm->gli_register_arr) _inArrayRock = (*g_vm->gli_register_arr)(buf, maxlen, "&+#!Cn"); // Switch focus to the new window _windows->inputGuessFocus(); } void TextBufferWindow::requestLineEventUni(uint32 *buf, uint maxlen, uint initlen) { if (_charRequest || _lineRequest || _charRequestUni || _lineRequestUni) { warning("request_line_event_uni: window already has keyboard request"); return; } int pw; _lineRequestUni = true; gli_tts_flush(); // because '>' prompt is ugly without extra space if (_numChars && _chars[_numChars - 1] == '>') putCharUni(' '); if (_numChars && _chars[_numChars - 1] == '?') putCharUni(' '); // make sure we have some space left for typing... pw = (_bbox.right - _bbox.left - g_conf->_tMarginX * 2) * GLI_SUBPIX; pw = pw - 2 * SLOP - _radjw + _ladjw; if (calcWidth(_chars, _attrs, 0, _numChars, -1) >= pw * 3 / 4) putCharUni('\n'); //_lastSeen = 0; _inBuf = buf; _inMax = maxlen; _inFence = _numChars; _inCurs = _numChars; _origAttr = _attr; _attr.set(style_Input); _historyPos = _historyPresent; if (initlen) { touch(0); putTextUni(buf, initlen, _inCurs, 0); } _echoLineInput = _echoLineInputBase; if (_lineTerminatorsBase && _termCt) { _lineTerminators = new uint[_termCt + 1]; if (_lineTerminators) { memcpy(_lineTerminators, _lineTerminatorsBase, _termCt * sizeof(uint)); _lineTerminators[_termCt] = 0; } } if (g_vm->gli_register_arr) _inArrayRock = (*g_vm->gli_register_arr)(buf, maxlen, "&+#!Iu"); // Switch focus to the new window _windows->inputGuessFocus(); } void TextBufferWindow::requestCharEvent() { _charRequest = true; // Switch focus to the new window _windows->inputGuessFocus(); } void TextBufferWindow::requestCharEventUni() { _charRequestUni = true; // Switch focus to the new window _windows->inputGuessFocus(); } void TextBufferWindow::cancelLineEvent(Event *ev) { gidispatch_rock_t inarrayrock; int ix; int len; void *inbuf; int inmax; int unicode = _lineRequestUni; Event dummyEv; if (!ev) ev = &dummyEv; ev->clear(); if (!_lineRequest && !_lineRequestUni) return; if (!_inBuf) return; inbuf = _inBuf; inmax = _inMax; inarrayrock = _inArrayRock; len = _numChars - _inFence; if (_echoStream) _echoStream->echoLineUni(_chars + _inFence, len); if (len > inmax) len = inmax; if (!unicode) { for (ix = 0; ix < len; ix++) { uint32 ch = _chars[_inFence + ix]; if (ch > 0xff) ch = '?'; ((char *)inbuf)[ix] = (char)ch; } } else { for (ix = 0; ix < len; ix++) ((uint *)inbuf)[ix] = _chars[_inFence + ix]; } _attr = _origAttr; ev->type = evtype_LineInput; ev->window = this; ev->val1 = len; ev->val2 = 0; _lineRequest = false; _lineRequestUni = false; if (_lineTerminators) { delete[] _lineTerminators; _lineTerminators = nullptr; } _inBuf = nullptr; _inMax = 0; if (_echoLineInput) { putCharUni('\n'); } else { _numChars = _inFence; touch(0); } if (g_vm->gli_unregister_arr) (*g_vm->gli_unregister_arr)(inbuf, inmax, unicode ? "&+#!Iu" : "&+#!Cn", inarrayrock); } void TextBufferWindow::redraw() { int linelen; int nsp, spw, pw; int x0, y0, x1, y1; int x, y, w; int a, b; uint link; int font; uint color; int i; int hx0, hx1, hy0, hy1; int selrow, selchar, sx0, sx1, selleft, selright; bool selBuf; int tx, tsc, tsw, lsc, rsc; Screen &screen = *g_vm->_screen; Window::redraw(); _lines[0]._len = _numChars; sx0 = sx1 = selleft = selright = 0; x0 = (_bbox.left + g_conf->_tMarginX) * GLI_SUBPIX; x1 = (_bbox.right - g_conf->_tMarginX - g_conf->_scrollWidth) * GLI_SUBPIX; y0 = _bbox.top + g_conf->_tMarginY; y1 = _bbox.bottom - g_conf->_tMarginY; pw = x1 - x0 - 2 * GLI_SUBPIX; // check if any part of buffer is selected selBuf = g_vm->_selection->checkSelection(Rect(x0 / GLI_SUBPIX, y0, x1 / GLI_SUBPIX, y1)); for (i = _scrollPos + _height - 1; i >= _scrollPos; i--) { // top of line y = y0 + (_height - (i - _scrollPos) - 1) * _font._leading; // check if part of line is selected if (selBuf) { selrow = g_vm->_selection->getSelection(Rect(x0 / GLI_SUBPIX, y, x1 / GLI_SUBPIX, y + _font._leading), &sx0, &sx1); selleft = (sx0 == x0 / GLI_SUBPIX); selright = (sx1 == x1 / GLI_SUBPIX); } else { selrow = false; } // mark selected line dirty if (selrow) _lines[i]._dirty = true; TextBufferRow ln(_lines[i]); // skip if we can if (!ln._dirty && !ln._repaint && !Windows::_forceRedraw && _scrollPos == 0) continue; // repaint previously selected lines if needed if (ln._repaint && !Windows::_forceRedraw) _windows->redrawRect(Rect(x0 / GLI_SUBPIX, y, x1 / GLI_SUBPIX, y + _font._leading)); // keep selected line dirty and flag for repaint if (!selrow) { _lines[i]._dirty = false; _lines[i]._repaint = false; } else { _lines[i]._repaint = true; } // leave bottom line blank for [more] prompt if (i == _scrollPos && i > 0) continue; linelen = ln._len; // kill spaces at the end unless they're a different color color = Windows::_overrideBgSet ? g_conf->_windowColor : _bgColor; while (i > 0 && linelen > 1 && ln._chars[linelen - 1] == ' ' && _styles[ln._attrs[linelen - 1].style].bg == color && !_styles[ln._attrs[linelen - 1].style].reverse) linelen --; // kill characters that would overwrite the scroll bar while (linelen > 1 && calcWidth(ln._chars, ln._attrs, 0, linelen, -1) >= pw) linelen --; /* * count spaces and width for justification */ if (_font._justify && !ln._newLine && i > 0) { for (a = 0, nsp = 0; a < linelen; a++) if (ln._chars[a] == ' ') nsp ++; w = calcWidth(ln._chars, ln._attrs, 0, linelen, 0); if (nsp) spw = (x1 - x0 - ln._lm - ln._rm - 2 * SLOP - w) / nsp; else spw = 0; } else { spw = -1; } // find and highlight selected characters if (selrow && !Windows::_claimSelect) { lsc = 0; rsc = 0; selchar = false; // optimized case for all chars selected if (selleft && selright) { rsc = linelen > 0 ? linelen - 1 : 0; selchar = calcWidth(ln._chars, ln._attrs, lsc, rsc, spw) / GLI_SUBPIX; } else { // optimized case for leftmost char selected if (selleft) { tsc = linelen > 0 ? linelen - 1 : 0; selchar = calcWidth(ln._chars, ln._attrs, lsc, tsc, spw) / GLI_SUBPIX; } else { // find the substring contained by the selection tx = (x0 + SLOP + ln._lm) / GLI_SUBPIX; // measure string widths until we find left char for (tsc = 0; tsc < linelen; tsc++) { tsw = calcWidth(ln._chars, ln._attrs, 0, tsc, spw) / GLI_SUBPIX; if (tsw + tx >= sx0 || ((tsw + tx + GLI_SUBPIX) >= sx0 && ln._chars[tsc] != ' ')) { lsc = tsc; selchar = true; break; } } } if (selchar) { // optimized case for rightmost char selected if (selright) { rsc = linelen > 0 ? linelen - 1 : 0; } else { // measure string widths until we find right char for (tsc = lsc; tsc < linelen; tsc++) { tsw = calcWidth(ln._chars, ln._attrs, lsc, tsc, spw) / GLI_SUBPIX; if (tsw + sx0 < sx1) rsc = tsc; } if (lsc && !rsc) rsc = lsc; } } } // reverse colors for selected chars if (selchar) { for (tsc = lsc; tsc <= rsc; tsc++) { ln._attrs[tsc].reverse = !ln._attrs[tsc].reverse; _copyBuf[_copyPos] = ln._chars[tsc]; _copyPos++; } } // add newline if we reach the end of the line if (ln._len == 0 || ln._len == (rsc + 1)) { _copyBuf[_copyPos] = '\n'; _copyPos++; } } // clear any stored hyperlink coordinates g_vm->_selection->putHyperlink(0, x0 / GLI_SUBPIX, y, x1 / GLI_SUBPIX, y + _font._leading); /* * fill in background colors */ color = Windows::_overrideBgSet ? g_conf->_windowColor : _bgColor; screen.fillRect(Rect::fromXYWH(x0 / GLI_SUBPIX, y, (x1 - x0) / GLI_SUBPIX, _font._leading), color); x = x0 + SLOP + ln._lm; a = 0; for (b = 0; b < linelen; b++) { if (ln._attrs[a] != ln._attrs[b]) { link = ln._attrs[a].hyper; font = ln._attrs[a].attrFont(_styles); color = ln._attrs[a].attrBg(_styles); w = screen.stringWidthUni(font, Common::U32String(ln._chars + a, b - a), spw); screen.fillRect(Rect::fromXYWH(x / GLI_SUBPIX, y, w / GLI_SUBPIX, _font._leading), color); if (link) { screen.fillRect(Rect::fromXYWH(x / GLI_SUBPIX + 1, y + _font._baseLine + 1, w / GLI_SUBPIX + 1, _font._linkStyle), _font._linkColor); g_vm->_selection->putHyperlink(link, x / GLI_SUBPIX, y, x / GLI_SUBPIX + w / GLI_SUBPIX, y + _font._leading); } x += w; a = b; } } link = ln._attrs[a].hyper; font = ln._attrs[a].attrFont(_styles); color = ln._attrs[a].attrBg(_styles); w = screen.stringWidthUni(font, Common::U32String(ln._chars + a, b - a), spw); screen.fillRect(Rect::fromXYWH(x / GLI_SUBPIX, y, w / GLI_SUBPIX, _font._leading), color); if (link) { screen.fillRect(Rect::fromXYWH(x / GLI_SUBPIX + 1, y + _font._baseLine + 1, w / GLI_SUBPIX + 1, _font._linkStyle), _font._linkColor); g_vm->_selection->putHyperlink(link, x / GLI_SUBPIX, y, x / GLI_SUBPIX + w / GLI_SUBPIX, y + _font._leading); } x += w; color = Windows::_overrideBgSet ? g_conf->_windowColor : _bgColor; screen.fillRect(Rect::fromXYWH(x / GLI_SUBPIX, y, x1 / GLI_SUBPIX - x / GLI_SUBPIX, _font._leading), color); /* * draw caret */ if (_windows->getFocusWindow() == this && i == 0 && (_lineRequest || _lineRequestUni)) { w = calcWidth(_chars, _attrs, 0, _inCurs, spw); if (w < pw - _font._caretShape * 2 * GLI_SUBPIX) _font.drawCaret(Point(x0 + SLOP + ln._lm + w, y + _font._baseLine)); } /* * draw text */ x = x0 + SLOP + ln._lm; a = 0; for (b = 0; b < linelen; b++) { if (ln._attrs[a] != ln._attrs[b]) { link = ln._attrs[a].hyper; font = ln._attrs[a].attrFont(_styles); color = link ? _font._linkColor : ln._attrs[a].attrFg(_styles); x = screen.drawStringUni(Point(x, y + _font._baseLine), font, color, Common::U32String(ln._chars + a, b - a), spw); a = b; } } link = ln._attrs[a].hyper; font = ln._attrs[a].attrFont(_styles); color = link ? _font._linkColor : ln._attrs[a].attrFg(_styles); screen.drawStringUni(Point(x, y + _font._baseLine), font, color, Common::U32String(ln._chars + a, linelen - a), spw); } /* * draw more prompt */ if (_scrollPos && _height > 1) { x = x0 + SLOP; y = y0 + (_height - 1) * _font._leading; g_vm->_selection->putHyperlink(0, x0 / GLI_SUBPIX, y, x1 / GLI_SUBPIX, y + _font._leading); color = Windows::_overrideBgSet ? g_conf->_windowColor : _bgColor; screen.fillRect(Rect::fromXYWH(x / GLI_SUBPIX, y, x1 / GLI_SUBPIX - x / GLI_SUBPIX, _font._leading), color); w = screen.stringWidth(_font._moreFont, _font._morePrompt); if (_font._moreAlign == 1) // center x = x0 + SLOP + (x1 - x0 - w - SLOP * 2) / 2; if (_font._moreAlign == 2) // right x = x1 - SLOP - w; color = Windows::_overrideFgSet ? _font._moreColor : _fgColor; screen.drawString(Point(x, y + _font._baseLine), _font._moreFont, color, _font._morePrompt); y1 = y; // don't want pictures overdrawing "[more]" // try to claim the focus _moreRequest = true; Windows::_moreFocus = true; } else { _moreRequest = false; y1 = y0 + _height * _font._leading; } /* * draw the images */ for (i = 0; i < _scrollBack; i++) { TextBufferRow ln(_lines[i]); y = y0 + (_height - (i - _scrollPos) - 1) * _font._leading; if (ln._lPic) { if (y < y1 && y + ln._lPic->h > y0) { ln._lPic->drawPicture(Point(x0 / GLI_SUBPIX, y), Rect(x0 / GLI_SUBPIX, y0, x1 / GLI_SUBPIX, y1)); link = ln._lHyper; hy0 = y > y0 ? y : y0; hy1 = y + ln._lPic->h < y1 ? y + ln._lPic->h : y1; hx0 = x0 / GLI_SUBPIX; hx1 = x0 / GLI_SUBPIX + ln._lPic->w < x1 / GLI_SUBPIX ? x0 / GLI_SUBPIX + ln._lPic->w : x1 / GLI_SUBPIX; g_vm->_selection->putHyperlink(link, hx0, hy0, hx1, hy1); } } if (ln._rPic) { if (y < y1 && y + ln._rPic->h > y0) { ln._rPic->drawPicture(Point(x1 / GLI_SUBPIX - ln._rPic->w, y), Rect(x0 / GLI_SUBPIX, y0, x1 / GLI_SUBPIX, y1)); link = ln._rHyper; hy0 = y > y0 ? y : y0; hy1 = y + ln._rPic->h < y1 ? y + ln._rPic->h : y1; hx0 = x1 / GLI_SUBPIX - ln._rPic->w > x0 / GLI_SUBPIX ? x1 / GLI_SUBPIX - ln._rPic->w : x0 / GLI_SUBPIX; hx1 = x1 / GLI_SUBPIX; g_vm->_selection->putHyperlink(link, hx0, hy0, hx1, hy1); } } } /* * Draw the scrollbar */ // try to claim scroll keys _scrollRequest = _scrollMax > _height; if (_scrollRequest && g_conf->_scrollWidth) { int t0, t1; x0 = _bbox.right - g_conf->_scrollWidth; x1 = _bbox.right; y0 = _bbox.top + g_conf->_tMarginY; y1 = _bbox.bottom - g_conf->_tMarginY; g_vm->_selection->putHyperlink(0, x0, y0, x1, y1); y0 += g_conf->_scrollWidth / 2; y1 -= g_conf->_scrollWidth / 2; // pos = thbot, pos - ht = thtop, max = wtop, 0 = wbot t0 = (_scrollMax - _scrollPos) - (_height - 1); t1 = (_scrollMax - _scrollPos); if (_scrollMax > _height) { t0 = t0 * (y1 - y0) / _scrollMax + y0; t1 = t1 * (y1 - y0) / _scrollMax + y0; } else { t0 = t1 = y0; } screen.fillRect(Rect::fromXYWH(x0 + 1, y0, x1 - x0 - 2, y1 - y0), g_conf->_scrollBg); screen.fillRect(Rect::fromXYWH(x0 + 1, t0, x1 - x0 - 2, t1 - t0), g_conf->_scrollFg); for (i = 0; i < g_conf->_scrollWidth / 2 + 1; i++) { screen.fillRect(Rect::fromXYWH(x0 + g_conf->_scrollWidth / 2 - i, y0 - g_conf->_scrollWidth / 2 + i, i * 2, 1), g_conf->_scrollFg); screen.fillRect(Rect::fromXYWH(x0 + g_conf->_scrollWidth / 2 - i, y1 + g_conf->_scrollWidth / 2 - i, i * 2, 1), g_conf->_scrollFg); } } // Keep track of selected text to be ready when user copies it to the clipboard if (selBuf && _copyPos) { Windows::_claimSelect = true; g_vm->_clipboard->clipboardStore(Common::U32String(_copyBuf, _copyPos)); for (i = 0; i < _copyPos; i++) _copyBuf[i] = 0; _copyPos = 0; } // no more prompt means all text has been seen if (!_moreRequest) _lastSeen = 0; } int TextBufferWindow::acceptScroll(uint arg) { int pageht = _height - 2; // 1 for prompt, 1 for overlap int startpos = _scrollPos; switch (arg) { case keycode_PageUp: _scrollPos += pageht; break; case keycode_End: _scrollPos = 0; break; case keycode_Up: _scrollPos++; break; case keycode_Down: case keycode_Return: _scrollPos--; break; case keycode_MouseWheelUp: _scrollPos += 3; startpos = true; break; case keycode_MouseWheelDown: _scrollPos -= 3; startpos = true; break; case ' ': case keycode_PageDown: //default: if (pageht) _scrollPos -= pageht; else _scrollPos = 0; break; } if (_scrollPos > _scrollMax - _height + 1) _scrollPos = _scrollMax - _height + 1; if (_scrollPos < 0) _scrollPos = 0; touchScroll(); return (startpos || _scrollPos); } void TextBufferWindow::acceptReadChar(uint arg) { uint key; if (_height < 2) _scrollPos = 0; if (_scrollPos || arg == keycode_PageUp || arg == keycode_MouseWheelUp) { acceptScroll(arg); return; } switch (arg) { case keycode_Erase: key = keycode_Delete; break; case keycode_MouseWheelUp: case keycode_MouseWheelDown: return; default: key = arg; break; } gli_tts_purge(); if (key > 0xff && key < (0xffffffff - keycode_MAXVAL + 1)) { if (!(_charRequestUni) || key > 0x10ffff) key = keycode_Unknown; } _charRequest = false; _charRequestUni = false; g_vm->_events->store(evtype_CharInput, this, key, 0); } void TextBufferWindow::acceptReadLine(uint32 arg) { uint *cx; Common::U32String s; int len; if (_height < 2) _scrollPos = 0; if (!_inBuf) return; if (_lineTerminators && checkTerminators(arg)) { for (cx = _lineTerminators; *cx; cx++) { if (*cx == arg) { acceptLine(arg); return; } } } if (_scrollPos || arg == keycode_PageUp || arg == keycode_MouseWheelUp) { acceptScroll(arg); return; } switch (arg) { // History keys (up and down) case keycode_Up: if (_historyPos == _historyFirst) return; if (_historyPos == _historyPresent) { len = _numChars - _inFence; if (len > 0) s = Common::U32String(&(_chars[_inFence]), len); _history[_historyPos] = s; } _historyPos--; if (_historyPos < 0) _historyPos += HISTORYLEN; s = _history[_historyPos]; putTextUni(s.c_str(), s.size(), _inFence, _numChars - _inFence); break; case keycode_Down: if (_historyPos == _historyPresent) return; _historyPos++; if (_historyPos >= HISTORYLEN) _historyPos -= HISTORYLEN; s = _history[_historyPos]; putTextUni(s.c_str(), s.size(), _inFence, _numChars - _inFence); break; // Cursor movement keys, during line input. case keycode_Left: if (_inCurs <= _inFence) return; _inCurs--; break; case keycode_Right: if (_inCurs >= _numChars) return; _inCurs++; break; case keycode_Home: if (_inCurs <= _inFence) return; _inCurs = _inFence; break; case keycode_End: if (_inCurs >= _numChars) return; _inCurs = _numChars; break; case keycode_SkipWordLeft: while (_inCurs > _inFence && _chars[_inCurs - 1] == ' ') _inCurs--; while (_inCurs > _inFence && _chars[_inCurs - 1] != ' ') _inCurs--; break; case keycode_SkipWordRight: while (_inCurs < _numChars && _chars[_inCurs] != ' ') _inCurs++; while (_inCurs < _numChars && _chars[_inCurs] == ' ') _inCurs++; break; // Delete keys, during line input. case keycode_Delete: if (_inCurs <= _inFence) return; putTextUni(nullptr, 0, _inCurs - 1, 1); break; case keycode_Erase: if (_inCurs >= _numChars) return; putTextUni(nullptr, 0, _inCurs, 1); break; case keycode_Escape: if (_inFence >= _numChars) return; putTextUni(nullptr, 0, _inFence, _numChars - _inFence); break; // Regular keys case keycode_Return: acceptLine(arg); break; default: if (arg >= 32 && arg <= 0x10FFFF) { if (_font._caps && (arg > 0x60 && arg < 0x7b)) arg -= 0x20; putTextUni(&arg, 1, _inCurs, 0); } break; } touch(0); } void TextBufferWindow::acceptLine(uint32 keycode) { int ix; int len, olen; void *inbuf; Common::U32String s, o; int inmax; gidispatch_rock_t inarrayrock; int unicode = _lineRequestUni; if (!_inBuf) return; inbuf = _inBuf; inmax = _inMax; inarrayrock = _inArrayRock; len = _numChars - _inFence; if (_echoStream) _echoStream->echoLineUni(_chars + _inFence, len); gli_tts_purge(); if (g_conf->_speakInput) { const uint32 NEWLINE = '\n'; gli_tts_speak(_chars + _inFence, len); gli_tts_speak((const uint32 *)&NEWLINE, 1); } /* * Store in history. * The history is a ring buffer, with historypresent being the index of the most recent * element and historyfirst the index of the oldest element. * A history entry should not repeat the string from the entry before it. */ if (len) { s = Common::U32String(_chars + _inFence, len); _history[_historyPresent].clear(); o = _history[(_historyPresent == 0 ? HISTORYLEN : _historyPresent) - 1]; olen = o.size(); if (len != olen || !s.equals(o)) { _history[_historyPresent] = s; _historyPresent++; if (_historyPresent == HISTORYLEN) _historyPresent = 0; if (_historyPresent == _historyFirst) { _historyFirst++; if (_historyFirst == HISTORYLEN) _historyFirst = 0; } } } // Store in event buffer. if (len > inmax) len = inmax; if (!unicode) { for (ix = 0; ix < len; ix++) { uint32 ch = _chars[_inFence + ix]; if (ch > 0xff) ch = '?'; ((char *)inbuf)[ix] = (char)ch; } } else { for (ix = 0; ix < len; ix++) ((uint *)inbuf)[ix] = _chars[_inFence + ix]; } _attr = _origAttr; if (_lineTerminators) { if (keycode == keycode_Return) keycode = 0; else // TODO: Currently particularly for Beyond Zork, we don't echo a newline // for line terminators, allowing description area scrolling to not keep adding // blank lines in the command area. In the future I may need to make it configurable // when I see if any other line terminators need to have a newline _echoLineInput = false; g_vm->_events->store(evtype_LineInput, this, len, keycode); delete[] _lineTerminators; _lineTerminators = nullptr; } else { g_vm->_events->store(evtype_LineInput, this, len, 0); } _lineRequest = false; _lineRequestUni = false; _inBuf = nullptr; _inMax = 0; if (_echoLineInput) { putCharUni('\n'); } else { _numChars = _inFence; touch(0); } if (g_vm->gli_unregister_arr) (*g_vm->gli_unregister_arr)(inbuf, inmax, unicode ? "&+#!Iu" : "&+#!Cn", inarrayrock); } bool TextBufferWindow::leftquote(uint32 c) { switch (c) { case '(': case '[': // The following are Unicode characters in the "Separator, Space" category. case 0x0020: case 0x00a0: case 0x1680: case 0x2000: case 0x2001: case 0x2002: case 0x2003: case 0x2004: case 0x2005: case 0x2006: case 0x2007: case 0x2008: case 0x2009: case 0x200a: case 0x202f: case 0x205f: case 0x3000: return true; default: return false; } } void TextBufferWindow::scrollOneLine(bool forced) { _lastSeen++; _scrollMax++; if (_scrollMax > _scrollBack - 1 || _lastSeen > _scrollBack - 1) scrollResize(); if (_lastSeen >= _height) _scrollPos++; if (_scrollPos > _scrollMax - _height + 1) _scrollPos = _scrollMax - _height + 1; if (_scrollPos < 0) _scrollPos = 0; if (forced) _dashed = 0; _spaced = 0; _lines[0]._len = _numChars; _lines[0]._newLine = forced; for (int i = _scrollBack - 1; i > 0; i--) { memcpy(&_lines[i], &_lines[i - 1], sizeof(TextBufferRow)); if (i < _height) touch(i); } if (_radjn) _radjn--; if (_radjn == 0) _radjw = 0; if (_ladjn) _ladjn--; if (_ladjn == 0) _ladjw = 0; touch(0); _lines[0]._len = 0; _lines[0]._newLine = 0; _lines[0]._lm = _ladjw; _lines[0]._rm = _radjw; _lines[0]._lPic = nullptr; _lines[0]._rPic = nullptr; _lines[0]._lHyper = 0; _lines[0]._rHyper = 0; Common::fill(_chars, _chars + TBLINELEN, ' '); Attributes *a = _attrs; for (int i = 0; i < TBLINELEN; ++i, ++a) a->clear(); _numChars = 0; touchScroll(); } void TextBufferWindow::scrollResize() { int i; _lines.clear(); _lines.resize(_scrollBack + SCROLLBACK); _chars = _lines[0]._chars; _attrs = _lines[0]._attrs; for (i = _scrollBack; i < (_scrollBack + SCROLLBACK); i++) { _lines[i]._dirty = false; _lines[i]._repaint = false; _lines[i]._lm = 0; _lines[i]._rm = 0; _lines[i]._lPic = 0; _lines[i]._rPic = 0; _lines[i]._lHyper = 0; _lines[i]._rHyper = 0; _lines[i]._len = 0; _lines[i]._newLine = 0; memset(_lines[i]._chars, ' ', sizeof _lines[i]._chars); memset(_lines[i]._attrs, 0, sizeof _lines[i]._attrs); } _scrollBack += SCROLLBACK; } int TextBufferWindow::calcWidth(const uint32 *chars, const Attributes *attrs, int startchar, int numChars, int spw) { Screen &screen = *g_vm->_screen; int w = 0; int a, b; a = startchar; for (b = startchar; b < numChars; b++) { if (attrs[a] != attrs[b]) { w += screen.stringWidthUni(attrs[a].attrFont(_styles), Common::U32String(chars + a, b - a), spw); a = b; } } w += screen.stringWidthUni(attrs[a].attrFont(_styles), Common::U32String(chars + a, b - a), spw); return w; } void TextBufferWindow::getSize(uint *width, uint *height) const { if (width) *width = (_bbox.width() - g_conf->_tMarginX * 2) / _font._cellW; if (height) *height = (_bbox.height() - g_conf->_tMarginY * 2) / _font._cellH; } void TextBufferWindow::flowBreak() { while (_ladjn || _radjn) putCharUni('\n'); } /*--------------------------------------------------------------------------*/ TextBufferWindow::TextBufferRow::TextBufferRow() : _len(0), _newLine(0), _dirty(false), _repaint(false), _lPic(nullptr), _rPic(nullptr), _lHyper(0), _rHyper(0), _lm(0), _rm(0) { Common::fill(&_chars[0], &_chars[TBLINELEN], 0); } } // End of namespace Glk