diff --git a/TODO.md b/TODO.md index 8fd8af3906..b37238ed07 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,7 @@ -# Current PR (box drawings) +# Current PR + +- [ ] maxHistoryLineCount cannot be "unlimited" anymore. -- [ ] `builtin_box_drawing: false` ignored? check! -- [ ] top-left and bottom-left arc show artifacts on very left side. why? most likely overflow in pixel painting. # RELEASE CHECKLIST diff --git a/autogen.sh b/autogen.sh index ddfaf51fd2..c224f350a5 100755 --- a/autogen.sh +++ b/autogen.sh @@ -29,5 +29,5 @@ exec cmake "${ROOTDIR}" \ -DCONTOUR_COVERAGE="OFF" \ -DCONTOUR_PERF_STATS="ON" \ -DCONTOUR_BLUR_PLATFORM_KWIN="ON" \ - -GNinja + #-GNinja diff --git a/docs/grid-redesign.md b/docs/grid-redesign.md new file mode 100644 index 0000000000..c9db1b8f58 --- /dev/null +++ b/docs/grid-redesign.md @@ -0,0 +1,89 @@ +# Grid (re)design + +## grid properties + +- line and column positions are given in offsets (0-based indices!) +- grid cells of the full scrollback + main page area are stored in dense linear space +- `Line` is just a view into the ring buffer +- logical lines above main page area's line count cannot be edited +- scroll offset represents the *bottom* line of the viewport. +- maybe make scroll offset negative (to have all ops being additions) + +## corollary + +- the VT screen line/column position to offset translation (minus 1) + happens as early as possible in the `Sequencer`. +- `Line::Wrapped`-flag can be removed. +- writing an overlong line does not need to care about auto-wrapping + - because `lineLength / pageWidth == numberOfLinesWriten` + - the new cursor position can be computed analogous to the above +- because of the meaning of the scroll-offset, reflow can be O(1) implemented + with a `Line()` being arbitrary long(!) and the top screen line being + computed by subtracting `(PageLineCount - 1) * PageColumnCount)`, + then *just* lineary walking forward until the bottom right. + - this enables relative jumps (`CUU` etc) to jump up logical lines. + - we could make the cursor jump behavior configurable, via DEC mode. +- On char write overflow + - if Reflow DEC mode is enabled, then continue appending character + - else if AutoWrap DEC mode is enabled, then linefeed() + - else overwrite character on right margin +- I think with the above approach we do not need DEC mode 2027 for disabling + reflow in order to protect command prompts. as they're automatically protected + by the grid line architecture. + +### Notes + +Suppose only a single overly long line is written and ocupies the full screen +(many page counts). + +``` +bottomRightOffset = line(cursor.line + realOffset(scrollOffset_)).right_offset; +topLeftOffset = bottomRightOffset - pageSize.lines * pageSize.columns; + +def renderViewport(): +{ + for (auto i = topLeftOffset, k = 0; i < bottomRightOffset; ++i, ++k) + { + auto rowNr = k / pageSize.columns; + auto colNr = k % pageSize.columns; + renderCell(buffer_[i], rowNr, colNr); + } +} +``` + +## shrink columns + + page size: 6x2 + lines: 2 + line.1: 0..5 abcdef + line.2: 6..11 ABCDEF + + shrink to pageSize: 4x2 + + line.1: 0..3 abcd + line.2: 4..5 ef Wrapped + line.3: 6..9 ABCD + line.4: 10..11 EF Wrapped + +Shrinking does not destroy, as it always reflows. +`CUU` will move by logical line numbers. + +## overlong lines + +4x2 + + + line.1 ABCDEFabcdef + line.2 ABCD + + view: + + history.line.-1 ABCD + history.line.0 EFAB + logical.line.1 cdef + logical.line.2 ABCD + +## Rendering a wrapped line + +- the scroll-offset represents the number of real lines scrolled up +- diff --git a/src/contour/Config.cpp b/src/contour/Config.cpp index 66ea8a72a0..d8e3d9fcfa 100644 --- a/src/contour/Config.cpp +++ b/src/contour/Config.cpp @@ -1172,10 +1172,10 @@ TerminalProfile loadTerminalProfile(UsedKeys& _usedKeys, else errorlog().write("Invalid render_mode \"{}\" in configuration.", renderModeStr); - auto intValue = profile.maxHistoryLineCount.value_or(std::numeric_limits::max()); + auto intValue = profile.maxHistoryLineCount; tryLoadChild(_usedKeys, _doc, basePath, "history.limit", intValue); - if (unbox(intValue) < 0 || intValue == std::numeric_limits::max()) - profile.maxHistoryLineCount = nullopt; + if (unbox(intValue) < 0) + profile.maxHistoryLineCount = LineCount(0); else profile.maxHistoryLineCount = intValue; diff --git a/src/contour/Config.h b/src/contour/Config.h index e4c737d548..5409bf801b 100644 --- a/src/contour/Config.h +++ b/src/contour/Config.h @@ -127,7 +127,7 @@ struct TerminalProfile { terminal::PageSize terminalSize = {terminal::LineCount(10), terminal::ColumnCount(40)}; terminal::VTType terminalId = terminal::VTType::VT525; - std::optional maxHistoryLineCount; + terminal::LineCount maxHistoryLineCount; terminal::LineCount historyScrollMultiplier; ScrollBarPosition scrollbarPosition = ScrollBarPosition::Right; bool hideScrollbarInAltScreen = true; diff --git a/src/contour/TerminalSession.cpp b/src/contour/TerminalSession.cpp index 700bba6e07..d8f8b0a7b3 100644 --- a/src/contour/TerminalSession.cpp +++ b/src/contour/TerminalSession.cpp @@ -487,10 +487,11 @@ void TerminalSession::operator()(actions::DecreaseOpacity) void TerminalSession::operator()(actions::FollowHyperlink) { +#if defined(LIBTERMINAL_HYPERLINKS) auto const _l = scoped_lock{terminal()}; auto const currentMousePosition = terminal().currentMousePosition(); auto const currentMousePositionRel = terminal::Coordinate{ - currentMousePosition.row - terminal().viewport().relativeScrollOffset().as(), + currentMousePosition.row + terminal().viewport().scrollOffset().as(), currentMousePosition.column }; if (terminal().screen().contains(currentMousePosition)) @@ -501,6 +502,7 @@ void TerminalSession::operator()(actions::FollowHyperlink) return; } } +#endif } void TerminalSession::operator()(actions::IncreaseFontSize) diff --git a/src/contour/TerminalWindow.cpp b/src/contour/TerminalWindow.cpp index f2237e39df..af4647045e 100644 --- a/src/contour/TerminalWindow.cpp +++ b/src/contour/TerminalWindow.cpp @@ -124,8 +124,8 @@ void ScrollableDisplay::updateValues() return; scrollBar_->setMaximum(session_.terminal().screen().historyLineCount().as()); - if (auto const s = session_.terminal().viewport().absoluteScrollOffset(); s.has_value()) - scrollBar_->setValue(s.value().as()); + if (auto const s = session_.terminal().viewport().scrollOffset(); s.value) + scrollBar_->setValue(s.value); else scrollBar_->setValue(scrollBar_->maximum()); } @@ -196,7 +196,7 @@ void ScrollableDisplay::updatePosition() void ScrollableDisplay::onValueChanged() { - session_.terminal().viewport().scrollToAbsolute(terminal::StaticScrollbackPosition(scrollBar_->value())); + session_.terminal().viewport().scrollTo(terminal::ScrollOffset::cast_from(scrollBar_->value())); session_.scheduleRedraw(); } #endif // }}} diff --git a/src/contour/opengl/TerminalWidget.cpp b/src/contour/opengl/TerminalWidget.cpp index 67e7e463c9..b050f1c020 100644 --- a/src/contour/opengl/TerminalWidget.cpp +++ b/src/contour/opengl/TerminalWidget.cpp @@ -602,7 +602,7 @@ void TerminalWidget::assertInitialized() void TerminalWidget::onScrollBarValueChanged(int _value) { - terminal().viewport().scrollToAbsolute(terminal::StaticScrollbackPosition::cast_from(_value)); + terminal().viewport().scrollTo(terminal::ScrollOffset::cast_from(_value)); scheduleRedraw(); } diff --git a/src/crispy/boxed.h b/src/crispy/boxed.h index 958aaf9aba..6c15560ae7 100644 --- a/src/crispy/boxed.h +++ b/src/crispy/boxed.h @@ -86,10 +86,10 @@ template struct boxed } }; -template constexpr T& operator++(boxed& a) noexcept { ++a.value; return a; } -template constexpr T& operator++(boxed& a, int) noexcept { a.value++; return a; } -template constexpr T& operator--(boxed& a) noexcept { --a.value; return a; } -template constexpr T& operator--(boxed& a, int) noexcept { a.value--; return a; } +template constexpr boxed& operator++(boxed& a) noexcept { ++a.value; return a; } +template constexpr boxed& operator++(boxed& a, int) noexcept { a.value++; return a; } +template constexpr boxed& operator--(boxed& a) noexcept { --a.value; return a; } +template constexpr boxed& operator--(boxed& a, int) noexcept { a.value--; return a; } template constexpr T const& operator*(boxed const& a) noexcept { return a.value; } template constexpr bool operator<(boxed const& a, boxed const& b) noexcept { return a.value < b.value; } template constexpr bool operator>(boxed const& a, boxed const& b) noexcept { return a.value > b.value; } @@ -102,6 +102,10 @@ template constexpr boxed operator+(boxed co template constexpr boxed operator-(boxed const& a, boxed const& b) noexcept { return boxed{a.value - b.value}; } template constexpr boxed operator*(boxed const& a, boxed const& b) noexcept { return boxed{a.value * b.value}; } template constexpr boxed operator/(boxed const& a, boxed const& b) noexcept { return boxed{a.value / b.value}; } +template constexpr boxed operator%(boxed const& a, boxed const& b) noexcept { return boxed{a.value % b.value}; } + +template constexpr boxed operator-(boxed const& a) noexcept { return boxed{-a.value}; } +template constexpr boxed operator+(boxed const& a) noexcept { return boxed{+a.value}; } template constexpr boxed operator+(boxed const& a, T b) noexcept { return boxed{a.value + b}; } template constexpr boxed operator-(boxed const& a, T b) noexcept { return boxed{a.value - b}; } @@ -112,6 +116,7 @@ template constexpr boxed& operator+=(boxed& template constexpr boxed& operator-=(boxed& a, boxed const& b) noexcept { a.value -= b.value; return a; } template constexpr boxed& operator*=(boxed& a, boxed const& b) noexcept { a.value *= b.value; return a; } template constexpr boxed& operator/=(boxed& a, boxed const& b) noexcept { a.value /= b.value; return a; } +template constexpr boxed& operator%=(boxed& a, boxed const& b) noexcept { a.value %= b.value; return a; } template std::ostream& operator<<(std::ostream& os, boxed const& v) { return os << v.value; } diff --git a/src/terminal/CMakeLists.txt b/src/terminal/CMakeLists.txt index 2e8866eb0d..36736739fa 100644 --- a/src/terminal/CMakeLists.txt +++ b/src/terminal/CMakeLists.txt @@ -61,6 +61,7 @@ set(terminal_SOURCES Capabilities.cpp Color.cpp Grid.cpp + Screen.cpp # TODO(pr) move back. only moved here for test-compiles Functions.cpp Image.cpp InputBinding.cpp @@ -69,7 +70,7 @@ set(terminal_SOURCES Parser.cpp Process.cpp RenderBuffer.cpp - Screen.cpp + #Screen.cpp Sequence.cpp Sequencer.cpp Selector.cpp diff --git a/src/terminal/Charset.h b/src/terminal/Charset.h index d89b2eb1f1..6777a129d7 100644 --- a/src/terminal/Charset.h +++ b/src/terminal/Charset.h @@ -39,11 +39,23 @@ class CharsetMapping { } {} - char32_t map(char _code) noexcept + char32_t map(char32_t _code) noexcept { - auto result = map(shift_, _code); - shift_ = selected_; - return result; + // TODO: could surely be implemented branchless with a jump-table and computed goto. + if (_code < 127) + { + auto result = map(shift_, static_cast(_code)); + shift_ = selected_; + return result; + } + else if (_code != 127) + { + return static_cast(_code); + } + else + { + return L' '; + } } char32_t map(CharsetTable _table, char _code) const noexcept diff --git a/src/terminal/Coordinate.h b/src/terminal/Coordinate.h index 722611c8fe..8a954398c4 100644 --- a/src/terminal/Coordinate.h +++ b/src/terminal/Coordinate.h @@ -13,30 +13,28 @@ */ #pragma once -#include +#include +#include namespace terminal { struct [[nodiscard]] Coordinate { - int row = 0; - int column = 0; + LineOffset row{}; + ColumnOffset column{}; - constexpr Coordinate& operator+=(Coordinate const& a) noexcept + constexpr Coordinate& operator+=(Coordinate a) noexcept { row += a.row; column += a.column; return *this; } - - constexpr Coordinate(int _row, int _column) : row{_row}, column{_column} {} - constexpr explicit Coordinate(crispy::Point p) : row{p.y}, column{p.x} {} - constexpr Coordinate() = default; - constexpr Coordinate(Coordinate const&) = default; - constexpr Coordinate(Coordinate&& v) = default; - constexpr Coordinate& operator=(Coordinate const&) = default; - constexpr Coordinate& operator=(Coordinate &&) = default; }; +inline std::ostream& operator<<(std::ostream& os, Coordinate coord) +{ + return os << fmt::format("({}, {})", coord.row, coord.column); +} + constexpr void swap(Coordinate& a, Coordinate& b) noexcept { auto c = a; @@ -44,10 +42,10 @@ constexpr void swap(Coordinate& a, Coordinate& b) noexcept b = c; } -constexpr bool operator==(Coordinate const& a, Coordinate const& b) noexcept { return a.row == b.row && a.column == b.column; } -constexpr bool operator!=(Coordinate const& a, Coordinate const& b) noexcept { return !(a == b); } +constexpr bool operator==(Coordinate a, Coordinate b) noexcept { return a.row == b.row && a.column == b.column; } +constexpr bool operator!=(Coordinate a, Coordinate b) noexcept { return !(a == b); } -constexpr bool operator<(Coordinate const& a, Coordinate const& b) noexcept +constexpr bool operator<(Coordinate a, Coordinate b) noexcept { if (a.row < b.row) return true; @@ -58,27 +56,22 @@ constexpr bool operator<(Coordinate const& a, Coordinate const& b) noexcept return false; } -constexpr bool operator<=(Coordinate const& a, Coordinate const& b) noexcept +constexpr bool operator<=(Coordinate a, Coordinate b) noexcept { return a < b || a == b; } -constexpr bool operator>=(Coordinate const& a, Coordinate const& b) noexcept +constexpr bool operator>=(Coordinate a, Coordinate b) noexcept { return !(a < b); } -constexpr bool operator>(Coordinate const& a, Coordinate const& b) noexcept +constexpr bool operator>(Coordinate a, Coordinate b) noexcept { return !(a == b || a < b); } -inline Coordinate operator+(Coordinate const& a, Coordinate const& b) noexcept { return {a.row + b.row, a.column + b.column}; } - -constexpr Coordinate operator+(Coordinate const& a, crispy::Point b) noexcept -{ - return {a.row + b.y, a.column + b.x}; -} +inline Coordinate operator+(Coordinate a, Coordinate b) noexcept { return {a.row + b.row, a.column + b.column}; } } @@ -97,3 +90,4 @@ namespace fmt { } }; } + diff --git a/src/terminal/Grid.cpp b/src/terminal/Grid.cpp index c33e7ee8ae..b73e0f95ba 100644 --- a/src/terminal/Grid.cpp +++ b/src/terminal/Grid.cpp @@ -19,6 +19,8 @@ #include +#include + #include #include #include @@ -28,8 +30,12 @@ using crispy::Comparison; +using gsl::span; + using std::back_inserter; +using std::copy_n; using std::fill_n; +using std::fill; using std::for_each; using std::front_inserter; using std::generate_n; @@ -42,6 +48,7 @@ using std::prev; using std::reverse; using std::rotate; using std::string; +using std::string_view; using std::tuple; #if defined(LIBTERMINAL_EXECUTION_PAR) @@ -71,6 +78,40 @@ namespace // {{{ helper std::cout << fmt::format(std::forward(_args)...) << '\n'; #endif } + + GridBuffer createBuffer(PageSize _size, LineCount _historyLineCount) + { + auto const size = unbox(_size.lines + _historyLineCount) * unbox(_size.columns); + return GridBuffer(size, Cell{}); + } + + Lines createLines(GridBuffer& _buffer, + PageSize _screenSize, + LineCount _maxHistoryLineCount, + bool _reflowOnResize, + GraphicsAttributes _initialSGR) + { + auto const defaultLineFlags = _reflowOnResize + ? Line::Flags::Wrappable + : Line::Flags::None; + + auto const lineCount = (_screenSize.lines + _maxHistoryLineCount).as(); + + Lines lines; + lines.reserve(lineCount); + + for (size_t i = 0; i < lineCount; ++i) + { + lines.emplace_back(Line{ + _screenSize.columns.as() * i, + _screenSize.columns, + defaultLineFlags, + _initialSGR + }); + } + + return lines; + } } // }}} // {{{ Cell impl @@ -82,37 +123,36 @@ string Cell::toUtf8() const return " "; } // }}} -// {{{ Line impl -Line::Line(Buffer&& _init, Flags _flags) : - buffer_{ move(_init) }, - flags_{ static_cast(_flags) } +// {{{ Grid impl +Grid::Grid(PageSize _screenSize, bool _reflowOnResize, LineCount _maxHistoryLineCount) : + screenSize_{ _screenSize }, + reflowOnResize_{ _reflowOnResize }, + maxHistoryLineCount_{ _maxHistoryLineCount }, + buffer_{createBuffer(_screenSize, _maxHistoryLineCount)}, + lines_{createLines(buffer_, _screenSize, _maxHistoryLineCount, _reflowOnResize, {})}, + mainPageTopLineIndex_{0}, + linesUsed_{_screenSize.lines} { } -Line::Line(iterator const& _begin, iterator const& _end, Flags _flags) : - buffer_(_begin, _end), - flags_{ static_cast(_flags) } +// {{{ Grid: Line access +span Grid::lineBufferRightTrimmed(LineOffset _line) const noexcept { -} + Line const& line = lines_[realLineOffset(_line).as()]; -Line::Line(ColumnCount _numCols, Buffer&& _init, Flags _flags) : - buffer_{move(_init)}, - flags_{ static_cast(_flags) } -{ - buffer_.resize(unbox(_numCols)); -} + Cell const* start = buffer_.data() + line.begin_offset(); + Cell const* end = start + line.size().as(); -Line::Line(ColumnCount _numCols, std::string_view const& _s, Flags _flags) : - Line(_numCols, Cell{}, _flags) -{ - for (auto const [i, ch] : crispy::indexed(_s)) - buffer_.at(i).setCharacter(static_cast(ch)); + while (start != end && is_blank(*std::prev(end))) + --end; + + return gsl::make_span(start, end); } -string Line::toUtf8() const +string Grid::lineText(LineOffset _line) const { std::stringstream sstr; - for (Cell const& cell : crispy::range(begin(), next(begin(), unbox(size())))) + for (Cell const& cell: lineBuffer(_line)) { if (cell.codepointCount() == 0) { @@ -127,140 +167,64 @@ string Line::toUtf8() const return sstr.str(); } -string Line::toUtf8Trimmed() const +string Grid::lineTextTrimmed(LineOffset _line) const { - string output = toUtf8(); + + string output = lineText(_line); while (!output.empty() && isspace(output.back())) output.pop_back(); return output; } -void Line::prepend(Buffer const& _cells) +void Grid::setLineText(LineOffset _line, string_view _text) { - buffer_.insert(buffer_.begin(), _cells.begin(), _cells.end()); -} + auto line = lineBuffer(_line); -void Line::append(Buffer const& _cells) -{ - buffer_.insert(buffer_.end(), _cells.begin(), _cells.end()); -} - -void Line::append(int _count, Cell const& _initial) -{ - fill_n(back_inserter(buffer_), _count, _initial); -} - -crispy::range Line::trim_blank_right() const -{ - auto i = buffer_.cbegin(); - auto e = buffer_.cend(); - - while (i != e && is_blank(*prev(e))) - e = prev(e); - - return crispy::range(i, e); -} - -Line::Buffer Line::shift_left(int _count, Cell const& _fill) -{ - auto const actualShiftCount = min(_count, unbox(size())); - auto const from = std::begin(buffer_); - auto const to = std::next(std::begin(buffer_), actualShiftCount); - - auto out = remove(from, to); - append(actualShiftCount, _fill); - - // trim trailing blanks - while (!out.empty() && is_blank(out.back())) - out.pop_back(); - - return out; -} - -Line::Buffer Line::remove(iterator const& _from, iterator const& _to) -{ - auto removedColumns = Buffer(_from, _to); - buffer_.erase(_from, _to); - return removedColumns; -} - -void Line::setText(std::string_view _u8string) -{ - for (auto const [i, ch] : crispy::indexed(unicode::convert_to(_u8string))) - buffer_.at(i).setCharacter(ch); + size_t i = 0; + for (auto const ch: unicode::convert_to(_text)) + { + line[i++].setCharacter(ch); + } } -void Line::resize(ColumnCount _size) +bool Grid::isLineBlank(LineOffset _line) const noexcept { - assert(*_size >= 0); - buffer_.resize(unbox(_size)); + auto line = lineBuffer(_line); + return std::all_of(line.begin(), line.end(), is_blank); } -bool Line::blank() const noexcept +// TODO: rename to include word Logical +/** + * Computes the relative line number for the bottom-most @p _n logical lines. + */ +int Grid::computeLogicalLineNumberFromBottom(LineCount _n) const noexcept { - return std::all_of(cbegin(), cend(), is_blank); -} + int logicalLineCount = 0; + auto outputRelativePhysicalLine = *screenSize_.lines; -Line::Buffer Line::reflow(ColumnCount _newColumnCount) -{ - switch (crispy::strongCompare(_newColumnCount, size())) + auto i = lines_.rbegin(); + while (i != lines_.rend()) { - case Comparison::Equal: - break; - case Comparison::Greater: - buffer_.resize(unbox(_newColumnCount)); + if (!i->wrapped()) + logicalLineCount++; + outputRelativePhysicalLine--; + ++i; + if (logicalLineCount == *_n) break; - case Comparison::Less: - { - // TODO: properly handle wide character cells - // - when cutting in the middle of a wide char, the wide char gets wrapped and an empty - // cell needs to be injected to match the expected column width. - - if (wrappable()) - { - auto const [reflowStart, reflowEnd] = [this, _newColumnCount]() - { - auto const reflowStart = next(buffer_.begin(), *_newColumnCount /* - buffer_[_newColumnCount].width()*/); - auto reflowEnd = buffer_.end(); - - while (reflowEnd != reflowStart && is_blank(*prev(reflowEnd))) - reflowEnd = prev(reflowEnd); - - return tuple{reflowStart, reflowEnd}; - }(); + } - auto removedColumns = Buffer(reflowStart, reflowEnd); - buffer_.erase(reflowStart, buffer_.end()); - assert(size() == _newColumnCount); - return removedColumns; - } - else - { - auto const reflowStart = next(buffer_.cbegin(), *_newColumnCount); - buffer_.erase(reflowStart, buffer_.end()); - assert(size() == _newColumnCount); - return {}; - } - } + // XXX If the top-most logical line is reached, we still need to traverse upwards until the + // beginning of the top-most logical line (the one that does not have the wrapped-flag set). + while (i != lines_.rend() && i->wrapped()) + { + printf("further upwards: l %d, p %d\n", logicalLineCount, outputRelativePhysicalLine); + outputRelativePhysicalLine--; + ++i; } - return {}; + + return outputRelativePhysicalLine; } // }}} -// {{{ Grid impl -Grid::Grid(PageSize _screenSize, bool _reflowOnResize, optional _maxHistoryLineCount) : - screenSize_{ _screenSize }, - reflowOnResize_{ _reflowOnResize }, - maxHistoryLineCount_{ _maxHistoryLineCount }, - lines_( - unbox(_screenSize.lines), - Line( - _screenSize.columns, - Cell{}, - _reflowOnResize ? Line::Flags::Wrappable : Line::Flags::None - ) - ) -{ -} /** * Appends logical line by splitting into fixed-width lines. @@ -271,6 +235,7 @@ Grid::Grid(PageSize _screenSize, bool _reflowOnResize, optional _maxH * @param _baseFlags * @param _initialNoWrap */ +/* XXX TODO(pr) void addNewWrappedLines(Lines& _targetLines, ColumnCount _newColumnCount, Line::Buffer&& _logicalLineBuffer, // TODO: don't move, do (c)ref instead @@ -298,46 +263,37 @@ void addNewWrappedLines(Lines& _targetLines, logf(" - add line: '{}' ({})", _targetLines.back().toUtf8(), _targetLines.back().flags()); } } +*/ -void Grid::setMaxHistoryLineCount(optional _maxHistoryLineCount) +void Grid::setMaxHistoryLineCount(LineCount _maxHistoryLineCount) { maxHistoryLineCount_ = _maxHistoryLineCount; clampHistory(); } -// TODO: rename to include word Logical -/** - * Computes the relative line number for the bottom-most @p _n logical lines. - */ -int Grid::computeRelativeLineNumberFromBottom(int _n) const noexcept +// {{{ resize +Coordinate Grid::resize(PageSize _newSize, Coordinate _currentCursorPos) { - int logicalLineCount = 0; - auto outputRelativePhysicalLine = unbox(screenSize_.lines); + std::cout << fmt::format("Resize {} -> {}\n", screenSize_, _newSize); - auto i = lines_.rbegin(); - while (i != lines_.rend()) - { - if (!i->wrapped()) - logicalLineCount++; - outputRelativePhysicalLine--; - ++i; - if (logicalLineCount == _n) - break; - } + if (_newSize == screenSize_) + return _currentCursorPos; - // XXX If the top-most logical line is reached, we still need to traverse upwards until the - // beginning of the top-most logical line (the one that does not have the wrapped-flag set). - while (i != lines_.rend() && i->wrapped()) - { - printf("further upwards: l %d, p %d\n", logicalLineCount, outputRelativePhysicalLine); - outputRelativePhysicalLine--; - ++i; - } + auto newBuffer = createBuffer(_newSize, maxHistoryLineCount_); + auto newLines = createLines(newBuffer, _newSize, maxHistoryLineCount_, reflowOnResize_, {}); - return outputRelativePhysicalLine; + buffer_ = newBuffer; + lines_ = newLines; + + // TODO + // - when only shrinking height, do not recreate buffer or lines + // - when only growing height, see if we can realloc() buffer_ at least + + return _currentCursorPos; } -Coordinate Grid::resize(PageSize _newSize, Coordinate _currentCursorPos, bool _wrapPending) +/* XXX +Coordinate Grid::resize(PageSize _newSize, Coordinate _currentCursorPos) { auto const growLines = [this](LineCount _newHeight) -> Coordinate { @@ -384,7 +340,7 @@ Coordinate Grid::resize(PageSize _newSize, Coordinate _currentCursorPos, bool _w } }; - auto const growColumns = [this, _wrapPending](ColumnCount _newColumnCount, Coordinate _cursor) -> Coordinate + auto const growColumns = [this](ColumnCount _newColumnCount, Coordinate _cursor) -> Coordinate { if (!reflowOnResize_) { @@ -392,7 +348,7 @@ Coordinate Grid::resize(PageSize _newSize, Coordinate _currentCursorPos, bool _w if (line.size() < _newColumnCount) line.resize(_newColumnCount); screenSize_.columns = _newColumnCount; - return _cursor + Coordinate{0, _wrapPending ? 1 : 0}; + return _cursor; } else { @@ -447,7 +403,7 @@ Coordinate Grid::resize(PageSize _newSize, Coordinate _currentCursorPos, bool _w lines_ = move(grownLines); screenSize_.columns = _newColumnCount; - //auto diff = int(lines_.size()) - unbox(screenSize_.lines); + //auto diff = unbox(linesUsed_) - unbox(screenSize_.lines); auto cy = 0; if (*historyLineCount() < 0) { @@ -455,7 +411,7 @@ Coordinate Grid::resize(PageSize _newSize, Coordinate _currentCursorPos, bool _w appendNewLines(LineCount(-*historyLineCount()), lines_.back()->back().attributes()); } - return _cursor + Coordinate{cy, _wrapPending ? 1 : 0}; + return _cursor + Coordinate{cy, {}}; } }; @@ -579,277 +535,195 @@ Coordinate Grid::resize(PageSize _newSize, Coordinate _currentCursorPos, bool _w return cursorPosition; } +*/ +// }}} void Grid::appendNewLines(LineCount _count, GraphicsAttributes _attr) { - auto const wrappableFlag = lines_.back().wrappableFlag(); + auto const wrappable = lines_[*linesUsed_].wrappableFlag() == Line::Flags::Wrappable; - if (historyLineCount() == maxHistoryLineCount().value_or(std::numeric_limits::max())) + for (auto i = LineOffset(0); i < _count.as(); ++i) { - // We've reached to history line count limit already. - // Rotate lines that would fall off down to the bottom again in a clean state. - // We do save quite some overhead due to avoiding unnecessary memory allocations. - for (int i = 0; i < unbox(_count); ++i) - { - Line line = move(lines_.front()); - lines_.pop_front(); - line.reset(_attr); - lines_.emplace_back(move(line)); - } - return; + auto const bufferOffset = realLineOffset(screenSize_.lines.as() + i); + Line& line = lines_[bufferOffset.as()]; + line.reset(_attr); + line.setWrappable(wrappable); } - if (auto const n = min(_count, screenSize_.lines); *n > 0) - { - generate_n( - back_inserter(lines_), - *n, - [&]() { return Line(screenSize_.columns, Cell{{}, _attr}, wrappableFlag); } - ); - clampHistory(); - } + mainPageTopLineIndex_ += _count.as(); } void Grid::clearHistory() { - if (*historyLineCount()) - lines_.erase(begin(lines_), next(begin(lines_), *historyLineCount())); + linesUsed_ = screenSize_.lines; } void Grid::clampHistory() { - if (!maxHistoryLineCount_.has_value()) - return; - - auto const actual = historyLineCount(); - auto const maxHistoryLines = maxHistoryLineCount_.value(); - if (actual < maxHistoryLines) - return; - - auto const diff = actual - maxHistoryLines; - - // any line that moves into history is using the default Wrappable flag. - for (auto& line: lines(boxed_cast(historyLineCount() - diff), - boxed_cast(historyLineCount()))) - { - auto const wrappable = true; - // std::cout << fmt::format( - // "clampHistory: wrappable={}: \"{}\"\n", - // wrappable ? "true" : "false", - // line.toUtf8() - // ); - line.setFlag(Line::Flags::Wrappable, wrappable); - } + linesUsed_ = screenSize_.lines; +} - lines_.erase(begin(lines_), next(begin(lines_), unbox(diff))); +void Grid::scrollUpUninitialized(LineCount _n) +{ + mainPageTopLineIndex_ += _n.as(); + mainPageTopLineIndex_ %= LineOffset::cast_from(lines_.size()); + linesUsed_ = min(linesUsed_ + _n, LineCount::cast_from(lines_.size())); } void Grid::scrollUp(LineCount _n, GraphicsAttributes const& _defaultAttributes, Margin const& _margin) { - if (_margin.horizontal != Margin::Range{1, unbox(screenSize_.columns)}) - { - // a full "inside" scroll-up - auto const marginHeight = LineCount::cast_from(_margin.vertical.length()); - auto const n = min(_n, marginHeight); - - if (n < marginHeight) - { - auto targetLine = next(begin(mainPage()), _margin.vertical.from - 1); // target line - auto sourceLine = next(begin(mainPage()), _margin.vertical.from - 1 + unbox(n)); // source line - auto const bottomLine = next(begin(mainPage()), _margin.vertical.to); // bottom margin's end-line iterator - - for (; sourceLine != bottomLine; ++sourceLine, ++targetLine) - { - copy_n( - next(begin(*sourceLine), _margin.horizontal.from - 1), - _margin.horizontal.length(), - next(begin(*targetLine), _margin.horizontal.from - 1) - ); - } - } + // these two booleans could be cached and updated whenever _margin updates, + // so not even this needs to be computed for the general case. + auto const fullHorizontal = _margin.horizontal == Margin::Horizontal{ColumnOffset{0}, screenSize_.columns.as() - ColumnOffset(1)}; + auto const fullVertical = _margin.vertical == Margin::Vertical{LineOffset(0), screenSize_.lines.as() - LineOffset(1)}; - // clear bottom n lines in margin. - auto const topLine = next(begin(mainPage()), _margin.vertical.to - *n); - auto const bottomLine = next(begin(mainPage()), _margin.vertical.to); // bottom margin's end-line iterator -#if 1 - for (Line& line : crispy::range(topLine, bottomLine)) - { - fill_n( - next(begin(line), _margin.horizontal.from - 1), - _margin.horizontal.length(), - Cell{{}, _defaultAttributes} - ); - } -#else - for_each( - topLine, - bottomLine, - [&](Line& line) { - fill_n( - next(begin(line), _margin.horizontal.from - 1), - _margin.horizontal.length(), - Cell{{}, _defaultAttributes} - ); - } - ); -#endif - } - else if (_margin.vertical == Margin::Range{1, unbox(screenSize_.lines)}) + if (fullHorizontal && fullVertical) { + // Margin spans full page. if (auto const n = min(_n, screenSize_.lines); *n > 0) { - appendNewLines(n, _defaultAttributes); + auto const wrappable = lines_[*linesUsed_].wrappableFlag() == Line::Flags::Wrappable; + auto const baseLineOffset = mainPageTopLineIndex_ + + screenSize_.lines.as(); + + for (auto const i: ranges::views::iota(0u, n.as())) + resetLine(baseLineOffset + LineOffset::cast_from(i), _defaultAttributes); + + scrollUpUninitialized(n); } + return; } - else + + if (fullHorizontal) { // scroll up only inside vertical margin with full horizontal extend auto const marginHeight = LineCount(_margin.vertical.length()); auto const n = min(_n, marginHeight); if (n < marginHeight) { - rotate( - next(begin(mainPage()), _margin.vertical.from - 1), - next(begin(mainPage()), _margin.vertical.from - 1 + *n), - next(begin(mainPage()), _margin.vertical.to) - ); + auto a = iteratorAt(_margin.vertical.from, _margin.horizontal.from); + auto b = iteratorAt(_margin.vertical.from + n.as(), _margin.horizontal.from); + auto c = iteratorAt(_margin.vertical.to, _margin.horizontal.to + ColumnOffset(1)); + rotate(a, b, c); } + // TODO(pr): Also rotate line attributes up. What do other TEs do? - for_each( - LIBTERMINAL_EXECUTION_COMMA(par) - next(begin(mainPage()), _margin.vertical.to - *n), - next(begin(mainPage()), _margin.vertical.to), - [&](Line& line) { - fill(begin(line), end(line), Cell{{}, _defaultAttributes}); + for (auto i = n; *i; --i) + { + auto a = iteratorAt(_margin.vertical.to - i.as() + LineOffset(1), _margin.horizontal.from); + auto b = iteratorAt(_margin.vertical.to - i.as() + LineOffset(1), _margin.horizontal.to + ColumnOffset(1)); + while (a != b) + (*a++).reset(_defaultAttributes); + } + } + else + { + // a full "inside" scroll-up + auto const marginHeight = _margin.vertical.length(); + auto const n = min(_n, marginHeight); + + if (n <= marginHeight) + { + for (LineOffset line = _margin.vertical.from; line <= _margin.vertical.from + *n; ++line) + { + auto t = iteratorAt(line, _margin.horizontal.from); + auto s = iteratorAt(line + *n, _margin.horizontal.from); + copy_n(s, _margin.horizontal.length().as(), t); } - ); + for (LineOffset line = _margin.vertical.from + *n + 1; line <= _margin.vertical.to; ++line) + { + auto a = iteratorAt(line, _margin.horizontal.from); + auto b = iteratorAt(line, _margin.horizontal.to + 1); + while (a != b) + (*a++).reset(_defaultAttributes); + } + } } } void Grid::scrollDown(LineCount v_n, GraphicsAttributes const& _defaultAttributes, Margin const& _margin) { - auto const marginHeight = LineCount(_margin.vertical.length()); - auto const n = min(v_n, marginHeight); + Expects(v_n >= LineCount(0)); + + // these two booleans could be cached and updated whenever _margin updates, + // so not even this needs to be computed for the general case. + auto const fullHorizontal = _margin.horizontal == Margin::Horizontal{ColumnOffset{0}, screenSize_.columns.as() - ColumnOffset(1)}; + auto const fullVertical = _margin.vertical == Margin::Vertical{LineOffset(0), screenSize_.lines.as() - LineOffset(1)}; + + auto const n = min(v_n, _margin.vertical.length()); - if (_margin.horizontal != Margin::Range{1, *screenSize_.columns}) + if (fullHorizontal && fullVertical) { - // full "inside" scroll-down - if (n < marginHeight) - { - auto sourceLine = next(begin(mainPage()), _margin.vertical.to - *n - 1); - auto targetLine = next(begin(mainPage()), _margin.vertical.to - 1); - auto const sourceEndLine = next(begin(mainPage()), _margin.vertical.from - 1); + // full screen scrolling - while (sourceLine != sourceEndLine) - { - copy_n( - next(begin(*sourceLine), _margin.horizontal.from - 1), - _margin.horizontal.length(), - next(begin(*targetLine), _margin.horizontal.from - 1) - ); - --targetLine; - --sourceLine; - } + // on the full screen (all lines) + // move all lines up by N lines + // bottom N lines are wiped out - copy_n( - next(begin(*sourceLine), _margin.horizontal.from - 1), - _margin.horizontal.length(), - next(begin(*targetLine), _margin.horizontal.from - 1) - ); - - for_each( - next(begin(mainPage()), _margin.vertical.from - 1), - next(begin(mainPage()), _margin.vertical.from - 1 + *n), - [&](Line& line) { - fill_n( - next(begin(line), _margin.horizontal.from - 1), - _margin.horizontal.length(), - Cell{{}, _defaultAttributes} - ); - } - ); - } - else - { - // clear everything in margin - for_each( - next(begin(mainPage()), _margin.vertical.from - 1), - next(begin(mainPage()), _margin.vertical.to), - [&](Line& line) { - fill_n( - next(begin(line), _margin.horizontal.from - 1), - _margin.horizontal.length(), - Cell{{}, _defaultAttributes} - ); - } - ); - } + auto S = mainPage(); + auto mid = n.as() * screenSize_.columns.as(); + rotate(S.rbegin(), + S.rbegin() + mid, + S.rend()); + + for (Cell& cell: span(&*S.begin(), &*S.begin() + mid)) + cell.reset(_defaultAttributes); + return; } - else if (_margin.vertical == Margin::Range{1, *screenSize_.lines}) + + if (fullHorizontal) // => but ont fully vertical { - rotate( - begin(mainPage()), - next(begin(mainPage()), *marginHeight - *n), - end(mainPage()) + // scroll down only inside vertical margin with full horizontal extend + auto const width = screenSize_.columns.as(); + auto S = mainPage().subspan( + _margin.vertical.from.as() * width, + _margin.vertical.to.as() * width ); + auto mid = n.as() * screenSize_.columns.as(); - for_each( - begin(mainPage()), - next(begin(mainPage()), *n), - [&](Line& line) { - fill( - begin(line), - end(line), - Cell{{}, _defaultAttributes} - ); - } - ); + rotate(S.rbegin(), + S.rbegin() + mid, + S.rend()); + + for (Cell& cell: span(&*S.begin(), &*S.begin() + mid)) + cell.reset(_defaultAttributes); } else { - // scroll down only inside vertical margin with full horizontal extend - rotate( - next(begin(mainPage()), _margin.vertical.from - 1), - next(begin(mainPage()), _margin.vertical.to - *n), - next(begin(mainPage()), _margin.vertical.to) - ); + // a full "inside" scroll-down + if (n <= _margin.vertical.length()) + { + auto const gapLineCount = _margin.vertical.length() - n; - for_each( - next(begin(mainPage()), _margin.vertical.from - 1), - next(begin(mainPage()), _margin.vertical.from - 1 + *n), - [&](Line& line) { - fill( - begin(line), - end(line), - Cell{{}, _defaultAttributes} - ); + for (LineOffset line = _margin.vertical.to; + line >= _margin.vertical.to - *n; --line) + { + auto s = iteratorAt(line - *n, _margin.horizontal.from); + auto t = iteratorAt(line, _margin.horizontal.from); + copy_n(s, _margin.horizontal.length().as(), t); } - ); + + for (LineOffset line = _margin.vertical.from; line < _margin.vertical.from + *n; ++line) + { + auto a = iteratorAt(line, _margin.horizontal.from); + auto b = iteratorAt(line, _margin.horizontal.to + 1); + while (a != b) + *a++ = Cell{_defaultAttributes}; + } + } } } -string Grid::renderTextLineAbsolute(int row) const +string Grid::renderTextLine(LineOffset _row) const { string line; - line.reserve(*screenSize_.columns); - for (int col = 1; col <= *screenSize_.columns; ++col) - if (auto const& cell = at({row - unbox(historyLineCount()) + 1, col}); cell.codepointCount()) - line += cell.toUtf8(); - else - line += " "; // fill character - - return line; -} + line.reserve(screenSize_.columns.as()); -string Grid::renderTextLine(int row) const -{ - string line; - line.reserve(*screenSize_.columns); - for (int col = 1; col <= *screenSize_.columns; ++col) - if (auto const& cell = at({row, col}); cell.codepointCount()) + for (Cell const& cell: lineBuffer(_row)) + if (cell.codepointCount()) line += cell.toUtf8(); else - line += " "; // fill character + line += ' '; // fill character return line; } @@ -862,23 +736,40 @@ string Grid::renderAllText() const (unbox(screenSize_.columns) + 1) ); - for (int lineNr = 0; lineNr < unbox(historyLineCount()) + *screenSize_.lines; ++lineNr) + int columnIndex = 0; + for (auto line = LineOffset(0); line < linesUsed_.as(); ++line) { - text += renderTextLineAbsolute(lineNr); + auto lineStart = lines_[realLineOffset(line).as()].begin_offset(); + auto const lineEnd = lineStart + lineLength(line).as(); + + while (lineStart < lineEnd) + { + if (++columnIndex == screenSize_.columns.as()) + { + text += '\n'; + columnIndex = 0; + } + + Cell const& cell = buffer_[lineStart++]; + if (cell.codepointCount() == 0) + text += cell.toUtf8(); + else + text += ' '; + } text += '\n'; } return text; } -string Grid::renderText() const +string Grid::renderMainPageText() const { string text; text.reserve(*screenSize_.lines * (*screenSize_.columns + 1)); - for (int lineNr = 1; lineNr <= *screenSize_.lines; ++lineNr) + for (auto line = LineOffset(0); line < screenSize_.lines.as(); ++line) { - text += renderTextLine(lineNr); + text += renderTextLine(line); text += '\n'; } diff --git a/src/terminal/Grid.h b/src/terminal/Grid.h index bc2b0572ad..0833e96e36 100644 --- a/src/terminal/Grid.h +++ b/src/terminal/Grid.h @@ -35,6 +35,10 @@ #include +#include +#include +#include + #include #include #include @@ -53,20 +57,36 @@ namespace terminal { // {{{ Margin -struct Margin { - struct Range { - int from; - int to; +struct Margin +{ + struct Horizontal + { + ColumnOffset from; + ColumnOffset to; // one past last - constexpr int length() const noexcept { return to - from + 1; } - constexpr bool operator==(Range const& rhs) const noexcept { return from == rhs.from && to == rhs.to; } - constexpr bool operator!=(Range const& rhs) const noexcept { return !(*this == rhs); } + constexpr ColumnCount length() const noexcept { + return (to - from).as() + ColumnCount(1); + } + constexpr bool operator==(Horizontal rhs) const noexcept { return from == rhs.from && to == rhs.to; } + constexpr bool operator!=(Horizontal rhs) const noexcept { return !(*this == rhs); } - constexpr bool contains(int _value) const noexcept { return from <= _value && _value <= to; } + constexpr bool contains(ColumnOffset _value) const noexcept { return from <= _value && _value < to; } }; - Range vertical{}; // top-bottom - Range horizontal{}; // left-right + struct Vertical + { + LineOffset from; + LineOffset to; + + constexpr LineCount length() const noexcept { return (to - from).as() + LineCount(1); } + constexpr bool operator==(Vertical const& rhs) const noexcept { return from == rhs.from && to == rhs.to; } + constexpr bool operator!=(Vertical const& rhs) const noexcept { return !(*this == rhs); } + + constexpr bool contains(LineOffset _value) const noexcept { return from <= _value && _value < to; } + }; + + Vertical vertical{}; // top-bottom + Horizontal horizontal{}; // left-right }; // }}} @@ -201,12 +221,16 @@ constexpr bool operator!=(GraphicsAttributes const& a, GraphicsAttributes const& // }}} // {{{ Cell + +#define CONTOUR_TERMINAL_CELL_USE_STRING 1 + /// Grid cell with character and graphics rendition information. -class Cell { - public: +class Cell +{ +public: static size_t constexpr MaxCodepoints = 9; - Cell(char32_t _codepoint, GraphicsAttributes _attrib) noexcept : + explicit Cell(GraphicsAttributes _attrib) noexcept : #if defined(CONTOUR_TERMINAL_CELL_USE_STRING) codepoints_{}, #else @@ -215,6 +239,11 @@ class Cell { #endif width_{1}, attributes_{_attrib} + { + } + + Cell(GraphicsAttributes _attrib, char32_t _codepoint) noexcept: + Cell(_attrib) { // setCharacter(_codepoint); if (_codepoint) @@ -229,6 +258,14 @@ class Cell { } } +#if defined(LIBTERMINAL_HYPERLINKS) + Cell(GraphicsAttributes _attrib, HyperlinkRef _hyperlink) noexcept: + Cell{_attrib} + { + hyperlink_ = std::move(_hyperlink); + } +#endif + Cell() noexcept : #if defined(CONTOUR_TERMINAL_CELL_USE_STRING) codepoints_{}, @@ -242,35 +279,18 @@ class Cell { void reset(GraphicsAttributes _attributes = {}) noexcept { - attributes_ = _attributes; - width_ = 1; -#if defined(LIBTERMINAL_HYPERLINKS) - hyperlink_ = nullptr; -#endif -#if defined(CONTOUR_TERMINAL_CELL_USE_STRING) - codepoints_.clear(); -#else - codepointCount_ = 0; -#endif -#if defined(LIBTERMINAL_IMAGES) - imageFragment_.reset(); -#endif + *this = Cell{_attributes}; + } + + void reset(GraphicsAttributes _attributes, char32_t _ch) noexcept + { + *this = Cell{_attributes, _ch}; } #if defined(LIBTERMINAL_HYPERLINKS) - void reset(GraphicsAttributes _attribs, HyperlinkRef const& _hyperlink) noexcept + void reset(GraphicsAttributes _attribs, HyperlinkRef _hyperlink) noexcept { - attributes_ = _attribs; - width_ = 1; -#if defined(CONTOUR_TERMINAL_CELL_USE_STRING) - codepoints_.clear(); -#else - codepointCount_ = 0; -#endif - hyperlink_ = _hyperlink; -#if defined(LIBTERMINAL_IMAGES) - imageFragment_.reset(); -#endif + *this = Cell{_attribs, std::move(_hyperlink)}; } #endif @@ -372,9 +392,8 @@ class Cell { int appendCharacter(char32_t _codepoint) noexcept { -#if defined(LIBTERMINAL_IMAGES) - imageFragment_.reset(); -#endif + assert(codepointCount() != 0); + if (codepointCount() < MaxCodepoints) { #if defined(CONTOUR_TERMINAL_CELL_USE_STRING) @@ -383,7 +402,7 @@ class Cell { codepoints_[codepointCount_++] = _codepoint; #endif - constexpr bool AllowWidthChange = false; // TODO: make configurable + constexpr bool AllowWidthChange = true; auto const width = [&]() { switch (_codepoint) @@ -397,7 +416,7 @@ class Cell { } }(); - if (width != width_ && AllowWidthChange) + if (width && width != width_) { int const diff = width - width_; width_ = static_cast(width); @@ -419,7 +438,7 @@ class Cell { void setHyperlink(HyperlinkRef const& _hyperlink) { hyperlink_ = _hyperlink; } #endif - private: +private: /// Unicode codepoint to be displayed. #if defined(CONTOUR_TERMINAL_CELL_USE_STRING) std::u32string codepoints_; @@ -461,80 +480,47 @@ inline bool operator==(Cell const& a, Cell const& b) noexcept // }}} -class Line { // {{{ - public: - enum class Flags : uint8_t { +using GridBuffer = std::vector; + +class Line // {{{ +{ +public: + enum class Flags : uint8_t + { None = 0x0000, Wrappable = 0x0001, Wrapped = 0x0002, Marked = 0x0004, + // TODO: DoubleWidth = 0x0010, + // TODO: DoubleHeight = 0x0020, }; - using Buffer = std::vector; - using iterator = Buffer::iterator; - using const_iterator = Buffer::const_iterator; - using reverse_iterator = Buffer::reverse_iterator; - - Line(ColumnCount _numCols, Cell const& _defaultCell, Flags _flags) : - buffer_(unbox(_numCols), _defaultCell), - flags_{static_cast(_flags)} - {} - - Line(Buffer const& _init, Flags _flags) : Line(Buffer(_init), _flags) {} - Line(Buffer&& _init, Flags _flags); - Line(iterator const& _begin, iterator const& _end, Flags _flags); - Line(ColumnCount _numCols, Buffer&& _init, Flags _flags); - Line(ColumnCount _numCols, std::string_view const& _s, Flags _flags); - - Buffer& buffer() noexcept { return buffer_; } - - Line() = default; + Line(): start_{}, size_{} {} Line(Line const&) = default; Line(Line&&) = default; Line& operator=(Line const&) = default; Line& operator=(Line&&) = default; + Line(size_t _start, ColumnCount _size, Flags _flags, GraphicsAttributes _initialSGR): + start_{_start}, + size_{_size}, + flags_{static_cast(_flags)}, + initialSGR_{_initialSGR} + {} + + GraphicsAttributes initialSGR() const noexcept { return initialSGR_; } + void reset(GraphicsAttributes _attributes) noexcept { - for (Cell& cell: buffer_) - cell.reset(_attributes); + initialSGR_ = _attributes; + //size_ = ColumnCount(0); } - Buffer* operator->() noexcept { return &buffer_; } - Buffer const* operator->() const noexcept { return &buffer_; } - auto& operator[](std::size_t _index) { return buffer_[_index]; } - auto const& operator[](std::size_t _index) const { return buffer_[_index]; } + ColumnCount size() const noexcept { return size_; } + void setSize(ColumnCount _n) noexcept { size_ = _n; } - void prepend(Buffer const&); - void append(Buffer const&); - void append(int _count, Cell const& _initial); - - Buffer remove(iterator const& _from, iterator const& _to); - - /// Shhift left by @p _count cells and fill right with cells of @p _fill. - /// - /// @returns sequence of cells that have been shifted out. - Buffer shift_left(int _count, Cell const& _fill); - - crispy::range trim_blank_right() const; - - ColumnCount size() const noexcept { return ColumnCount::cast_from(buffer_.size()); } - - bool blank() const noexcept; - - // TODO (trimmed version of size()): int maxOccupiedColumns() const noexcept { return size(); } - - void resize(ColumnCount _size); - [[nodiscard]] Buffer reflow(ColumnCount _column); - - iterator begin() { return buffer_.begin(); } - iterator end() { return buffer_.end(); } - const_iterator begin() const { return buffer_.begin(); } - const_iterator end() const { return buffer_.end(); } - reverse_iterator rbegin() { return buffer_.rbegin(); } - reverse_iterator rend() { return buffer_.rend(); } - const_iterator cbegin() const { return buffer_.cbegin(); } - const_iterator cend() const { return buffer_.cend(); } + size_t begin_offset() const noexcept { return start_; } + size_t end_offset() const noexcept { return start_ + size_.as(); } bool marked() const noexcept { return isFlagEnabled(Flags::Marked); } void setMarked(bool _enable) { setFlag(Flags::Marked, _enable); } @@ -548,11 +534,6 @@ class Line { // {{{ Flags wrappableFlag() const noexcept { return wrappable() ? Line::Flags::Wrappable : Line::Flags::None; } Flags markedFlag() const noexcept { return marked() ? Line::Flags::Marked : Line::Flags::None; } - std::string toUtf8() const; - std::string toUtf8Trimmed() const; - - void setText(std::string_view _u8string); - Flags flags() const noexcept { return static_cast(flags_); } Flags inheritableFlags() const noexcept @@ -570,11 +551,24 @@ class Line { // {{{ flags_ &= ~static_cast(_flag); } - bool isFlagEnabled(Flags _flag) const noexcept { return (flags_ & static_cast(_flag)) != 0; } + bool isFlagEnabled(Flags _flag) const noexcept + { + return (flags_ & static_cast(_flag)) != 0; + } private: - Buffer buffer_; unsigned flags_; + + // {{{ NOTE: these fields make only sense if we allow variable sized lines, as a matter of optimization. + /// absolute offset into buffer pointing to the first column of this line + size_t start_; + + // Number of columns this line covers. + ColumnCount size_; + + // SGR for uninitialized columns + GraphicsAttributes initialSGR_; + // }}} }; constexpr Line::Flags operator|(Line::Flags a, Line::Flags b) noexcept @@ -588,16 +582,7 @@ constexpr bool operator&(Line::Flags a, Line::Flags b) noexcept } // }}} -using Lines = std::deque; -using ColumnIterator = Line::iterator; -using LineIterator = Lines::iterator; - -inline auto begin(Line& _line) { return _line.begin(); } -inline auto end(Line& _line) { return _line.end(); } -inline auto begin(Line const& _line) { return _line.cbegin(); } -inline auto end(Line const& _line) { return _line.cend(); } -inline Line::const_iterator cbegin(Line const& _line) { return _line.cbegin(); } -inline Line::const_iterator cend(Line const& _line) { return _line.cend(); } +using Lines = std::vector; /** * Manages the screen grid buffer (main screen + scrollback history). @@ -625,72 +610,128 @@ inline Line::const_iterator cend(Line const& _line) { return _line.cend(); } * 1 screenSize.columns * */ -class Grid { - public: - // TODO: Rename all "History" to "Scrollback"? - - Grid(PageSize _screenSize, bool _reflowOnResize, std::optional _maxHistoryLineCount); +class Grid +{ +// TODO: Rename all "History" to "Scrollback"? +public: + Grid(PageSize _screenSize, bool _reflowOnResize, LineCount _maxHistoryLineCount); Grid(): Grid(PageSize{LineCount(25), ColumnCount(80)}, false, LineCount(0)) {} + // {{{ grid global properties + LineCount maxHistoryLineCount() const noexcept { return maxHistoryLineCount_; } + void setMaxHistoryLineCount(LineCount _maxHistoryLineCount); + + bool reflowOnResize() const noexcept { return reflowOnResize_; } + void setReflowOnResize(bool _enabled) { reflowOnResize_ = _enabled; } + PageSize screenSize() const noexcept { return screenSize_; } /// Resizes the main page area of the grid and adapts the scrollback area's width accordingly. /// /// @param _screenSize new size of the main page area /// @param _currentCursorPos current cursor position - /// @param _wrapPending indicates whether a cursor wrap is pending before the next text write. /// /// @returns updated cursor position. - Coordinate resize(PageSize _screenSize, Coordinate _currentCursorPos, bool _wrapPending); - - std::optional maxHistoryLineCount() const noexcept { return maxHistoryLineCount_; } - void setMaxHistoryLineCount(std::optional _maxHistoryLineCount); + Coordinate resize(PageSize _screenSize, Coordinate _currentCursorPos); - bool reflowOnResize() const noexcept { return reflowOnResize_; } - void setReflowOnResize(bool _enabled) { reflowOnResize_ = _enabled; } + LineOffset scrollTopOffset() const noexcept + { + return -(linesUsed_ - screenSize_.lines).as(); + } LineCount historyLineCount() const noexcept { - return LineCount::cast_from(lines_.size()) - screenSize_.lines; + return linesUsed_ - screenSize_.lines; } - /// Renders the full screen by passing every grid cell to the callback. - template - void render(RendererT && _render, std::optional _scrollOffset = std::nullopt) const; + // }}} - Line& absoluteLineAt(int _line) noexcept; - Line const& absoluteLineAt(int _line) const noexcept; + // {{{ Line API /// @returns reference to Line at given relative offset @p _line. - Line& lineAt(int _line) noexcept; - Line const& lineAt(int _line) const noexcept; + Line& lineAt(LineOffset _line) noexcept; + Line const& lineAt(LineOffset _line) const noexcept; - /// Converts a relative line number into an absolute line number. - int toAbsoluteLine(int _relativeLine) const noexcept; + gsl::span lines(LineOffset _start, LineCount _count) const; + gsl::span lines(LineOffset _start, LineCount _count); + + gsl::span lineBuffer(LineOffset _line) noexcept + { + auto const width = screenSize_.columns.as(); + auto const lineOffset = realLineOffset(_line).as(); + Cell* start = buffer_.data() + lineOffset * width; + return gsl::make_span(start, width); + } - /// Converts an absolute line number into a relative line number. - int toRelativeLine(int _absoluteLine) const noexcept; + gsl::span lineBuffer(LineOffset _line) const noexcept + { + return const_cast(this)->lineBuffer(_line); + } - int computeRelativeLineNumberFromBottom(int _n) const noexcept; + gsl::span lineBufferRightTrimmed(LineOffset _line) const noexcept; - /// Gets a reference to the cell relative to screen origin (top left, 1:1). - Cell& at(Coordinate const& _coord) noexcept; + std::string lineText(LineOffset _line) const; + std::string lineTextTrimmed(LineOffset _line) const; + + void setLineText(LineOffset _line, std::string_view _text); + + void resetLine(LineOffset _line, GraphicsAttributes _attribs) + { + Line& line = lines_[realLineOffset(_line).as()]; + line.reset(_attribs); + for (Cell& cell: lineBuffer(_line)) + cell.reset(_attribs); + } + + ColumnCount lineLength(LineOffset _line) const noexcept + { + auto const realLine = realLineOffset(_line); + return lines_[realLine.as()].size(); + } + + bool isLineBlank(LineOffset _line) const noexcept; + + bool isLineWrapped(LineOffset _line) const noexcept + { + return lines_[realLineOffset(_line).as()].wrapped(); + } + + int computeLogicalLineNumberFromBottom(LineCount _n) const noexcept; + // }}} + + // {{{ Cell Access /// Gets a reference to the cell relative to screen origin (top left, 1:1). - Cell const& at(Coordinate const& _coord) const noexcept; + Cell& at(LineOffset _line, ColumnOffset _column) noexcept; + Cell const& at(LineOffset _line, ColumnOffset _column) const noexcept; + + GridBuffer::const_iterator iteratorAt(LineOffset _line, ColumnOffset _column) const + { + return std::next(buffer_.begin(), realLineOffset(_line).as() + _column.as()); + } - crispy::range lines(LinePosition _start, LinePosition _end) const; - crispy::range lines(LinePosition _start, LinePosition _end); - // TODO: ^^ these are actually of type HistoryLinePostiion ^^ + GridBuffer::iterator iteratorAt(LineOffset _line, ColumnOffset _column) + { + // NOTE: This is intentionally not doing any bounds checking on line nor column. + return std::next(buffer_.begin(), realLineOffset(_line).as() * screenSize_.columns.as() + _column.as()); + } + // }}} + + // {{{ page view API + gsl::span pageAtScrollOffset(ScrollOffset _scrollOffset); + gsl::span pageAtScrollOffset(ScrollOffset _scrollOffset) const; - crispy::range pageAtScrollOffset(std::optional _scrollOffset) const; - crispy::range pageAtScrollOffset(std::optional _scrollOffset); + gsl::span mainPage(); + gsl::span mainPage() const; - crispy::range mainPage() const; - crispy::range mainPage(); + gsl::span scrollbackLines() const; + // }}} - crispy::range scrollbackLines() const; + // {{{ buffer manipulation + + GridBuffer& buffer() noexcept { return buffer_; } + GridBuffer const& buffer() const noexcept { return buffer_; } /// Completely deletes all scrollback lines. void clearHistory(); @@ -702,6 +743,8 @@ class Grid { /// @param _margin the margin coordinates to perform the scrolling action into. void scrollUp(LineCount _n, GraphicsAttributes const& _defaultAttributes, Margin const& _margin); + void scrollUpUninitialized(LineCount _n); + /// Scrolls down by @p _n lines within the given margin. /// /// @param _n number of lines to scroll down within the given margin. @@ -709,15 +752,42 @@ class Grid { /// @param _margin the margin coordinates to perform the scrolling action into. void scrollDown(LineCount _n, GraphicsAttributes const& _defaultAttributes, Margin const& _margin); - std::string renderTextLineAbsolute(int row) const; - std::string renderTextLine(int row) const; - std::string renderText() const; + // }}} + + // {{{ Rendering API + /// Renders the full screen by passing every grid cell to the callback. + template + void render(RendererT && _render, ScrollOffset _scrollOffset = {}) const; + + /// Takes text-screenshot of the given line offset. + std::string renderTextLine(LineOffset _row) const; + + /// Takes text-screenshot of the main page. + std::string renderMainPageText() const; /// Renders the full grid's text characters. /// /// Empty cells are represented as strings and lines split by LF. std::string renderAllText() const; + // }}} + + /// Converts a relative line number into an absolute line number. + int toAbsoluteLine(int _relativeLine) const noexcept; + + LineOffset realLineOffset(LineOffset p) const noexcept + { + return (mainPageTopLineIndex_.as() + p) + % LineOffset::cast_from(lines_.size()); + } + + /// Translates relative line offset into an absolute offset into the buffer. + size_t toBufferOffset(LineOffset p) const noexcept + { + auto const lineNr = (mainPageTopLineIndex_.as() + p).as() % lines_.size(); + return lineNr * screenSize_.columns.as(); + } + private: /// Ensures the maxHistoryLineCount attribute will be satisified, potentially deleting any /// overflowing history line. @@ -728,156 +798,132 @@ class Grid { // PageSize screenSize_; bool reflowOnResize_; - std::optional maxHistoryLineCount_; + LineCount maxHistoryLineCount_; + + GridBuffer buffer_; + Lines lines_; + LineOffset mainPageTopLineIndex_; //!< index to first line in the ring buffer. + LineCount linesUsed_; //!< number of lines used in the scrollback ring buffer. + // XXX ^^^ do we want/need you? }; // {{{ inlines template -inline void Grid::render(RendererT && _render, std::optional _scrollOffset) const +inline void Grid::render(RendererT && _render, ScrollOffset _scrollOffset) const { - for (auto const && [rowNumber, line] : crispy::indexed(pageAtScrollOffset(_scrollOffset), 1)) - { - for (auto const && [colNumber, column] : crispy::indexed(line, 1)) - _render({rowNumber, colNumber}, column); + assert(!_scrollOffset || _scrollOffset.as() <= historyLineCount()); - auto const columnCount = std::max( - ColumnCount(0), - screenSize_.columns - line.size() - ); - for (auto const colNumber : crispy::times(unbox(line.size()) + 1, unbox(columnCount))) - _render({rowNumber, colNumber}, Cell{}); - } -} + auto const scrollOffset = -_scrollOffset.as(); + auto const topLeftOffset = realLineOffset(scrollOffset).as() * screenSize_.columns.as(); + auto const bottomRightOffset = topLeftOffset + *screenSize_.columns * *screenSize_.lines; + auto const pageCellCount = screenSize_.lines.as() * screenSize_.columns.as(); -inline Line& Grid::absoluteLineAt(int _line) noexcept -{ - assert(crispy::ascending(0, _line, static_cast(lines_.size()) - 1)); - return *next(lines_.begin(), _line); -} + assert(bottomRightOffset >= pageCellCount); -inline Line const& Grid::absoluteLineAt(int _line) const noexcept -{ - return const_cast(*this).absoluteLineAt(_line); + auto a = buffer_.data() + topLeftOffset; + auto b = buffer_.data() + bottomRightOffset; + auto k = 0ul; + while (a != b) + { + _render(*a++, + LineOffset(k / screenSize_.columns.as()), + ColumnOffset(k % screenSize_.columns.as())); + k++; + } } -inline Line& Grid::lineAt(int _line) noexcept +inline Line& Grid::lineAt(LineOffset _line) noexcept { - assert(crispy::ascending(1 - *historyLineCount(), _line, *screenSize_.lines)); - - return *next(lines_.begin(), *historyLineCount() + _line - 1); + auto i = realLineOffset(_line).as(); + return lines_[i]; } -inline Line const& Grid::lineAt(int _line) const noexcept +inline Line const& Grid::lineAt(LineOffset _line) const noexcept { return const_cast(*this).lineAt(_line); } inline int Grid::toAbsoluteLine(int _relativeLine) const noexcept { - return *historyLineCount() + _relativeLine - 1; + return *historyLineCount() + _relativeLine; } -inline int Grid::toRelativeLine(int _absoluteLine) const noexcept +inline Cell& Grid::at(LineOffset _line, ColumnOffset _column) noexcept { - return _absoluteLine - *historyLineCount(); -} + Expects(ColumnOffset(0) <= _column); + Expects(_column < screenSize_.columns.as()); -inline Cell& Grid::at(Coordinate const& _coord) noexcept -{ - assert(crispy::ascending(1 - unbox(historyLineCount()), _coord.row, unbox(screenSize_.lines))); - assert(crispy::ascending(1, _coord.column, unbox(screenSize_.columns))); - - if (_coord.row > 0) - return (*next(lines_.rbegin(), unbox(screenSize_.lines) - _coord.row))[static_cast(_coord.column - 1)]; - else - return (*next(lines_.begin(), unbox(historyLineCount()) + _coord.row - 1))[static_cast(_coord.column - 1)]; + auto const lineStart = realLineOffset(_line).as() * screenSize_.columns.as(); + return buffer_[lineStart + _column.as()]; } -inline Cell const& Grid::at(Coordinate const& _coord) const noexcept +inline Cell const& Grid::at(LineOffset _line, ColumnOffset _column) const noexcept { - return const_cast(*this).at(_coord); + return const_cast(*this).at(_line, _column); } -inline crispy::range Grid::lines(LinePosition _start, LinePosition _end) const +inline gsl::span Grid::lines(LineOffset _start, LineCount _count) { - assert(crispy::ascending(0, *_start, int(lines_.size()) - 1) && "Absolute scroll offset must not be negative or overflowing."); - assert(crispy::ascending(_start, _end, LinePosition::cast_from(lines_.size() - 1)) && "Absolute scroll offset must not be negative or overflowing."); + assert(crispy::ascending(LineOffset{0}, _start, boxed_cast(linesUsed_)) && "Absolute scroll offset must not be negative or overflowing."); + assert(_start.as() + _count.as() <= linesUsed_ && "Absolute scroll offset must not be negative or overflowing."); - return crispy::range( - next(lines_.cbegin(), unbox(_start)), - next(lines_.cbegin(), unbox(_end)) - ); + return gsl::span{ + lines_.data() + _start.as(), + _count.as() + }; } -inline crispy::range Grid::lines(LinePosition _start, LinePosition _end) +inline gsl::span Grid::lines(LineOffset _start, LineCount _count) const { - assert(crispy::ascending(LinePosition{0}, _start, LinePosition::cast_from(lines_.size())) && "Absolute scroll offset must not be negative or overflowing."); - assert(crispy::ascending(_start, _end, LinePosition::cast_from(lines_.size())) && "Absolute scroll offset must not be negative or overflowing."); - - return crispy::range( - next(lines_.begin(), unbox(_start)), - next(lines_.begin(), unbox(_end)) - ); + return const_cast(this)->lines(_start, _count); } -inline crispy::range Grid::pageAtScrollOffset(std::optional _scrollOffset) const +inline gsl::span Grid::pageAtScrollOffset(ScrollOffset _scrollOffset) const { - assert( - crispy::ascending( - StaticScrollbackPosition(0), - _scrollOffset.value_or(StaticScrollbackPosition{0}), - boxed_cast(historyLineCount()) - ) && - "Absolute scroll offset must not be negative or overflowing." - ); - - auto const start = std::next(lines_.cbegin(), - unbox(_scrollOffset.value_or(boxed_cast(historyLineCount())))); - auto const end = std::next(start, unbox(screenSize_.lines)); - - return crispy::range(start, end); + assert(scrollTopOffset().as() <= _scrollOffset); + assert(_scrollOffset <= ScrollOffset(0)); + + size_t const i = *realLineOffset(_scrollOffset.as()) + * *screenSize_.columns; + + return gsl::span{ + &buffer_[i], + static_cast( + screenSize_.columns.value * screenSize_.lines.value + ) + }; } -inline crispy::range Grid::pageAtScrollOffset(std::optional _scrollOffset) +inline gsl::span Grid::pageAtScrollOffset(ScrollOffset _scrollOffset) { - assert( - crispy::ascending( - StaticScrollbackPosition{0}, - _scrollOffset.value_or(StaticScrollbackPosition{0}), - boxed_cast(historyLineCount()) - ) && - "Absolute scroll offset must not be negative or overflowing." - ); - - return crispy::range( - std::next( - lines_.begin(), - unbox(_scrollOffset.value_or(boxed_cast(historyLineCount()))) - ), - lines_.end() - ); + assert(scrollTopOffset().as() <= _scrollOffset); + assert(_scrollOffset <= ScrollOffset(0)); + + size_t const i = *realLineOffset(_scrollOffset.as()) + * *screenSize_.columns; + + return gsl::span{ + &buffer_[i], + static_cast( + screenSize_.columns.value * screenSize_.lines.value + ) + }; } -inline crispy::range Grid::mainPage() const +inline gsl::span Grid::mainPage() const { - return pageAtScrollOffset(std::nullopt); + return pageAtScrollOffset({}); } -inline crispy::range Grid::mainPage() +inline gsl::span Grid::mainPage() { - return pageAtScrollOffset(std::nullopt); + return pageAtScrollOffset({}); } -inline crispy::range Grid::scrollbackLines() const +inline gsl::span Grid::scrollbackLines() const { - return crispy::range( - lines_.cbegin(), - std::next( - lines_.cbegin(), - unbox(historyLineCount()) - ) - ); + return gsl::span(lines_.data(), historyLineCount().as()); } // }}} @@ -893,7 +939,6 @@ namespace fmt { { static const std::array, 3> nameMap = { std::pair{ terminal::Line::Flags::Wrappable, std::string_view("Wrappable")}, - std::pair{ terminal::Line::Flags::Wrapped, std::string_view("Wrapped")}, std::pair{ terminal::Line::Flags::Marked, std::string_view("Marked")} }; std::string s; diff --git a/src/terminal/Grid_test.cpp b/src/terminal/Grid_test.cpp index ba2babca14..63fcbebc22 100644 --- a/src/terminal/Grid_test.cpp +++ b/src/terminal/Grid_test.cpp @@ -27,36 +27,110 @@ namespace // {{{ helper { UNSCOPED_INFO(fmt::format("Grid.dump({}, {}): {}\n", _grid.historyLineCount(), _grid.screenSize(), _headline)); - for (int row = 0; row < unbox(_grid.historyLineCount()) + unbox(_grid.screenSize().lines); ++row) + // std::string text; + // for (Cell const& cell: _grid.buffer()) + // text += cell.toUtf8(); + // fmt::print("full text: '{}'\n", text); + + for (int row = -_grid.historyLineCount().as(); row < _grid.screenSize().lines.as(); ++row) { UNSCOPED_INFO(fmt::format( - "{}: \"{}\" {}\n", + "{:>2}: \"{}\" {}\n", row, - _grid.renderTextLine(row - unbox(_grid.historyLineCount()) + 1), - _grid.absoluteLineAt(row).flags() + _grid.renderTextLine(LineOffset::cast_from(row - _grid.historyLineCount().as())), + _grid.lineAt(LineOffset::cast_from(row)).flags() )); } } Grid setupGrid5x2() { - auto grid = Grid(PageSize{LineCount(2), ColumnCount(5)}, true, std::nullopt); - grid.lineAt(1).setText("ABCDE"); - grid.lineAt(2).setText("abcde"); + auto grid = Grid(PageSize{LineCount(2), ColumnCount(5)}, true, LineCount(0)); + grid.setLineText(LineOffset{0}, "ABCDE"); + grid.setLineText(LineOffset{1}, "abcde"); + + logGridText(grid, "setup grid at 5x2"); return grid; } Grid setupGrid8x2() { - auto grid = Grid(PageSize{LineCount(2), ColumnCount(8)}, true, std::nullopt); - grid.lineAt(1).setText("ABCDEFGH"); - grid.lineAt(2).setText("abcdefgh"); + auto grid = Grid(PageSize{LineCount(2), ColumnCount(8)}, true, LineCount(0)); + grid.setLineText(LineOffset{0}, "ABCDEFGH"); + grid.setLineText(LineOffset{1}, "abcdefgh"); logGridText(grid, "setup grid at 5x2"); return grid; } } // }}} +TEST_CASE("Grid.setup", "[grid]") +{ + auto grid = Grid(PageSize{LineCount(2), ColumnCount(5)}, true, LineCount(0)); + grid.setLineText(LineOffset{0}, "ABCDE"sv); + grid.setLineText(LineOffset{1}, "abcde"sv); + logGridText(grid, "setup grid at 5x2"); + + CHECK(grid.lineText(LineOffset{0}) == "ABCDE"sv); + CHECK(grid.lineText(LineOffset{1}) == "abcde"sv); +} + +TEST_CASE("Grid.write_overlong_line_via_iteratorAt", "[grid]") +{ + auto grid = Grid(PageSize{LineCount(2), ColumnCount(5)}, true, LineCount(0)); + auto i = grid.iteratorAt(LineOffset(0), ColumnOffset(0)); + for (auto const ch: L"ABCDEabcde"sv) + (i++)->setCharacter(ch); + logGridText(grid, "setup grid at 5x2"); + + CHECK(grid.lineText(LineOffset{0}) == "ABCDE"sv); + CHECK(grid.lineText(LineOffset{1}) == "abcde"sv); +} + +TEST_CASE("Grid.writeAndScrollUp", "[grid]") +{ + auto grid = Grid(PageSize{LineCount(2), ColumnCount(5)}, true, LineCount(3)); + grid.setLineText(LineOffset{0}, "ABCDE"); + grid.setLineText(LineOffset{1}, "abcde"); + CHECK(grid.historyLineCount() == LineCount(0)); + logGridText(grid, "setup grid at 5x2"); + + grid.scrollUpUninitialized(LineCount(1)); + CHECK(grid.historyLineCount() == LineCount(1)); + grid.setLineText(LineOffset{-1}, "ABCDE"); + grid.setLineText(LineOffset{ 0}, "abcde"); + grid.setLineText(LineOffset{ 1}, " "); + + grid.scrollUpUninitialized(LineCount(1)); + CHECK(grid.historyLineCount() == LineCount(2)); + grid.setLineText(LineOffset{-2}, " "); + grid.setLineText(LineOffset{-1}, "ABCDE"); + grid.setLineText(LineOffset{ 0}, "abcde"); + grid.setLineText(LineOffset{ 1}, " "); +} + +TEST_CASE("iteratorAt", "[grid]") +{ + auto grid = Grid(PageSize{LineCount(3), ColumnCount(3)}, true, LineCount(0)); + grid.setLineText(LineOffset{0}, "ABC"sv); + grid.setLineText(LineOffset{1}, "DEF"sv); + grid.setLineText(LineOffset{2}, "GHI"sv); + logGridText(grid); + + auto const a00 = grid.iteratorAt(LineOffset(0), ColumnOffset(0)); + CHECK(a00->toUtf8() == "A"); + auto const a01 = grid.iteratorAt(LineOffset(0), ColumnOffset(1)); + CHECK(a01->toUtf8() == "B"); + auto const a02 = grid.iteratorAt(LineOffset(0), ColumnOffset(2)); + CHECK(a02->toUtf8() == "C"); + + auto const a11 = grid.iteratorAt(LineOffset(1), ColumnOffset(1)); + CHECK(a11->toUtf8() == "E"); + auto const a22 = grid.iteratorAt(LineOffset(2), ColumnOffset(2)); + CHECK(a22->toUtf8() == "I"); +} + +#if 0 // {{{ grid lines TEST_CASE("Line.reflow.unwrappable", "[grid]") { auto line = Line(ColumnCount(5), "ABCDE"sv, Line::Flags::None); @@ -98,15 +172,18 @@ TEST_CASE("Line.reflow.empty", "[grid]") CHECK(*reflowed.size() == 0); CHECK(reflowed.toUtf8() == ""); } - +#endif +// }}} +// {{{ grid reflow +#if 0 TEST_CASE("Grid.reflow.shrink.wrappable", "[grid]") { - auto grid = Grid(PageSize{LineCount(4), ColumnCount(4)}, true, std::nullopt); - grid.lineAt(1).setText("ABCD"); - grid.lineAt(1).setWrappable(true); - grid.lineAt(2).setWrappable(false); - grid.lineAt(3).setWrappable(false); - grid.lineAt(4).setWrappable(false); + auto grid = Grid(PageSize{LineCount(4), ColumnCount(4)}, true, LineCount(0)); + grid.lineAt(LineOffset{0}).setText("ABCD"); + grid.lineAt(LineOffset{0}).setWrappable(true); + grid.lineAt(LineOffset{1}).setWrappable(false); + grid.lineAt(LineOffset{2}).setWrappable(false); + grid.lineAt(LineOffset{3}).setWrappable(false); logGridText(grid, "initial"); (void) grid.resize(PageSize{LineCount(2), ColumnCount(3)}, Coordinate{1, 1}, false); @@ -340,10 +417,10 @@ TEST_CASE("Grid.reflow.tripple", "[grid]") CHECK(grid.historyLineCount() == LineCount(0)); CHECK(grid.screenSize() == PageSize{LineCount(2), ColumnCount(8)}); - CHECK(!grid.lineAt(1).wrapped()); + CHECK(!grid.lineAt(LineOffset{0}).wrapped()); CHECK(grid.renderTextLine(1) == "ABCDEFGH"); - CHECK(!grid.lineAt(2).wrapped()); + CHECK(!grid.lineAt(LineOffset{1}).wrapped()); CHECK(grid.renderTextLine(2) == "abcdefgh"); } @@ -439,3 +516,5 @@ TEST_CASE("Grid.reflow.tripple", "[grid]") // }}} } } +#endif +// }}} diff --git a/src/terminal/Image.cpp b/src/terminal/Image.cpp index dde3d0faaa..c873f5f543 100644 --- a/src/terminal/Image.cpp +++ b/src/terminal/Image.cpp @@ -34,8 +34,8 @@ Image::Data RasterizedImage::fragment(Coordinate _pos) const Image::Data fragData; fragData.resize(*cellSize_.width * *cellSize_.height * 4); // RGBA - auto const availableWidth = min(unbox(image_->width()) - pixelOffset.column, unbox(cellSize_.width)); - auto const availableHeight = min(unbox(image_->height()) - pixelOffset.row, unbox(cellSize_.height)); + auto const availableWidth = min(unbox(image_->width()) - *pixelOffset.column, unbox(cellSize_.width)); + auto const availableHeight = min(unbox(image_->height()) - *pixelOffset.row, unbox(cellSize_.height)); // auto const availableSize = Size{availableWidth, availableHeight}; // std::cout << fmt::format( @@ -71,7 +71,7 @@ Image::Data RasterizedImage::fragment(Coordinate _pos) const for (int y = 0; y < availableHeight; ++y) { - auto const startOffset = ((pixelOffset.row + (availableHeight - 1 - y)) * *image_->width() + pixelOffset.column) * 4; + auto const startOffset = ((*pixelOffset.row + (availableHeight - 1 - y)) * *image_->width() + *pixelOffset.column) * 4; auto const source = &image_->data()[startOffset]; target = copy(source, source + availableWidth * 4, target); diff --git a/src/terminal/InputGenerator.cpp b/src/terminal/InputGenerator.cpp index e7fe7a2a62..fb44b2ad85 100644 --- a/src/terminal/InputGenerator.cpp +++ b/src/terminal/InputGenerator.cpp @@ -560,8 +560,8 @@ namespace bool InputGenerator::generateMouse(MouseButton _button, Modifier _modifier, - int _row, - int _column, + LineOffset _row, + ColumnOffset _column, MouseEventType _eventType) { if (!mouseProtocol_.has_value()) @@ -626,7 +626,7 @@ bool InputGenerator::generateMouse(MouseButton _button, return false; } -bool InputGenerator::mouseTransport(uint8_t _button, uint8_t _modifier, int _row, int _column, MouseEventType _eventType) +bool InputGenerator::mouseTransport(uint8_t _button, uint8_t _modifier, LineOffset _row, ColumnOffset _column, MouseEventType _eventType) { switch (mouseTransport_) { @@ -645,16 +645,16 @@ bool InputGenerator::mouseTransport(uint8_t _button, uint8_t _modifier, int _row return false; } -bool InputGenerator::mouseTransportX10(uint8_t _button, uint8_t _modifier, int _row, int _column) +bool InputGenerator::mouseTransportX10(uint8_t _button, uint8_t _modifier, LineOffset _row, ColumnOffset _column) { constexpr uint8_t SkipCount = 0x20; // TODO std::numeric_limits::max(); constexpr uint8_t MaxCoordValue = std::numeric_limits::max() - SkipCount; - if (_row <= MaxCoordValue && _column <= MaxCoordValue) + if (*_row < MaxCoordValue && *_column < MaxCoordValue) { uint8_t const button = SkipCount + static_cast(_button | _modifier); - uint8_t const row = static_cast(SkipCount + _row); - uint8_t const column = static_cast(SkipCount + _column); + uint8_t const row = static_cast(SkipCount + *_row); + uint8_t const column = static_cast(SkipCount + *_column); append("\033[M"); append(button); append(column); @@ -665,35 +665,35 @@ bool InputGenerator::mouseTransportX10(uint8_t _button, uint8_t _modifier, int _ return false; } -bool InputGenerator::mouseTransportSGR(uint8_t _button, uint8_t _modifier, int _row, int _column, MouseEventType _eventType) +bool InputGenerator::mouseTransportSGR(uint8_t _button, uint8_t _modifier, LineOffset _row, ColumnOffset _column, MouseEventType _eventType) { append("\033[<"); append(static_cast(_button | _modifier)); append(';'); - append(static_cast(_column)); + append(unbox(_column)); append(';'); - append(static_cast(_row)); + append(unbox(_row)); append(_eventType != MouseEventType::Release ? 'M' : 'm'); return true; } -bool InputGenerator::mouseTransportURXVT(uint8_t _button, uint8_t _modifier, int _row, int _column, MouseEventType _eventType) +bool InputGenerator::mouseTransportURXVT(uint8_t _button, uint8_t _modifier, LineOffset _row, ColumnOffset _column, MouseEventType _eventType) { if (_eventType == MouseEventType::Press) { append("\033["); append(static_cast(_button | _modifier)); append(';'); - append(static_cast(_column)); + append(unbox(_column)); append(';'); - append(static_cast(_row)); + append(unbox(_row)); append('M'); } return true; } -bool InputGenerator::generateMousePress(MouseButton _button, Modifier _modifier, int _row, int _column) +bool InputGenerator::generateMousePress(MouseButton _button, Modifier _modifier, LineOffset _row, ColumnOffset _column) { currentMousePosition_ = {_row, _column}; @@ -736,7 +736,7 @@ bool InputGenerator::generateMousePress(MouseButton _button, Modifier _modifier, return generateMouse(_button, _modifier, _row, _column, MouseEventType::Press); } -bool InputGenerator::generateMouseRelease(MouseButton _button, Modifier _modifier, int _row, int _column) +bool InputGenerator::generateMouseRelease(MouseButton _button, Modifier _modifier, LineOffset _row, ColumnOffset _column) { currentMousePosition_ = {_row, _column}; @@ -746,7 +746,7 @@ bool InputGenerator::generateMouseRelease(MouseButton _button, Modifier _modifie return generateMouse(_button, _modifier, _row, _column, MouseEventType::Release); } -bool InputGenerator::generateMouseMove(int _row, int _column, Modifier _modifier) +bool InputGenerator::generateMouseMove(LineOffset _row, ColumnOffset _column, Modifier _modifier) { if (!mouseProtocol_.has_value()) return false; diff --git a/src/terminal/InputGenerator.h b/src/terminal/InputGenerator.h index c0b641151b..b48431bc88 100644 --- a/src/terminal/InputGenerator.h +++ b/src/terminal/InputGenerator.h @@ -270,9 +270,9 @@ class InputGenerator { bool generate(std::u32string const& _characterEvent, Modifier _modifier); bool generate(Key _key, Modifier _modifier); void generatePaste(std::string_view const& _text); - bool generateMousePress(MouseButton _button, Modifier _modifier, int _row, int _column); - bool generateMouseMove(int _row, int _column, Modifier _modifier); - bool generateMouseRelease(MouseButton _button, Modifier _modifier, int _row, int _column); + bool generateMousePress(MouseButton _button, Modifier _modifier, LineOffset _row, ColumnOffset _column); + bool generateMouseMove(LineOffset _row, ColumnOffset _column, Modifier _modifier); + bool generateMouseRelease(MouseButton _button, Modifier _modifier, LineOffset _row, ColumnOffset _column); bool generateFocusInEvent(); bool generateFocusOutEvent(); @@ -299,21 +299,22 @@ class InputGenerator { private: bool generateMouse(MouseButton _button, Modifier _modifier, - int _row, - int _column, + LineOffset _row, + ColumnOffset _column, MouseEventType _eventType); - bool mouseTransport(uint8_t _button, uint8_t _modifier, int _row, int _column, MouseEventType _type); - bool mouseTransportX10(uint8_t _button, uint8_t _modifier, int _row, int _column); - bool mouseTransportSGR(uint8_t _button, uint8_t _modifier, int _row, int _column, MouseEventType _type); - bool mouseTransportURXVT(uint8_t _button, uint8_t _modifier, int _row, int _column, MouseEventType _type); + bool mouseTransport(uint8_t _button, uint8_t _modifier, LineOffset _row, ColumnOffset _column, MouseEventType _type); + bool mouseTransportX10(uint8_t _button, uint8_t _modifier, LineOffset _row, ColumnOffset _column); + bool mouseTransportSGR(uint8_t _button, uint8_t _modifier, LineOffset _row, ColumnOffset _column, MouseEventType _type); + bool mouseTransportURXVT(uint8_t _button, uint8_t _modifier, LineOffset _row, ColumnOffset _column, MouseEventType _type); inline bool append(std::string_view _sequence); inline bool append(char _asciiChar); inline bool append(uint8_t _byte); inline bool append(unsigned int _asciiChar); - private: + // private fields + // KeyMode cursorKeysMode_ = KeyMode::Normal; KeyMode numpadKeysMode_ = KeyMode::Normal; bool bracketedPaste_ = false; @@ -324,7 +325,7 @@ class InputGenerator { Sequence pendingSequence_{}; std::set currentlyPressedMouseButtons_{}; - Coordinate currentMousePosition_{0, 0}; // current mouse position + Coordinate currentMousePosition_{}; // current mouse position }; inline std::string to_string(InputGenerator::MouseEventType _value) diff --git a/src/terminal/Parser.cpp b/src/terminal/Parser.cpp index a5f1ad451e..405c44469a 100644 --- a/src/terminal/Parser.cpp +++ b/src/terminal/Parser.cpp @@ -102,24 +102,21 @@ void Parser::parseFragment(string_view _data) if (input != end && *input == '\n') { eventListener_.execute(static_cast(*input++)); - continue; } + continue; } } static constexpr char32_t ReplacementCharacter {0xFFFD}; - while (input != end) - { - unicode::ConvertResult const r = unicode::from_utf8(utf8DecoderState_, *input); + unicode::ConvertResult const r = unicode::from_utf8(utf8DecoderState_, *input); - if (std::holds_alternative(r)) - processInput(std::get(r).value); - else if (std::holds_alternative(r)) - processInput(ReplacementCharacter); + if (std::holds_alternative(r)) + processInput(std::get(r).value); + else if (std::holds_alternative(r)) + processInput(ReplacementCharacter); - ++input; - } + ++input; } while (input != end); } diff --git a/src/terminal/Screen.cpp b/src/terminal/Screen.cpp index b9fbfb097e..571d5de470 100644 --- a/src/terminal/Screen.cpp +++ b/src/terminal/Screen.cpp @@ -31,6 +31,8 @@ #include #include +#include + #include #include #include @@ -47,14 +49,22 @@ #define LIBTERMINAL_EXECUTION_COMMA(par) /*!*/ #endif -using namespace crispy; using namespace std::string_view_literals; +using crispy::escape; +using crispy::for_each; +using crispy::times; +using crispy::toHexString; + +using gsl::span; + using std::accumulate; -using std::clamp; using std::array; +using std::clamp; using std::distance; using std::endl; +using std::fill; +using std::fill_n; using std::function; using std::get; using std::holds_alternative; @@ -68,8 +78,11 @@ using std::optional; using std::ostringstream; using std::pair; using std::ref; +using std::rotate; +using std::prev; using std::string; using std::string_view; +using std::tuple; using std::vector; namespace terminal { @@ -345,7 +358,7 @@ namespace // {{{ helper constexpr bool GridTextReflowEnabled = true; - array emptyGrids(PageSize _size, optional _maxHistoryLineCount) + array emptyGrids(PageSize _size, LineCount _maxHistoryLineCount) { return array{ Grid(_size, GridTextReflowEnabled, _maxHistoryLineCount), @@ -359,7 +372,7 @@ Screen::Screen(PageSize _size, ScreenEvents& _eventListener, bool _logRaw, bool _logTrace, - optional _maxHistoryLineCount, + LineCount _maxHistoryLineCount, ImageSize _maxImageSize, int _maxImageColorRegisters, bool _sixelCursorConformance, @@ -389,10 +402,21 @@ Screen::Screen(PageSize _size, parser_{ ref(sequencer_) }, size_{ _size }, sixelCursorConformance_{ _sixelCursorConformance }, + margin_{ + Margin::Vertical{{}, size_.lines.as() - LineOffset(1)}, + Margin::Horizontal{{}, size_.columns.as() - ColumnOffset(1)} + }, grids_{ emptyGrids(size(), _maxHistoryLineCount) }, - activeGrid_{ &primaryGrid() } + activeGrid_{ &primaryGrid() }, + cursor_{}, + lastCursorPosition_{} { +#if 0 resetHard(); +#else + setMode(DECMode::AutoWrap, true); + setMode(DECMode::TextReflow, true); +#endif } unsigned Screen::numericCapability(capabilities::Code _cap) const @@ -401,15 +425,15 @@ unsigned Screen::numericCapability(capabilities::Code _cap) const switch (_cap) { - case "li"_tcap: return unbox(size_.lines); - case "co"_tcap: return unbox(size_.columns); - case "it"_tcap: return tabWidth_; + case "li"_tcap: return size_.lines.as(); + case "co"_tcap: return size_.columns.as(); + case "it"_tcap: return tabWidth_.as(); default: return StaticDatabase::numericCapability(_cap); } } -void Screen::setMaxHistoryLineCount(optional _maxHistoryLineCount) +void Screen::setMaxHistoryLineCount(LineCount _maxHistoryLineCount) { primaryGrid().setMaxHistoryLineCount(_maxHistoryLineCount); } @@ -420,8 +444,8 @@ void Screen::resizeColumns(ColumnCount _newColumnCount, bool _clear) if (_clear) { // Sets the left, right, top and bottom scrolling margins to their default positions. - setTopBottomMargin(1, *size_.lines); // DECSTBM - setLeftRightMargin(1, *size_.columns); // DECRLM + setTopBottomMargin({}, size_.lines.as() - LineOffset(1)); // DECSTBM + setLeftRightMargin({}, size_.columns.as() - ColumnOffset(1)); // DECRLM // Erases all data in page memory clearScreen(); @@ -440,34 +464,26 @@ void Screen::resizeColumns(ColumnCount _newColumnCount, bool _clear) void Screen::resize(PageSize _newSize) { - cursor_.position = grid().resize(_newSize, cursor_.position, wrapPending_); - (void) backgroundGrid().resize(_newSize, cursor_.position, false); + cursor_.position = grid().resize(_newSize, cursor_.position); - // update wrap-pending - if (_newSize.columns > size_.columns) - wrapPending_ = 0; - else if (cursor_.position.column == unbox(size_.columns) && _newSize.columns < size_.columns) - // Shrink existing columns to _newSize.columns. - // Nothing should be done, I think, as we preserve prior (now exceeding) content. - if (!wrapPending_) - wrapPending_ = 1; + Grid& otherGrid = isPrimaryScreen() ? alternateGrid() : primaryGrid(); + (void) otherGrid.resize(_newSize, cursor_.position); // Reset margin to their default. margin_ = Margin{ - Margin::Range{1, unbox(_newSize.lines)}, - Margin::Range{1, unbox(_newSize.columns)} - }; + Margin::Vertical{{}, _newSize.lines.as()}, + Margin::Horizontal{{}, _newSize.columns.as()} + }, size_ = _newSize; cursor_.position = clampCoordinate(cursor_.position); - updateCursorIterators(); // update last-cursor position & iterators lastCursorPosition_ = clampCoordinate(lastCursorPosition_); // truncating tabs - while (!tabs_.empty() && tabs_.back() > boxed_cast(_newSize.columns)) + while (!tabs_.empty() && tabs_.back() > _newSize.columns.as()) tabs_.pop_back(); // TODO: find out what to do with DECOM mode. Reset it to? @@ -476,18 +492,7 @@ void Screen::resize(PageSize _newSize) void Screen::verifyState() const { #if !defined(NDEBUG) - auto const lrmm = isModeEnabled(DECMode::LeftRightMargin); - if (wrapPending_ && - ((lrmm && (cursor_.position.column + wrapPending_ - 1) != margin_.horizontal.to) - || (!lrmm && (cursor_.position.column + wrapPending_ - 1) != unbox(size_.columns)))) - { - fail(fmt::format( - "Wrap is pending but cursor's column ({}) is not at right side of margin ({}) or screen ({}).", - cursor_.position.column, margin_.horizontal.to, size_.columns - )); - } - - if (unbox(size_.lines) != grid().mainPage().size()) + if (size_.lines.as() * size_.columns.as() != grid().mainPage().size()) fail(fmt::format("Line count mismatch. Actual line count {} but should be {}.", grid().mainPage().size(), size_.lines)); // verify cursor positions @@ -495,18 +500,6 @@ void Screen::verifyState() const if (cursor_.position != clampedCursorPos) fail(fmt::format("Cursor {} does not match clamp to screen {}.", cursor_, clampedCursorPos)); // FIXME: the above triggers on tmux vertical screen split (cursor.column off-by-one) - - // verify iterators - [[maybe_unused]] auto const line = next(begin(grid().mainPage()), cursor_.position.row - 1); - [[maybe_unused]] auto const col = columnIteratorAt(cursor_.position.column); - - if (line != currentLine_) - fail(fmt::format("Calculated current line does not match.")); - else if (col != currentColumn()) - fail(fmt::format("Calculated current column does not match.")); - - if (wrapPending_ && (cursor_.position.column + wrapPending_ - 1) != unbox(size_.columns) && cursor_.position.column != margin_.horizontal.to) - fail(fmt::format("wrapPending flag set when cursor is not in last column.")); #endif } @@ -548,42 +541,93 @@ void Screen::writeText(std::string_view _chars) // TODO: can be optimized. // - This may require Grid/Line/Cell updates first before yielding any impact. // - Also, we could use SIMD to batch-store the grid cell attributes. +#if 0 for (char ch: _chars) writeText(ch); +#else + auto i = _chars.data(); + auto e = _chars.data() + _chars.size(); + size_t charsLeft = _chars.size(); + char32_t lastChar = sequencer_.precedingGraphicCharacter(); + + while (i != e) + { + bool const cursorInsideMargin = isModeEnabled(DECMode::LeftRightMargin) && isCursorInsideMargins(); + auto const cellsAvailable = cursorInsideMargin + ? *(margin_.horizontal.to - cursor_.position.column) - 1 + : *size_.columns - *cursor_.position.column - 1; + auto const n = min(size_t(cellsAvailable), charsLeft); + for (size_t k = 0; k < n && i != e; ++k) + { + char32_t const codepoint = cursor_.charsets.map(*i); + if (!lastChar || unicode::grapheme_segmenter::breakable(lastChar, codepoint)) + { + // writeCharToCurrentAndAdvance(codepoint); + Cell& cell = currentCell(); + cell.setCharacter(codepoint); + cell.setAttributes(cursor_.graphicsRendition); + #if defined(LIBTERMINAL_HYPERLINKS) + cell.setHyperlink(currentHyperlink_); + #endif + lastCursorPosition_ = cursor_.position; + cursor_.position.column += ColumnOffset::cast_from(cell.width()); + for (int i = 1; i < cell.width(); ++i) + { +#if defined(LIBTERMINAL_HYPERLINKS) + currentCell().reset(cursor_.graphicsRendition, currentHyperlink_); +#else + currentCell().reset(cursor_.graphicsRendition); +#endif + } + } + else + { + auto const extendedWidth = lastPosition().appendCharacter(codepoint); + if (extendedWidth > 0) + clearAndAdvance(extendedWidth); + } + ++i; + } + linefeed(margin_.horizontal.from); + } + + // TODO: Call this but with range range of point. + // XXX: But even if we keep it but enable the setReportDamage(bool), + // then this should still be cheap as it's only invoked when something + // is actually selected. + // eventListener_.markRegionDirty( + // cursor_.position.row, + // cursor_.position.column + // ); + + sequencer_.resetInstructionCounter(); +#endif } void Screen::writeText(char32_t _char) { #if defined(LIBTERMINAL_LOG_TRACE) - if (debugtag::enabled(VTParserTraceTag)) + if (crispy::debugtag::enabled(VTParserTraceTag)) debuglog(VTParserTraceTag).write("text: {}", unicode::convert_to(_char)); #endif - bool const consecutiveTextWrite = sequencer_.instructionCounter() == 1; - if (wrapPending_ && cursor_.autoWrap) + if (wrapPending_ && cursor_.autoWrap) // && !isModeEnabled(DECMode::TextReflow)) { linefeed(margin_.horizontal.from); - if (isModeEnabled(DECMode::TextReflow)) - currentLine_->setWrapped(true); } - char32_t const ch = - _char < 127 ? cursor_.charsets.map(static_cast(_char)) - : _char == 0x7F ? ' ' : _char; - - char32_t const lastChar = - consecutiveTextWrite && !lastPosition().empty() - ? lastPosition().codepoints().back() - : char32_t{0}; + bool const consecutiveTextWrite = sequencer_.instructionCounter() == 1; + char32_t const codepoint = cursor_.charsets.map(_char); - bool const insertToPrev = - lastChar && unicode::grapheme_segmenter::nonbreakable(lastChar, ch); + auto const lastChar = sequencer_.precedingGraphicCharacter(); - if (!insertToPrev) - writeCharToCurrentAndAdvance(ch); + if (!lastChar || unicode::grapheme_segmenter::breakable(lastChar, codepoint)) + { + writeCharToCurrentAndAdvance(codepoint); + } else { - auto const extendedWidth = lastPosition().appendCharacter(ch); + auto const extendedWidth = lastPosition().appendCharacter(codepoint); if (extendedWidth > 0) clearAndAdvance(extendedWidth); @@ -594,9 +638,11 @@ void Screen::writeText(char32_t _char) void Screen::writeCharToCurrentAndAdvance(char32_t _character) { - Cell& cell = *currentColumn(); + Cell& cell = currentCell(); + cell.setCharacter(_character); cell.setAttributes(cursor_.graphicsRendition); + #if defined(LIBTERMINAL_HYPERLINKS) cell.setHyperlink(currentHyperlink_); #endif @@ -605,8 +651,8 @@ void Screen::writeCharToCurrentAndAdvance(char32_t _character) bool const cursorInsideMargin = isModeEnabled(DECMode::LeftRightMargin) && isCursorInsideMargins(); auto const cellsAvailable = cursorInsideMargin - ? margin_.horizontal.to - cursor_.position.column - : unbox(size_.columns) - cursor_.position.column; + ? *(margin_.horizontal.to - cursor_.position.column) - 1 + : *size_.columns - *cursor_.position.column - 1; auto const n = min(cell.width(), cellsAvailable); @@ -617,19 +663,24 @@ void Screen::writeCharToCurrentAndAdvance(char32_t _character) for (int i = 1; i < n; ++i) { #if defined(LIBTERMINAL_HYPERLINKS) - currentColumn()->reset(cursor_.graphicsRendition, currentHyperlink_); + currentCell().reset(cursor_.graphicsRendition, currentHyperlink_); #else - currentColumn()->reset(cursor_.graphicsRendition; + currentCell().reset(cursor_.graphicsRendition); #endif cursor_.position.column++; } } else if (cursor_.autoWrap) - wrapPending_ = 1; + wrapPending_ = true; + // TODO: maybe move selector API up? So we can make this call conditional, + // and only call it when something is selected? + // Alternatively we could add a boolean to make this callback + // conditional, something like: setReportDamage(bool); + // The latter is probably the easiest. eventListener_.markRegionDirty( - LinePosition::cast_from(cursor_.position.row), - ColumnPosition::cast_from(cursor_.position.column) + cursor_.position.row, + cursor_.position.column ); } @@ -638,32 +689,33 @@ void Screen::clearAndAdvance(int _offset) if (_offset == 0) return; - auto const availableColumnCount = margin_.horizontal.length() - cursor_.position.column; - auto const n = min(_offset, availableColumnCount); + auto const availableColumnCount = margin_.horizontal.length() - *cursor_.position.column; + auto const n = min(_offset, *availableColumnCount); if (n == _offset) { cursor_.position.column++; assert(n > 0); - for (auto i = 0; i < n; ++i) + for (auto i = 1; i < n; ++i) { #if defined(LIBTERMINAL_HYPERLINKS) - currentColumn()->reset(cursor_.graphicsRendition, currentHyperlink_); + currentCell().reset(cursor_.graphicsRendition, currentHyperlink_); #else - currentColumn()->reset(cursor_.graphicsRendition); + currentCell().reset(cursor_.graphicsRendition); #endif cursor_.position.column++; } } else if (cursor_.autoWrap) { - wrapPending_ = 1; + wrapPending_ = true; } } std::string Screen::screenshot(function const& _postLine) const { auto result = std::stringstream{}; +#if 0 // TODO(pr) auto writer = VTWriter(result); for (int const absoluteRow : crispy::times(1, *grid().historyLineCount() + *size_.lines)) @@ -698,38 +750,41 @@ std::string Screen::screenshot(function const& _postLine) const writer.write('\n'); } +#endif return result.str(); } -optional Screen::findMarkerBackward(int _currentCursorLine) const +optional Screen::findMarkerUpwards(ScrollOffset _currentScrollOffset) const { - // XXX _currentCursorLine is an absolute history line coordinate - if (_currentCursorLine < 0 || isAlternateScreen()) + // XXX _currentScrollOffset is an absolute history line coordinate + if (*_currentScrollOffset < 0 || isAlternateScreen()) + return nullopt; + if (*_currentScrollOffset >= *historyLineCount()) return nullopt; - _currentCursorLine = min( - _currentCursorLine, - unbox(historyLineCount()) + unbox(size_.lines) - ); - - for (int i = _currentCursorLine - 1; i >= 0; --i) - if (grid().absoluteLineAt(i).marked()) - return {i}; + for (LineOffset i = -_currentScrollOffset.as() - 1; i >= -historyLineCount().as(); --i) + if (grid().lineAt(i).marked()) + return {-i.as()}; return nullopt; } -optional Screen::findMarkerForward(int _currentCursorLine) const +optional Screen::findMarkerDownwards(ScrollOffset _currentScrollOffset) const { - if (_currentCursorLine < 0 || !isPrimaryScreen()) + if (!isPrimaryScreen() || *_currentScrollOffset <= 0) return nullopt; - auto const end = unbox(historyLineCount()) + - unbox(grid().screenSize().lines); + auto const top = + *_currentScrollOffset < *historyLineCount() + ? -_currentScrollOffset.as() + : -historyLineCount().as() + ; + + auto const bottom = LineOffset(0); - for (int i = _currentCursorLine + 1; i < end; ++i) - if (grid().absoluteLineAt(i).marked()) - return {i}; + for (LineOffset i = top + 1; i <= bottom; ++i) + if (grid().lineAt(i).marked()) + return {-i.as()}; return nullopt; } @@ -743,14 +798,14 @@ void Screen::clearAllTabs() void Screen::clearTabUnderCursor() { // populate tabs vector in case of default tabWidth is used (until now). - if (tabs_.empty() && tabWidth_ != 0) - for (int column = tabWidth_; column <= unbox(size_.columns); column += tabWidth_) - tabs_.emplace_back(column); + if (tabs_.empty() && *tabWidth_ != 0) + for (auto column = tabWidth_.as(); column < size_.columns.as(); column += tabWidth_.as()) + tabs_.emplace_back(column - 1); // erase the specific tab underneath for (auto i = begin(tabs_); i != end(tabs_); ++i) { - if (*i == ColumnPosition(realCursorPosition().column)) + if (*i == realCursorPosition().column) { tabs_.erase(i); break; @@ -783,9 +838,8 @@ void Screen::restoreCursor() void Screen::restoreCursor(Cursor const& _savedCursor) { - wrapPending_ = 0; + wrapPending_ = false; cursor_ = _savedCursor; - updateCursorIterators(); } void Screen::resetSoft() @@ -794,15 +848,15 @@ void Screen::resetSoft() setMode(DECMode::BatchedRendering, false); setMode(DECMode::TextReflow, GridTextReflowEnabled); setGraphicsRendition(GraphicsRendition::Reset); // SGR - savedCursor_.position = Coordinate{1, 1}; // DECSC (Save cursor state) + savedCursor_.position = {}; // DECSC (Save cursor state) setMode(DECMode::VisibleCursor, true); // DECTCEM (Text cursor enable) setMode(DECMode::Origin, false); // DECOM setMode(AnsiMode::KeyboardAction, false); // KAM setMode(DECMode::AutoWrap, false); // DECAWM setMode(AnsiMode::Insert, false); // IRM setMode(DECMode::UseApplicationCursorKeys, false); // DECCKM (Cursor keys) - setTopBottomMargin(1, unbox(size_.lines)); // DECSTBM - setLeftRightMargin(1, unbox(size_.columns)); // DECRLM + setTopBottomMargin({}, size_.lines.as() - LineOffset(1)); // DECSTBM + setLeftRightMargin({}, size_.columns.as() - ColumnOffset(1)); // DECRLM #if defined(LIBTERMINAL_HYPERLINKS) currentHyperlink_ = {}; @@ -833,13 +887,12 @@ void Screen::resetHard() activeGrid_ = &primaryGrid(); cursor_ = {}; - updateCursorIterators(); lastCursorPosition_ = cursor_.position; margin_ = Margin{ - Margin::Range{1, unbox(size_.lines)}, - Margin::Range{1, unbox(size_.columns)} + Margin::Vertical{{}, size_.lines.as()}, + Margin::Horizontal{{}, size_.columns.as()} }; #if defined(LIBTERMINAL_HYPERLINKS) @@ -850,11 +903,19 @@ void Screen::resetHard() eventListener_.hardReset(); } -void Screen::moveCursorTo(Coordinate to) +void Screen::moveCursorTo(LineOffset _line, ColumnOffset _column) { - wrapPending_ = 0; - cursor_.position = clampToScreen(toRealCoordinate(to)); - updateCursorIterators(); + auto const [line, column] = [&]() + { + if (!cursor_.originMode) + return pair{_line, _column}; + else + return pair{_line + margin_.vertical.from, _column + margin_.horizontal.from}; + }(); + + wrapPending_ = false; + cursor_.position.row = clampedLine(line); + cursor_.position.column = clampedColumn(column); } void Screen::setBuffer(ScreenType _type) @@ -881,71 +942,48 @@ void Screen::setBuffer(ScreenType _type) } } -void Screen::linefeed(int _newColumn) +void Screen::linefeed(ColumnOffset _newColumn) { - wrapPending_ = 0; + wrapPending_ = false; + cursor_.position.column = _newColumn; - if (realCursorPosition().row == margin_.vertical.to || - realCursorPosition().row == unbox(size_.lines)) + if (*realCursorPosition().row == *margin_.vertical.to) { - scrollUp(LineCount(1)); - moveCursorTo({cursorPosition().row, _newColumn}); + activeGrid_->scrollUp(LineCount(1), cursor_.graphicsRendition, margin_); } else { - // using moveCursorTo() would embrace code reusage, but due to the fact that it's fully recalculating iterators, + // using moveCursorTo() would embrace code reusage, + // but due to the fact that it's fully recalculating iterators, // it may be faster to just incrementally update them. // moveCursorTo({cursorPosition().row + 1, margin_.horizontal.from}); cursor_.position.row++; - cursor_.position.column = _newColumn; - currentLine_++; } } void Screen::scrollUp(LineCount _n, Margin const& _margin) { grid().scrollUp(_n, cursor().graphicsRendition, _margin); - updateCursorIterators(); } void Screen::scrollDown(LineCount _n, Margin const& _margin) { grid().scrollDown(_n, cursor().graphicsRendition, _margin); - updateCursorIterators(); } -void Screen::setCurrentColumn(ColumnPosition _n) +void Screen::setCurrentColumn(ColumnOffset _n) { auto const col = cursor_.originMode - ? ColumnPosition::cast_from(margin_.horizontal.from + *_n - 1) + ? margin_.horizontal.from + _n : _n; - auto const clampedCol = min(col, boxed_cast(size_.columns)); - cursor_.position.column = *clampedCol; + auto const clampedCol = min(col, boxed_cast(size_.columns) - 1); + wrapPending_ = false; + cursor_.position.column = clampedCol; } -string Screen::renderText() const +string Screen::renderMainPageText() const { - return grid().renderText(); -} - -string Screen::renderTextLine(int row) const -{ - return grid().renderTextLine(row); -} - -string Screen::renderHistoryTextLine(int _lineNumberIntoHistory) const -{ - assert(1 <= _lineNumberIntoHistory && _lineNumberIntoHistory <= unbox(historyLineCount())); - string line; - line.reserve(*size_.columns); - - for (Cell const& cell : grid().lineAt(1 - _lineNumberIntoHistory)) - if (cell.codepointCount()) - line += cell.toUtf8(); - else - line += ' '; // fill character - - return line; + return grid().renderMainPageText(); } // }}} @@ -960,7 +998,8 @@ void Screen::linefeed() void Screen::backspace() { - moveCursorTo({cursorPosition().row, cursorPosition().column > 1 ? cursorPosition().column - 1 : 1}); + if (cursor_.position.column.value) + cursor_.position.column--; } void Screen::deviceStatusReport() @@ -970,13 +1009,13 @@ void Screen::deviceStatusReport() void Screen::reportCursorPosition() { - reply("\033[{};{}R", cursorPosition().row, cursorPosition().column); + reply("\033[{};{}R", cursorPosition().row + 1, cursorPosition().column + 1); } void Screen::reportExtendedCursorPosition() { auto const pageNum = 1; - reply("\033[{};{};{}R", cursorPosition().row, cursorPosition().column, pageNum); + reply("\033[{};{};{}R", cursorPosition().row + 1, cursorPosition().column + 1, pageNum); } void Screen::selectConformanceLevel(VTType _level) @@ -1048,34 +1087,37 @@ void Screen::sendTerminalId() void Screen::clearToEndOfScreen() { #if defined(LIBTERMINAL_HYPERLINKS) - if (isAlternateScreen() && cursor_.position.row == 1 && cursor_.position.column == 1) + if (isAlternateScreen() + && cursor_.position.row == LineOffset(0) + && cursor_.position.column == ColumnOffset(0)) hyperlinks_.clear(); #endif clearToEndOfLine(); - for_each( - LIBTERMINAL_EXECUTION_COMMA(par) - next(currentLine_), - end(grid().mainPage()), - [&](Line& line) { - fill(begin(line), end(line), Cell{{}, cursor_.graphicsRendition}); - } - ); + for (Line& line: grid().lines(LineOffset(0), size_.lines)) + line.reset(cursor_.graphicsRendition); + + auto i = grid().iteratorAt(cursor_.position.row + LineOffset(1), ColumnOffset(0)); + auto e = grid().iteratorAt(size_.lines.as() - LineOffset(1), + size_.columns.as()); + while (i != e) + { + (*i++).reset(cursor_.graphicsRendition); + } } void Screen::clearToBeginOfScreen() { clearToBeginOfLine(); - for_each( - LIBTERMINAL_EXECUTION_COMMA(par) - begin(grid().mainPage()), - currentLine_, - [&](Line& line) { - fill(begin(line), end(line), Cell{{}, cursor_.graphicsRendition}); - } - ); + auto e = grid().iteratorAt(cursor_.position.row - LineOffset(1), size_.columns.as()); + auto i = grid().iteratorAt(LineOffset(0), ColumnOffset(0)); + + while (i != e) + { + (*i++).reset(cursor_.graphicsRendition); + } } void Screen::clearScreen() @@ -1098,48 +1140,45 @@ void Screen::eraseCharacters(ColumnCount _n) // Spec: https://vt100.net/docs/vt510-rm/ECH.html // It's not clear from the spec how to perform erase when inside margin and number of chars to be erased would go outside margins. // TODO: See what xterm does ;-) + + // erase characters from current colum to the right size_t const n = min( - unbox(size_.columns) - realCursorPosition().column + 1, - *_n == 0 ? 1 : unbox(_n)); - fill_n(currentColumn(), n, Cell{{}, cursor_.graphicsRendition}); + *size_.columns - *realCursorPosition().column, + *_n == 0 ? 1 : *_n + ); + fill_n(currentCellIterator(), n, Cell{cursor_.graphicsRendition}); + + // Shrink column count for currentLine + currentLine().setSize(ColumnCount::cast_from(currentLine().end_offset() - n)); } void Screen::clearToEndOfLine() { - fill( - currentColumn(), - end(*currentLine_), - Cell{{}, cursor_.graphicsRendition} - ); + for (Cell& cell: grid().lineBuffer(cursor_.position.row.as()).subspan(cursor_.position.column.as())) + cell.reset(cursor_.graphicsRendition); } void Screen::clearToBeginOfLine() { - fill( - begin(*currentLine_), - next(currentColumn()), - Cell{{}, cursor_.graphicsRendition} - ); + for (Cell& cell: grid().lineBuffer(cursor_.position.row.as()).subspan(0, cursor_.position.column.as() + 1)) + cell.reset(cursor_.graphicsRendition); } void Screen::clearLine() { - fill( - begin(*currentLine_), - end(*currentLine_), - Cell{{}, cursor_.graphicsRendition} - ); + for (Cell& cell: grid().lineBuffer(cursor_.position.row.as())) + cell.reset(cursor_.graphicsRendition); } void Screen::moveCursorToNextLine(LineCount _n) { - moveCursorTo({cursorPosition().row + unbox(_n), 1}); + moveCursorTo(cursorPosition().row + _n.as(), ColumnOffset(0)); } void Screen::moveCursorToPrevLine(LineCount _n) { - auto const n = min(unbox(_n), cursorPosition().row - 1); - moveCursorTo({cursorPosition().row - n, 1}); + auto const n = min(_n.as(), cursorPosition().row); + moveCursorTo(cursorPosition().row - n, ColumnOffset(0)); } void Screen::insertCharacters(ColumnCount _n) @@ -1149,17 +1188,16 @@ void Screen::insertCharacters(ColumnCount _n) } /// Inserts @p _n characters at given line @p _lineNo. -void Screen::insertChars(int _lineNo, ColumnCount _n) +void Screen::insertChars(LineOffset _lineNo, ColumnCount _n) { auto const n = min( - unbox(_n), - margin_.horizontal.to - cursorPosition().column + 1 + *_n, + *margin_.horizontal.to - *cursorPosition().column + 1 ); - auto && line = grid().lineAt(_lineNo); - auto column0 = next(begin(line), realCursorPosition().column - 1); - auto column1 = next(begin(line), margin_.horizontal.to - n); - auto column2 = next(begin(line), margin_.horizontal.to); + auto column0 = grid().iteratorAt(_lineNo, realCursorPosition().column); + auto column1 = grid().iteratorAt(_lineNo, margin_.horizontal.to - ColumnOffset::cast_from(n - 1)); + auto column2 = grid().iteratorAt(_lineNo, margin_.horizontal.to + ColumnOffset(1)); rotate( column0, @@ -1167,11 +1205,10 @@ void Screen::insertChars(int _lineNo, ColumnCount _n) column2 ); - fill_n( - columnIteratorAt(begin(line), cursor_.position.column), - n, - Cell{L' ', cursor_.graphicsRendition} - ); + for (Cell& cell: grid().lineBuffer(_lineNo).subspan(cursor_.position.column.as(), n)) + { + cell.reset(cursor_.graphicsRendition, L' '); + } } void Screen::insertLines(LineCount _n) @@ -1191,7 +1228,7 @@ void Screen::insertLines(LineCount _n) void Screen::insertColumns(ColumnCount _n) { if (isCursorInsideMargins()) - for (int lineNo = margin_.vertical.from; lineNo <= margin_.vertical.to; ++lineNo) + for (auto lineNo = margin_.vertical.from; lineNo <= margin_.vertical.to; ++lineNo) insertChars(lineNo, _n); } @@ -1231,13 +1268,11 @@ void Screen::copyArea(int _top, int _left, int _bottom, int _right, int _page, { for (auto x = x0; x != xEnd; x += xInc) { - Cell const& sourceCell = at({_top + y, _left + x}); - Cell& targetCell = at({_targetTop + y, _targetLeft + x}); + Cell const& sourceCell = at(LineOffset::cast_from(_top + y), ColumnOffset::cast_from(_left + x)); + Cell& targetCell = at(LineOffset::cast_from(_targetTop + y), ColumnOffset::cast_from(_targetLeft + x)); targetCell = sourceCell; } } - - updateCursorIterators(); } void Screen::eraseArea(int _top, int _left, int _bottom, int _right) @@ -1250,14 +1285,9 @@ void Screen::eraseArea(int _top, int _left, int _bottom, int _right) for (int y = _top; y <= _bottom; ++y) { - Line& line = grid().lineAt(y); - auto column = next(begin(line), _left - 1); - for (int x = _left; x <= _right; ++x) + for (Cell& cell: grid().lineBuffer(LineOffset::cast_from(y)).subspan(_left - 1, _right - _left)) { - Cell& cell = *column; - cell.reset(); - cell.setCharacter(0x20); - ++column; + cell.reset({}, L' '); } } } @@ -1270,14 +1300,11 @@ void Screen::fillArea(char32_t _ch, int _top, int _left, int _bottom, int _right for (int y = _top; y <= _bottom; ++y) { - Line& line = grid().lineAt(y); - auto column = next(begin(line), _left - 1); - for (int x = _left; x <= _right; ++x) + auto const count = _right - _left; + for (Cell& cell: grid().lineBuffer(LineOffset::cast_from(y)) + .subspan(_left, _right - _left + 1)) { - Cell& cell = *column; - cell.reset(cursor().graphicsRendition); - cell.setCharacter(_ch); - ++column; + cell.reset(cursor().graphicsRendition, _ch); } } } @@ -1299,37 +1326,31 @@ void Screen::deleteLines(LineCount _n) void Screen::deleteCharacters(ColumnCount _n) { if (isCursorInsideMargins() && *_n != 0) - deleteChars(realCursorPosition().row, _n); + deleteChars(realCursorPosition().row, realCursorPosition().column, _n); } -void Screen::deleteChars(int _lineNo, ColumnCount _n) +void Screen::deleteChars(LineOffset _line, ColumnOffset _column, ColumnCount _n) { - auto line = next(begin(grid().mainPage()), _lineNo - 1); - auto column = next(begin(*line), realCursorPosition().column - 1); - auto rightMargin = next(begin(*line), margin_.horizontal.to); - auto const n = min(unbox(_n), static_cast(distance(column, rightMargin))); + gsl::span lineBuffer = grid().lineBuffer(_line); - rotate( - column, - next(column, n), - rightMargin - ); + Cell* left = lineBuffer.data() + _column.as(); + Cell* right = lineBuffer.data() + *margin_.horizontal.to + 1; + long const n = min(_n.as(), static_cast(distance(left, right))); + Cell* mid = left + n; - updateCursorIterators(); + rotate(left, mid, right); - rightMargin = next(begin(*line), margin_.horizontal.to); - - fill( - prev(rightMargin, n), - rightMargin, - Cell{L' ', cursor_.graphicsRendition} - ); + for (Cell& cell: gsl::make_span(right - n, right)) + { + cell.reset(cursor_.graphicsRendition, L' '); + } } + void Screen::deleteColumns(ColumnCount _n) { if (isCursorInsideMargins()) - for (int lineNo = margin_.vertical.from; lineNo <= margin_.vertical.to; ++lineNo) - deleteChars(lineNo, _n); + for (auto lineNo = margin_.vertical.from; lineNo <= margin_.vertical.to; ++lineNo) + deleteChars(lineNo, realCursorPosition().column, _n); } void Screen::horizontalTabClear(HorizontalTabClear _which) @@ -1379,67 +1400,59 @@ void Screen::hyperlink(string const& _id, string const& _uri) void Screen::moveCursorUp(LineCount _n) { auto const n = min( - unbox(_n), + _n.as(), cursorPosition().row > margin_.vertical.from ? cursorPosition().row - margin_.vertical.from - : cursorPosition().row - 1 + : cursorPosition().row ); cursor_.position.row -= n; - currentLine_ = prev(currentLine_, n); - setCurrentColumn(ColumnPosition::cast_from(cursorPosition().column)); + setCurrentColumn(cursorPosition().column); } void Screen::moveCursorDown(LineCount _n) { auto const currentLineNumber = cursorPosition().row; auto const n = min( - unbox(_n), + _n.as(), currentLineNumber <= margin_.vertical.to ? margin_.vertical.to - currentLineNumber - : unbox(size_.lines) - currentLineNumber + : (size_.lines.as() - 1) - currentLineNumber ); - // auto const n = - // v.n > margin_.vertical.to - // ? min(v.n, size_.lines - cursorPosition().row) - // : min(v.n, margin_.vertical.to - cursorPosition().row); cursor_.position.row += n; - currentLine_ = next(currentLine_, n); - setCurrentColumn(ColumnPosition(cursorPosition().column)); + setCurrentColumn(cursorPosition().column); } void Screen::moveCursorForward(ColumnCount _n) { - auto const n = min(unbox(_n), margin_.horizontal.length() - cursor_.position.column); - cursor_.position.column += n; + cursor_.position.column = min(cursor_.position.column + _n.as(), + margin_.horizontal.to); } void Screen::moveCursorBackward(ColumnCount _n) { // even if you move to 80th of 80 columns, it'll first write a char and THEN flag wrap pending - wrapPending_ = 0; + wrapPending_ = false; // TODO: skip cells that in counting when iterating backwards over a wide cell (such as emoji) - auto const n = min(unbox(_n), static_cast(cursor_.position.column - 1)); - setCurrentColumn(ColumnPosition(cursor_.position.column - n)); + auto const n = min(_n.as(), cursor_.position.column); + setCurrentColumn(cursor_.position.column - n); } -void Screen::moveCursorToColumn(ColumnPosition _column) +void Screen::moveCursorToColumn(ColumnOffset _column) { - wrapPending_ = 0; setCurrentColumn(_column); } void Screen::moveCursorToBeginOfLine() { - wrapPending_ = 0; - setCurrentColumn(ColumnPosition(1)); + setCurrentColumn(ColumnOffset(0)); } -void Screen::moveCursorToLine(LinePosition _row) +void Screen::moveCursorToLine(LineOffset _row) { - moveCursorTo(Coordinate{unbox(_row), cursor_.position.column}); + moveCursorTo(_row, cursor_.position.column); } void Screen::moveCursorToNextTab() @@ -1451,26 +1464,26 @@ void Screen::moveCursorToNextTab() { // advance to the next tab size_t i = 0; - while (i < tabs_.size() && realCursorPosition().column >= *tabs_[i]) + while (i < tabs_.size() && realCursorPosition().column >= tabs_[i]) ++i; auto const currentCursorColumn = cursorPosition().column; if (i < tabs_.size()) - moveCursorForward(ColumnCount(*tabs_[i] - currentCursorColumn)); + moveCursorForward((tabs_[i] - currentCursorColumn).as()); else if (realCursorPosition().column < margin_.horizontal.to) - moveCursorForward(ColumnCount(margin_.horizontal.to - currentCursorColumn)); + moveCursorForward((margin_.horizontal.to - currentCursorColumn).as()); else moveCursorToNextLine(LineCount(1)); } - else if (tabWidth_) + else if (tabWidth_.value) { // default tab settings if (realCursorPosition().column < margin_.horizontal.to) { auto const n = min( - ColumnCount(tabWidth_ - (cursor_.position.column - 1) % tabWidth_), - size_.columns - ColumnCount(cursorPosition().column) + (tabWidth_ - cursor_.position.column.as() % tabWidth_), + size_.columns - cursorPosition().column.as() ); moveCursorForward(n); } @@ -1482,7 +1495,7 @@ void Screen::moveCursorToNextTab() // no tab stops configured if (realCursorPosition().column < margin_.horizontal.to) // then TAB moves to the end of the screen - moveCursorToColumn(ColumnPosition(margin_.horizontal.to)); + moveCursorToColumn(margin_.horizontal.to); else // then TAB moves to next line left margin moveCursorToNextLine(LineCount(1)); @@ -1503,43 +1516,40 @@ void Screen::captureBuffer(int _lineCount, bool _logicalLines) auto writer = VTWriter([&](auto buf, auto len) { capturedBuffer += string_view(buf, len); }); // TODO: when capturing _lineCount < screenSize.lines, start at the lowest non-empty line. - auto const relativeStartLine = _logicalLines ? grid().computeRelativeLineNumberFromBottom(_lineCount) - : unbox(size_.lines) - _lineCount + 1; + auto const relativeStartLine = _logicalLines + ? grid().computeLogicalLineNumberFromBottom(LineCount::cast_from(_lineCount)) + : unbox(size_.lines) - _lineCount; auto const startLine = clamp( - 1 - unbox(historyLineCount()), + -unbox(historyLineCount()), relativeStartLine, unbox(size_.lines)); // dumpState(); - auto const lineCount = unbox(size_.lines) - startLine + 1; - auto const trimSpaceRight = [](string& value) { while (!value.empty() && value.back() == ' ') value.pop_back(); }; - for (int const row : crispy::times(startLine, lineCount)) + for (LineOffset row = LineOffset(startLine); row < size_.lines.as(); ++row) { - auto const& lineBuffer = grid().lineAt(row); - - if (_logicalLines && lineBuffer.wrapped() && !capturedBuffer.empty()) + if (_logicalLines && grid().lineAt(row).wrapped() && !capturedBuffer.empty()) capturedBuffer.pop_back(); - if (!lineBuffer.blank()) + if (grid().isLineBlank(row)) + continue; + + for (ColumnOffset col = ColumnOffset{0}; col < size_.columns.as(); ++col) { - for (int const col : crispy::times(1, unbox(size_.columns))) - { - Cell const& cell = at({row, col}); - if (!cell.codepointCount()) - writer.write(U' '); - else - for (char32_t const ch : cell.codepoints()) - writer.write(ch); - } - trimSpaceRight(capturedBuffer); + Cell const& cell = at({row, col}); + if (!cell.codepointCount()) + writer.write(U' '); + else + for (char32_t const ch : cell.codepoints()) + writer.write(ch); } + trimSpaceRight(capturedBuffer); writer.write('\n'); } @@ -1574,8 +1584,8 @@ void Screen::cursorBackwardTab(TabStopCount _count) for (unsigned k = 0; k < unbox(_count); ++k) { auto const i = std::find_if(rbegin(tabs_), rend(tabs_), - [&](ColumnPosition tabPos) -> bool { - return *tabPos <= cursorPosition().column - 1; + [&](ColumnOffset tabPos) -> bool { + return tabPos < cursorPosition().column; }); if (i != rend(tabs_)) { @@ -1584,22 +1594,22 @@ void Screen::cursorBackwardTab(TabStopCount _count) } else { - moveCursorToColumn(ColumnPosition(margin_.horizontal.from)); + moveCursorToColumn(margin_.horizontal.from); break; } } } - else if (tabWidth_) + else if (tabWidth_.value) { // default tab settings - if (cursor_.position.column <= tabWidth_) + if (*cursor_.position.column < *tabWidth_) moveCursorToBeginOfLine(); else { - auto const m = cursor_.position.column % tabWidth_; + auto const m = (*cursor_.position.column + 1) % *tabWidth_; auto const n = m - ? (*_count - 1) * tabWidth_ + m - : *_count * tabWidth_ + m; + ? (*_count - 1) * *tabWidth_ + m + : *_count * *tabWidth_ + m; moveCursorBackward(ColumnCount(n - 1)); } } @@ -1612,18 +1622,18 @@ void Screen::cursorBackwardTab(TabStopCount _count) void Screen::index() { - if (realCursorPosition().row == margin_.vertical.to) + if (*realCursorPosition().row == *margin_.vertical.to) scrollUp(LineCount(1)); else - moveCursorTo({cursorPosition().row + 1, cursorPosition().column}); + moveCursorDown(LineCount(1)); } void Screen::reverseIndex() { - if (realCursorPosition().row == margin_.vertical.from) + if (realCursorPosition().row.as() == margin_.vertical.from.as()) scrollDown(LineCount(1)); else - moveCursorTo({cursorPosition().row - 1, cursorPosition().column}); + moveCursorUp(LineCount(1)); } void Screen::backIndex() @@ -1631,15 +1641,15 @@ void Screen::backIndex() if (realCursorPosition().column == margin_.horizontal.from) ;// TODO: scrollRight(1); else - moveCursorTo({cursorPosition().row, cursorPosition().column - 1}); + moveCursorForward(ColumnCount(1)); } void Screen::forwardIndex() { - if (realCursorPosition().column == margin_.horizontal.to) + if (*realCursorPosition().column + 1 == *margin_.horizontal.to) ;// TODO: scrollLeft(1); else - moveCursorTo({cursorPosition().row, cursorPosition().column + 1}); + moveCursorDown(LineCount(1)); } void Screen::setForegroundColor(Color const& _color) @@ -1750,7 +1760,7 @@ void Screen::setGraphicsRendition(GraphicsRendition _rendition) void Screen::setMark() { - currentLine_->setMarked(true); + currentLine().setMarked(true); } void Screen::saveModes(std::vector const& _modes) @@ -1784,7 +1794,7 @@ void Screen::setMode(DECMode _mode, bool _enable) case DECMode::LeftRightMargin: // Resetting DECLRMM also resets the horizontal margins back to screen size. if (!_enable) - margin_.horizontal = {1, unbox(size_.columns)}; + margin_.horizontal = {{}, size_.columns.as()}; break; case DECMode::Origin: cursor_.originMode = _enable; @@ -1806,6 +1816,7 @@ void Screen::setMode(DECMode _mode, bool _enable) eventListener_.synchronizedOutput(_enable); break; case DECMode::TextReflow: +#if 0 // TODO(pr) if (isPrimaryScreen()) { if (_enable) @@ -1824,11 +1835,12 @@ void Screen::setMode(DECMode _mode, bool _enable) auto const endLine = LinePosition(numHistLines + numScreenLines); // auto const startLine = LinePosition(*historyLineCount() + realCursorPosition().row - 1); // auto const endLine = LinePosition(*historyLineCount() + *size_.lines); - assert(primaryGrid().lines(startLine, endLine).begin() == currentLine_); + assert(primaryGrid().lines(startLine, endLine).begin() == currentCellIterator()); for (Line& line : primaryGrid().lines(startLine, endLine)) line.setFlag(Line::Flags::Wrappable, _enable); } } +#endif break; case DECMode::DebugLogging: // Since this mode (Xterm extension) does not support finer graind control, @@ -1960,35 +1972,35 @@ void Screen::requestDECMode(int _mode) reply("\033[?{};{}$y", code, static_cast(modeResponse)); } -void Screen::setTopBottomMargin(optional _top, optional _bottom) +void Screen::setTopBottomMargin(optional _top, optional _bottom) { auto const bottom = _bottom.has_value() - ? min(_bottom.value(), unbox(size_.lines)) - : unbox(size_.lines); + ? min(_bottom.value(), size_.lines.as() - LineOffset(1)) + : size_.lines.as() - LineOffset(1); - auto const top = _top.value_or(1); + auto const top = _top.value_or(LineOffset(0)); if (top < bottom) { margin_.vertical.from = top; margin_.vertical.to = bottom; - moveCursorTo({1, 1}); + moveCursorTo({}, {}); } } -void Screen::setLeftRightMargin(optional _left, optional _right) +void Screen::setLeftRightMargin(optional _left, optional _right) { if (isModeEnabled(DECMode::LeftRightMargin)) { auto const right = _right.has_value() - ? min(_right.value(), unbox(size_.columns)) - : unbox(size_.columns); - auto const left = _left.value_or(1); - if (left + 1 < right) + ? min(_right.value(), size_.columns.as() - ColumnOffset(1)) + : size_.columns.as() - ColumnOffset(1); + auto const left = _left.value_or(ColumnOffset(0)); + if (left < right) { margin_.horizontal.from = left; margin_.horizontal.to = right; - moveCursorTo({1, 1}); + moveCursorTo({}, {}); } } } @@ -1996,27 +2008,19 @@ void Screen::setLeftRightMargin(optional _left, optional _right) void Screen::screenAlignmentPattern() { // sets the margins to the extremes of the page - margin_.vertical.from = 1; - margin_.vertical.to = *size_.lines; - margin_.horizontal.from = 1; - margin_.horizontal.to = *size_.columns; + margin_.vertical.from = LineOffset(0); + margin_.vertical.to = size_.lines.as() - LineOffset(1); + margin_.horizontal.from = ColumnOffset(0); + margin_.horizontal.to = size_.columns.as() - ColumnOffset(1); // and moves the cursor to the home position - moveCursorTo({1, 1}); + moveCursorTo({}, {}); // fills the complete screen area with a test pattern - crispy::for_each( - LIBTERMINAL_EXECUTION_COMMA(par) - grid().mainPage(), - [&](Line& line) { - fill( - LIBTERMINAL_EXECUTION_COMMA(par) - begin(line), - end(line), - Cell{'E', cursor_.graphicsRendition} - ); - } - ); + for (Cell& cell: grid().mainPage()) + { + cell.reset(cursor_.graphicsRendition, L'E'); + } } void Screen::sendMouseEvents(MouseProtocol _protocol, bool _enable) @@ -2044,16 +2048,16 @@ void Screen::singleShiftSelect(CharsetTable _table) void Screen::sixelImage(ImageSize _pixelSize, Image::Data&& _data) { - auto const columnCount = ColumnCount(unsigned(ceilf(float(*_pixelSize.width) / float(*cellPixelSize_.width)))); - auto const lineCount = LineCount(unsigned(ceilf(float(*_pixelSize.height) / float(*cellPixelSize_.height)))); + auto const columnCount = ColumnCount::cast_from(ceilf(float(*_pixelSize.width) / float(*cellPixelSize_.width))); + auto const lineCount = LineCount::cast_from(ceilf(float(*_pixelSize.height) / float(*cellPixelSize_.height))); auto const extent = GridSize{lineCount, columnCount}; auto const sixelScrolling = isModeEnabled(DECMode::SixelScrolling); - auto const topLeft = sixelScrolling ? cursorPosition() : Coordinate{1, 1}; + auto const topLeft = sixelScrolling ? cursorPosition() : Coordinate{}; auto const alignmentPolicy = ImageAlignment::TopStart; auto const resizePolicy = ImageResize::NoResize; - auto const imageOffset = Coordinate{0, 0}; + auto const imageOffset = Coordinate{}; auto const imageSize = _pixelSize; if (auto const imageRef = uploadImage(ImageFormat::RGBA, _pixelSize, move(_data)); imageRef) @@ -2078,6 +2082,7 @@ void Screen::renderImage(std::shared_ptr const& _imageRef, ImageResize _resizePolicy, bool _autoScroll) { +#if 0 // TODO(pr) // TODO: make use of _imageOffset and _imageSize (void) _imageOffset; (void) _imageSize; @@ -2087,9 +2092,9 @@ void Screen::renderImage(std::shared_ptr const& _imageRef, (void) _alignmentPolicy; (void) _autoScroll; #else - auto const linesAvailable = LineCount(1 + *size_.lines - _topLeft.row); + auto const linesAvailable = LineCount(1) + size_.lines - _topLeft.row.as(); auto const linesToBeRendered = min(_gridSize.lines, linesAvailable); - auto const columnsToBeRendered = ColumnCount(min(*_gridSize.columns, *size_.columns - _topLeft.column - 1)); + auto const columnsToBeRendered = ColumnCount(min(_gridSize.columns, size_.columns - _topLeft.column.as())); auto const gapColor = RGBAColor{}; // TODO: cursor_.graphicsRendition.backgroundColor; // TODO: make use of _imageOffset and _imageSize @@ -2111,11 +2116,11 @@ void Screen::renderImage(std::shared_ptr const& _imageRef, auto const cellCoord = Coordinate{ _topLeft.row + *offset.lines, _topLeft.column + *offset.columns}; - Cell& cell = at(cellCoord); + Cell& cell = at(cellCoord.row, cellCoord.column); cell.setImage( ImageFragment{ rasterizedImage, - Coordinate(*offset.lines, *offset.columns) + Coordinate{offset.lines.as(), offset.columns.as()} } ); #if defined(LIBTERMINAL_HYPERLINKS) @@ -2134,7 +2139,7 @@ void Screen::renderImage(std::shared_ptr const& _imageRef, for (auto const lineOffset : crispy::times(*remainingLineCount)) { linefeed(); - moveCursorForward(ColumnCount(_topLeft.column)); + moveCursorForward(_topLeft.column.as()); crispy::for_each( LIBTERMINAL_EXECUTION_COMMA(par) crispy::times(unbox(columnsToBeRendered)), @@ -2153,7 +2158,8 @@ void Screen::renderImage(std::shared_ptr const& _imageRef, } // move ansi text cursor to position of the sixel cursor - moveCursorToColumn(ColumnPosition(_topLeft.column + unbox(_gridSize.columns))); + moveCursorToColumn(_topLeft.column + _gridSize.columns.as()); +#endif #endif } @@ -2312,9 +2318,9 @@ void Screen::requestStatusString(RequestStatusString _value) #endif return nullopt; case RequestStatusString::DECSTBM: - return fmt::format("{};{}r", margin_.vertical.from, margin_.vertical.to); + return fmt::format("{};{}r", 1 + *margin_.vertical.from, *margin_.vertical.to); case RequestStatusString::DECSLRM: - return fmt::format("{};{}s", margin_.horizontal.from, margin_.horizontal.to); + return fmt::format("{};{}s", 1 + *margin_.horizontal.from, *margin_.horizontal.to); case RequestStatusString::DECSCPP: // EXTENSION: Usually DECSCPP only knows about 80 and 132, but we take any. return fmt::format("{}|$", size_.columns); @@ -2350,13 +2356,13 @@ void Screen::requestTabStops() { if (i) dcs << '/'; - dcs << *tabs_[i]; + dcs << *tabs_[i] + 1; } } - else if (tabWidth_ != 0) + else if (*tabWidth_ != 0) { dcs << 1; - for (int column = tabWidth_ + 1; column <= *size_.columns; column += tabWidth_) + for (auto column = *tabWidth_ + 1; column <= *size_.columns; column += *tabWidth_) dcs << '/' << column; } dcs << "\033\\"sv; // ST @@ -2511,10 +2517,12 @@ void Screen::dumpState(std::string const& _message, std::ostream& _os) const _os << fmt::format("horizontal margins : {}\n", margin_.horizontal); hline(); +#if 0 // TODO(pr) _os << screenshot([this](int _lineNo) -> string { //auto const absoluteLine = grid().toAbsoluteLine(_lineNo); return fmt::format("| {:>4}: {}", _lineNo, grid().lineAt(_lineNo).flags()); }); +#endif hline(); // TODO: print more useful debug information @@ -2523,7 +2531,6 @@ void Screen::dumpState(std::string const& _message, std::ostream& _os) const // - top/down margin // - cursor position // - autoWrap - // - wrapPending // - ... other output related modes } diff --git a/src/terminal/Screen.h b/src/terminal/Screen.h index e619297e0c..a33b8d05ce 100644 --- a/src/terminal/Screen.h +++ b/src/terminal/Screen.h @@ -106,7 +106,7 @@ class Modes { /// NB: Take care what to store here, as DECSC/DECRC will save/restore this struct. struct Cursor { - Coordinate position{1, 1}; + Coordinate position{LineOffset(0), ColumnOffset(0)}; bool autoWrap = true; // false; bool originMode = false; bool visible = true; @@ -141,7 +141,7 @@ class Screen : public capabilities::StaticDatabase { ScreenEvents& _eventListener, bool _logRaw = false, bool _logTrace = false, - std::optional _maxHistoryLineCount = std::nullopt, + LineCount _maxHistoryLineCount = LineCount(0), ImageSize _maxImageSize = ImageSize{Width(800), Height(600)}, int _maxImageColorRegisters = 256, bool _sixelCursorConformance = true, @@ -173,8 +173,8 @@ class Screen : public capabilities::StaticDatabase { terminalId_ = _id; } - void setMaxHistoryLineCount(std::optional _maxHistoryLineCount); - std::optional maxHistoryLineCount() const noexcept { return grid().maxHistoryLineCount(); } + void setMaxHistoryLineCount(LineCount _maxHistoryLineCount); + LineCount maxHistoryLineCount() const noexcept { return grid().maxHistoryLineCount(); } LineCount historyLineCount() const noexcept { return grid().historyLineCount(); } @@ -187,16 +187,13 @@ class Screen : public capabilities::StaticDatabase { /// Renders the full screen by passing every grid cell to the callback. template - void render(Renderer&& _render, std::optional _scrollOffset = std::nullopt) const + void render(Renderer&& _render, ScrollOffset _scrollOffset = {}) const { activeGrid_->render(std::forward(_render), _scrollOffset); } - /// Renders a single text line. - std::string renderTextLine(int _row) const; - /// Renders the full screen as text into the given string. Each line will be terminated by LF. - std::string renderText() const; + std::string renderMainPageText() const; /// Takes a screenshot by outputting VT sequences needed to render the current state of the screen. /// @@ -247,8 +244,8 @@ class Screen : public capabilities::StaticDatabase { void moveCursorDown(LineCount _n); // CUD void moveCursorForward(ColumnCount _n); // CUF void moveCursorToBeginOfLine(); // CR - void moveCursorToColumn(ColumnPosition _n); // CHA - void moveCursorToLine(LinePosition _n); // VPA + void moveCursorToColumn(ColumnOffset _n); // CHA + void moveCursorToLine(LineOffset _n); // VPA void moveCursorToNextLine(LineCount _n); // CNL void moveCursorToNextTab(); // HT void moveCursorToPrevLine(LineCount _n); // CPL @@ -290,8 +287,8 @@ class Screen : public capabilities::StaticDatabase { void setUnderlineColor(Color const& _color); void setCursorStyle(CursorDisplay _display, CursorShape _shape); void setGraphicsRendition(GraphicsRendition _rendition); - void setTopBottomMargin(std::optional _top, std::optional _bottom); - void setLeftRightMargin(std::optional _left, std::optional _right); + void setTopBottomMargin(std::optional _top, std::optional _bottom); + void setLeftRightMargin(std::optional _left, std::optional _right); void screenAlignmentPattern(); void sendMouseEvents(MouseProtocol _protocol, bool _enable); void applicationKeypadMode(bool _enable); @@ -378,29 +375,31 @@ class Screen : public capabilities::StaticDatabase { return realCursorPosition(); else return Coordinate{ - cursor_.position.row - margin_.vertical.from + 1, - cursor_.position.column - margin_.horizontal.from + 1 + cursor_.position.row - margin_.vertical.from, + cursor_.position.column - margin_.horizontal.from }; } - constexpr Coordinate origin() const noexcept { - if (cursor_.originMode) - return {margin_.vertical.from, margin_.horizontal.from}; - else - return {1, 1}; + constexpr Coordinate origin() const noexcept + { + if (!cursor_.originMode) + return {}; + + return { + margin_.vertical.from, + margin_.horizontal.from + }; } Cursor const& cursor() const noexcept { return cursor_; } - int wrapPending() const noexcept { return wrapPending_; } - /// Returns identity if DECOM is disabled (default), but returns translated coordinates if DECOM is enabled. Coordinate toRealCoordinate(Coordinate const& pos) const noexcept { if (!cursor_.originMode) return pos; else - return { pos.row + margin_.vertical.from - 1, pos.column + margin_.horizontal.from - 1 }; + return { pos.row + margin_.vertical.from, pos.column + margin_.horizontal.from }; } /// Clamps given coordinates, respecting DECOM (Origin Mode). @@ -416,61 +415,73 @@ class Screen : public capabilities::StaticDatabase { Coordinate clampToOrigin(Coordinate const& coord) const noexcept { return { - std::clamp(coord.row, int{0}, margin_.vertical.length()), - std::clamp(coord.column, int{0}, margin_.horizontal.length()) + std::clamp(coord.row, LineOffset{0}, margin_.vertical.length().as() - LineOffset(1)), + std::clamp(coord.column, ColumnOffset{0}, margin_.horizontal.length().as() - ColumnOffset(1)) }; } + LineOffset clampedLine(LineOffset _line) const noexcept + { + return std::clamp(_line, LineOffset(0), size_.lines.as() - LineOffset(1)); + } + + ColumnOffset clampedColumn(ColumnOffset _column) const noexcept + { + return std::clamp(_column, ColumnOffset(0), size_.columns.as() - ColumnOffset(1)); + } + Coordinate clampToScreen(Coordinate const& coord) const noexcept { return { - std::clamp(coord.row, int{1}, unbox(size_.lines)), - std::clamp(coord.column, int{1}, unbox(size_.columns)) + clampedLine(coord.row), + clampedColumn(coord.column) }; } // Tests if given coordinate is within the visible screen area. constexpr bool contains(Coordinate _coord) const noexcept { - return 1 <= _coord.row && _coord.row <= unbox(size_.lines) - && 1 <= _coord.column && _coord.column <= unbox(size_.columns); + return LineOffset(0) <= _coord.row && _coord.row < size_.lines.as() + && ColumnOffset(0) <= _coord.column && _coord.column <= size_.columns.as(); } - Cell& lastPosition() noexcept { return grid().at(lastCursorPosition_); } - Cell const& lastPosition() const noexcept { return grid().at(lastCursorPosition_); } + Cell& lastPosition() noexcept { return grid().at(lastCursorPosition_.row, lastCursorPosition_.column); } + Cell const& lastPosition() const noexcept { return grid().at(lastCursorPosition_.row, lastCursorPosition_.column); } - auto currentColumn() noexcept - { - return std::next(currentLine_->begin(), cursor_.position.column - 1); - } + Line& currentLine() { return grid().lineAt(cursor_.position.row); } + Line const& currentLine() const { return grid().lineAt(cursor_.position.row); } - auto currentColumn() const noexcept + decltype(auto) currentCellIterator() noexcept { - return std::next(currentLine_->cbegin(), cursor_.position.column - 1); + return grid().iteratorAt(cursor_.position.row, cursor_.position.column); } - Cell const& currentCell() const noexcept + decltype(auto) currentCellIterator() const noexcept { - return (*currentLine_)[cursor_.position.column - 1]; + return grid().iteratorAt(cursor_.position.row, cursor_.position.column); } Cell& currentCell() noexcept { - return (*currentLine_)[cursor_.position.column - 1]; + return activeGrid_->at(cursor_.position.row, cursor_.position.column); } - Cell& currentCell(Cell value) + Cell const& currentCell() const noexcept { - return (*currentLine_)[cursor_.position.column - 1] = std::move(value); + return activeGrid_->at(cursor_.position.row, cursor_.position.column); } - void moveCursorTo(Coordinate to); + void moveCursorTo(LineOffset _line, ColumnOffset _column); + void moveCursorTo(Coordinate to) { moveCursorTo(to.row, to.column); } // TODO(pr) kill /// Gets a reference to the cell relative to screen origin (top left, 1:1). - Cell& at(Coordinate const& _coord) noexcept { return grid().at(_coord); } + Cell& at(LineOffset _line, ColumnOffset _column) noexcept { return grid().at(_line, _column); } /// Gets a reference to the cell relative to screen origin (top left, 1:1). - Cell const& at(Coordinate const& _coord) const noexcept { return grid().at(_coord); } + Cell const& at(LineOffset _line, ColumnOffset _column) const noexcept { return grid().at(_line, _column); } + + Cell& at(Coordinate p) noexcept { return grid().at(p.row, p.column); } + Cell const& at(Coordinate p) const noexcept { return grid().at(p.row, p.column); } bool isPrimaryScreen() const noexcept { return activeGrid_ == &grids_[0]; } bool isAlternateScreen() const noexcept { return activeGrid_ == &grids_[1]; } @@ -492,16 +503,7 @@ class Screen : public capabilities::StaticDatabase { auto scrollbackLines() const noexcept { return grid().scrollbackLines(); } - void setTabWidth(uint8_t _value) { tabWidth_ = _value; } - - /** - * Returns the n'th saved line into the history scrollback buffer. - * - * @param _lineNumberIntoHistory the 1-based offset into the history buffer. - * - * @returns the textual representation of the n'th line into the history. - */ - std::string renderHistoryTextLine(int _lineNumberIntoHistory) const; + void setTabWidth(ColumnCount _value) { tabWidth_ = _value; } std::string const& windowTitle() const noexcept { return windowTitle_; } @@ -511,7 +513,7 @@ class Screen : public capabilities::StaticDatabase { /// (0..-N) for savedLines area /// @return cursor position relative to screen origin (1, 1), that is, if line Number os >= 1, it's /// in the screen area, and in the savedLines area otherwise. - std::optional findMarkerForward(int _currentCursorLine) const; + std::optional findMarkerDownwards(ScrollOffset _currentCursorLine) const; /// Finds the previous marker right next to the given line position. /// @@ -519,7 +521,7 @@ class Screen : public capabilities::StaticDatabase { /// (0..-N) for savedLines area /// @return cursor position relative to screen origin (1, 1), that is, if line Number os >= 1, it's /// in the screen area, and in the savedLines area otherwise. - std::optional findMarkerBackward(int _currentCursorLine) const; + std::optional findMarkerUpwards(ScrollOffset _currentCursorLine) const; /// ScreenBuffer's type, such as main screen or alternate screen. ScreenType bufferType() const noexcept { return screenType_; } @@ -564,17 +566,8 @@ class Screen : public capabilities::StaticDatabase { /// @returns the primary screen's grid if primary screen is active. Grid& grid() noexcept { return *activeGrid_; } - /// @returns the primary screen's grid if alternate screen is active, and the alternate screen's grid otherwise. - Grid& backgroundGrid() noexcept { return isPrimaryScreen() ? alternateGrid() : primaryGrid(); } - /// @returns true iff given absolute line number is wrapped, false otherwise. - bool lineWrapped(int _lineNumber) const { return activeGrid_->absoluteLineAt(_lineNumber).wrapped(); } - - int toAbsoluteLine(int _relativeLine) const noexcept { return activeGrid_->toAbsoluteLine(_relativeLine); } - Coordinate toAbsolute(Coordinate _coord) const noexcept { return {activeGrid_->toAbsoluteLine(_coord.row), _coord.column}; } - - int toRelativeLine(int _absoluteLine) const noexcept { return activeGrid_->toRelativeLine(_absoluteLine); } - Coordinate toRelative(Coordinate _coord) const noexcept { return {activeGrid_->toRelativeLine(_coord.row), _coord.column}; } + bool isLineWrapped(LineOffset _lineNumber) const noexcept { return activeGrid_->isLineWrapped(_lineNumber); } ColorPalette& colorPalette() noexcept { return colorPalette_; } ColorPalette const& colorPalette() const noexcept { return colorPalette_; } @@ -590,43 +583,20 @@ class Screen : public capabilities::StaticDatabase { void setTabUnderCursor(); /// Applies LF but also moves cursor to given column @p _column. - void linefeed(int _column); + void linefeed(ColumnOffset _column); void writeCharToCurrentAndAdvance(char32_t _codepoint); void clearAndAdvance(int _offset); void fail(std::string const& _message) const; - void updateCursorIterators() - { - currentLine_ = next(begin(grid().mainPage()), cursor_.position.row - 1); - } - - /// @returns an iterator to @p _n columns after column @p _begin. - ColumnIterator columnIteratorAt(ColumnIterator _begin, int _n) - { - return next(_begin, _n - 1); - } - - /// @returns an iterator to the real column number @p _n. - ColumnIterator columnIteratorAt(int _n) - { - return columnIteratorAt(std::begin(*currentLine_), _n); - } - - /// @returns an iterator to the real column number @p _n. - ColumnIterator columnIteratorAt(int _n) const - { - return const_cast(this)->columnIteratorAt(_n); - } - void scrollUp(LineCount n, Margin const& margin); void scrollDown(LineCount n, Margin const& margin); - void insertChars(int _lineNo, ColumnCount _n); - void deleteChars(int _lineNo, ColumnCount _n); + void insertChars(LineOffset _lineNo, ColumnCount _n); + void deleteChars(LineOffset _lineNo, ColumnOffset _column, ColumnCount _count); /// Sets the current column to given logical column number. - void setCurrentColumn(ColumnPosition _n); + void setCurrentColumn(ColumnOffset _n); // private fields // @@ -664,9 +634,8 @@ class Screen : public capabilities::StaticDatabase { // XXX moved from ScreenBuffer Margin margin_; - int wrapPending_ = 0; - uint8_t tabWidth_{8}; - std::vector tabs_; + ColumnCount tabWidth_{8}; + std::vector tabs_; // main/alt screen and history // @@ -683,8 +652,8 @@ class Screen : public capabilities::StaticDatabase { Cursor cursor_; Cursor savedCursor_; Cursor savedPrimaryCursor_; //!< saved cursor of primary-screen when switching to alt-screen. - LineIterator currentLine_; Coordinate lastCursorPosition_; + bool wrapPending_ = false; CursorDisplay cursorDisplay_ = CursorDisplay::Steady; CursorShape cursorShape_ = CursorShape::Block; @@ -708,11 +677,22 @@ class Screen : public capabilities::StaticDatabase { namespace fmt // {{{ { template <> - struct formatter { + struct formatter { + template + constexpr auto parse(ParseContext& ctx) { return ctx.begin(); } + template + auto format(const terminal::Margin::Horizontal range, FormatContext& ctx) + { + return format_to(ctx.out(), "{}..{}", range.from, range.to); + } + }; + + template <> + struct formatter { template constexpr auto parse(ParseContext& ctx) { return ctx.begin(); } template - auto format(const terminal::Margin::Range range, FormatContext& ctx) + auto format(const terminal::Margin::Vertical range, FormatContext& ctx) { return format_to(ctx.out(), "{}..{}", range.from, range.to); } diff --git a/src/terminal/ScreenEvents.h b/src/terminal/ScreenEvents.h index 9b573afb16..7100292885 100644 --- a/src/terminal/ScreenEvents.h +++ b/src/terminal/ScreenEvents.h @@ -67,7 +67,7 @@ class ScreenEvents { virtual void setWindowTitle(std::string_view /*_title*/) {} virtual void useApplicationCursorKeys(bool /*_enabled*/) {} virtual void hardReset() {} - virtual void markRegionDirty(LinePosition _line, ColumnPosition _column) {} + virtual void markRegionDirty(LineOffset _line, ColumnOffset _column) {} virtual void synchronizedOutput(bool _enabled) {} // Invoked by screen buffer when an image is not being referenced by any grid cell anymore. diff --git a/src/terminal/Screen_test.cpp b/src/terminal/Screen_test.cpp index a1f339f173..88d9ae8604 100644 --- a/src/terminal/Screen_test.cpp +++ b/src/terminal/Screen_test.cpp @@ -20,11 +20,51 @@ #include using crispy::Size; +using crispy::escape; using namespace terminal; using namespace std; -namespace +namespace // {{{ { + string mainPageText(Screen const& screen) + { + string text; + for (Cell const& cell: screen.grid().buffer()) + if (cell.codepointCount()) + text += cell.toUtf8(); + else + text += ' '; + return text; + } + + [[maybe_unused]] + void logScreenTextAlways(Screen const& screen, string const& headline = "") + { + fmt::print("{}: cursor={} HM={}..{}\n", + headline.empty() ? "screen dump"s : headline, + screen.realCursorPosition(), + screen.margin().horizontal.from, + screen.margin().horizontal.to + ); + + string text; + for (Cell const& cell: screen.grid().buffer()) + if (cell.codepointCount()) + text += cell.toUtf8(); + else + text += ' '; + fmt::print("raw buffer: '{}'\n", text); + + for (auto const row : ranges::views::iota(-screen.historyLineCount().as(), screen.size().lines.as())) + { + terminal::Line const& lineAttribs = screen.grid().lineAt(LineOffset(row)); + + fmt::print("[{}] \"{}\" | {}\n", + row, screen.grid().renderTextLine(LineOffset::cast_from(row)), + lineAttribs.flags()); + } + } + void logScreenText(Screen const& screen, string const& headline = "") { if (headline.empty()) @@ -32,113 +72,138 @@ namespace else UNSCOPED_INFO(headline + ":"); - for (auto const row : ranges::views::iota(1, *screen.size().lines)) - UNSCOPED_INFO(fmt::format("[{}] \"{}\"", row, screen.renderTextLine(static_cast(row)))); + for (auto const row : ranges::views::iota(0, *screen.size().lines)) + UNSCOPED_INFO(fmt::format("[{}] \"{}\"", row, screen.grid().renderTextLine(LineOffset::cast_from(row)))); } class MockScreen : public MockScreenEvents, public Screen { public: [[deprecated]] explicit MockScreen(crispy::Size _size): - MockScreen(PageSize{ LineCount(_size.height), ColumnCount(_size.width) }) + MockScreen(PageSize{LineCount(_size.height), ColumnCount(_size.width) }) { - grid().setReflowOnResize(false); } - explicit MockScreen(PageSize _size): Screen{_size, *this} + explicit MockScreen(PageSize _size, + LineCount _maxHistoryLineCount = {}): + Screen{ + _size, + *this, + false, // log raw + false, // log trace + _maxHistoryLineCount + } { grid().setReflowOnResize(false); } }; - auto e(string const& s) + decltype(auto) e(string const& s) { return crispy::escape(s); } } +// }}} TEST_CASE("Screen.isLineVisible", "[screen]") { - auto screen = MockScreen{PageSize{LineCount(1), ColumnCount(2)}}; + auto screen = MockScreen{PageSize{LineCount(1), ColumnCount(2)}, LineCount(5)}; auto viewport = terminal::Viewport{screen}; screen.write("10203040"); - REQUIRE("40" == screen.renderTextLine(1)); - REQUIRE("30" == screen.renderTextLine(0)); - REQUIRE("20" == screen.renderTextLine(-1)); - REQUIRE("10" == screen.renderTextLine(-2)); + CHECK("40" == screen.grid().renderTextLine(LineOffset(0))); + CHECK("30" == screen.grid().renderTextLine(LineOffset(-1))); + CHECK("20" == screen.grid().renderTextLine(LineOffset(-2))); + CHECK("10" == screen.grid().renderTextLine(LineOffset(-3))); - CHECK(viewport.isLineVisible(1)); - CHECK_FALSE(viewport.isLineVisible(0)); - CHECK_FALSE(viewport.isLineVisible(-1)); - CHECK_FALSE(viewport.isLineVisible(-2)); - CHECK_FALSE(viewport.isLineVisible(-3)); // minimal out-of-bounds + CHECK(viewport.isLineVisible(LineOffset{0})); + CHECK_FALSE(viewport.isLineVisible(LineOffset{-1})); + CHECK_FALSE(viewport.isLineVisible(LineOffset{-2})); + CHECK_FALSE(viewport.isLineVisible(LineOffset{-3})); + CHECK_FALSE(viewport.isLineVisible(LineOffset{-4})); // minimal out-of-bounds viewport.scrollUp(LineCount(1)); - CHECK_FALSE(viewport.isLineVisible(1)); - CHECK(viewport.isLineVisible(0)); - CHECK_FALSE(viewport.isLineVisible(-1)); - CHECK_FALSE(viewport.isLineVisible(-2)); + REQUIRE(viewport.scrollOffset() == ScrollOffset(-1)); + CHECK_FALSE(viewport.isLineVisible(LineOffset{0})); + CHECK(viewport.isLineVisible(LineOffset{-1})); + CHECK_FALSE(viewport.isLineVisible(LineOffset{-2})); + CHECK_FALSE(viewport.isLineVisible(LineOffset{-3})); viewport.scrollUp(LineCount(1)); - CHECK_FALSE(viewport.isLineVisible(1)); - CHECK_FALSE(viewport.isLineVisible(0)); - CHECK(viewport.isLineVisible(-1)); - CHECK_FALSE(viewport.isLineVisible(-2)); + CHECK_FALSE(viewport.isLineVisible(LineOffset{0})); + CHECK_FALSE(viewport.isLineVisible(LineOffset{-1})); + CHECK(viewport.isLineVisible(LineOffset{-2})); + CHECK_FALSE(viewport.isLineVisible(LineOffset{-3})); viewport.scrollUp(LineCount(1)); - CHECK_FALSE(viewport.isLineVisible(1)); - CHECK_FALSE(viewport.isLineVisible(0)); - CHECK_FALSE(viewport.isLineVisible(-1)); - CHECK(viewport.isLineVisible(-2)); + CHECK(!viewport.isLineVisible(LineOffset{ 0})); + CHECK(!viewport.isLineVisible(LineOffset{-1})); + CHECK(!viewport.isLineVisible(LineOffset{-2})); + CHECK( viewport.isLineVisible(LineOffset{-3})); +} + +TEST_CASE("writeText.sequence", "[screen]") +{ + auto screen = MockScreen{PageSize{LineCount(2), ColumnCount(3)}, + LineCount(2)}; + screen.writeText("12345689"sv); + + logScreenTextAlways(screen); } TEST_CASE("AppendChar", "[screen]") { - auto screen = MockScreen{PageSize{LineCount(1), ColumnCount(3)}}; - REQUIRE(" " == screen.renderTextLine(1)); + auto screen = MockScreen{PageSize{LineCount(1), ColumnCount(3)}, + LineCount(1)}; + REQUIRE(screen.grid().buffer().size() == 2 * 3); + REQUIRE(screen.historyLineCount() == LineCount(0)); + REQUIRE(screen.size().lines == LineCount(1)); + REQUIRE(" " == screen.grid().renderTextLine(LineOffset(0))); screen.setMode(DECMode::AutoWrap, false); screen.write("A"); - REQUIRE("A " == screen.renderTextLine(1)); + REQUIRE("A " == screen.grid().renderTextLine(LineOffset(0))); screen.write("B"); - REQUIRE("AB " == screen.renderTextLine(1)); + REQUIRE("AB " == screen.grid().renderTextLine(LineOffset(0))); screen.write("C"); - REQUIRE("ABC" == screen.renderTextLine(1)); + REQUIRE("ABC" == screen.grid().renderTextLine(LineOffset(0))); + logScreenText(screen, "before writing D"); screen.write("D"); - REQUIRE("ABD" == screen.renderTextLine(1)); + REQUIRE("ABD" == screen.grid().renderTextLine(LineOffset(0))); screen.setMode(DECMode::AutoWrap, true); + logScreenText(screen, "with AutoWrap on"); - screen.write("E"); - REQUIRE("ABE" == screen.renderTextLine(1)); + screen.write("E"sv); + REQUIRE("ABE" == screen.grid().renderTextLine(LineOffset(0))); - screen.write("F"); - REQUIRE("F " == screen.renderTextLine(1)); + screen.write("F"sv); + CHECK("F " == screen.grid().renderTextLine(LineOffset(0))); + CHECK("ABE" == screen.grid().renderTextLine(LineOffset(-1))); } TEST_CASE("AppendChar_CR_LF", "[screen]") { auto screen = MockScreen{PageSize{LineCount(2), ColumnCount(3)}}; - REQUIRE(" " == screen.renderTextLine(1)); + REQUIRE(" " == screen.grid().renderTextLine(LineOffset(0))); screen.setMode(DECMode::AutoWrap, false); screen.write("ABC"); - REQUIRE("ABC" == screen.renderTextLine(1)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 3}); + REQUIRE("ABC" == screen.grid().renderTextLine(LineOffset(0))); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(2)}); screen.write("\r"); - REQUIRE("ABC\n \n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{1, 1}); + REQUIRE("ABC\n \n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); screen.write("\n"); - REQUIRE("ABC\n \n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{2, 1}); + REQUIRE("ABC\n \n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(0)}); } TEST_CASE("AppendChar.emoji_exclamationmark", "[screen]") @@ -149,101 +214,158 @@ TEST_CASE("AppendChar.emoji_exclamationmark", "[screen]") screen.write(U"\u2757"); // ❗ // screen.write(U"\uFE0F"); - CHECK(screen.at({1, 1}).attributes().backgroundColor == IndexedColor::Blue); - CHECK(screen.at({1, 1}).width() == 2); - CHECK(screen.at({1, 2}).attributes().backgroundColor == IndexedColor::Blue); - CHECK(screen.at({1, 2}).width() == 1); + CHECK(screen.at(LineOffset(0), ColumnOffset(0)).attributes().backgroundColor == IndexedColor::Blue); + CHECK(screen.at(LineOffset(0), ColumnOffset(0)).width() == 2); + CHECK(screen.at(LineOffset(0), ColumnOffset(1)).attributes().backgroundColor == IndexedColor::Blue); + CHECK(screen.at(LineOffset(0), ColumnOffset(1)).width() == 1); screen.write(U"M"); - CHECK(screen.at({1, 3}).attributes().backgroundColor == IndexedColor::Blue); + CHECK(screen.at(LineOffset(0), ColumnOffset(2)).attributes().backgroundColor == IndexedColor::Blue); } -TEST_CASE("AppendChar.emoji_VS16_fixed_width", "[screen]") +TEST_CASE("AppendChar.emoji_VS15_smiley", "[screen]") { - auto screen = MockScreen{PageSize{LineCount(1), ColumnCount(5)}}; + auto screen = MockScreen{PageSize{LineCount(1), ColumnCount(4)}}; - // print letter-like symbol `i` with forced emoji presentation style. - screen.write(U"\u2139"); + // print letter-like symbol copyright sign with forced emoji presentation style. + screen.write(U"\U0001F600"); + screen.write(U"\uFE0E"); + screen.write(U"X"); + + // emoji + auto const& c1 = screen.at(LineOffset(0), ColumnOffset(0)); + CHECK(c1.codepoints() == U"\U0001F600\uFE0E"); + CHECK(c1.width() == 1); + + // unused cell + auto const& c2 = screen.at(LineOffset(0), ColumnOffset(1)); + CHECK(c2.empty()); + CHECK(c2.width() == 1); + + // character after the emoji + auto const& c3 = screen.at(LineOffset(0), ColumnOffset(2)); + CHECK(c3.codepoints() == U"X"); + CHECK(c3.width() == 1); + + // tail + auto const& c4 = screen.at(LineOffset(0), ColumnOffset(3)); + CHECK(c4.codepoints().empty()); +} + +TEST_CASE("AppendChar.emoji_VS16_copyright_sign", "[screen]") +{ + auto screen = MockScreen{PageSize{LineCount(1), ColumnCount(4)}}; + auto const& c0 = screen.at(LineOffset(0), ColumnOffset(0)); + auto const& c1 = screen.at(LineOffset(0), ColumnOffset(1)); + auto const& c2 = screen.at(LineOffset(0), ColumnOffset(2)); + auto const& c3 = screen.at(LineOffset(0), ColumnOffset(3)); + + // print letter-like symbol copyright sign with forced emoji presentation style. + REQUIRE(screen.cursor().position.column.value == 0); + screen.write(U"\u00A9"); + REQUIRE(screen.cursor().position.column.value == 1); + CHECK(screen.grid().at({},{}).codepoints().size() == 1); screen.write(U"\uFE0F"); + CHECK(screen.grid().at({},{}).codepoints().size() == 2); + REQUIRE(screen.cursor().position.column.value == 2); screen.write(U"X"); + REQUIRE(screen.cursor().position.column.value == 3); // double-width emoji with VS16 - auto const& c1 = screen.at({1, 1}); - CHECK(c1.codepoints() == U"\u2139\uFE0F"); - CHECK(c1.width() == 1); // XXX by default: do not change width (TODO: create test for optionally changing width by configuration) + CHECK(c0.codepoints() == U"\u00A9\uFE0F"); + CHECK(c0.width() == 2); + + // unused cell + CHECK(c1.empty()); + CHECK(c1.width() == 1); // character after the emoji - auto const& c2 = screen.at({1, 2}); CHECK(c2.codepoints() == U"X"); CHECK(c2.width() == 1); - // character after the emoji - auto const& c3 = screen.at({1, 3}); - CHECK(c3.codepointCount() == 0); + CHECK(c3.codepoints().empty()); } -#if 0 -TEST_CASE("AppendChar.emoji_VS16_with_changing_width", "[screen]") // TODO +TEST_CASE("AppendChar.emoji_VS16_i", "[screen]") { auto screen = MockScreen{PageSize{LineCount(1), ColumnCount(5)}}; + auto const& c0 = screen.at(LineOffset(0), ColumnOffset(0)); // print letter-like symbol `i` with forced emoji presentation style. screen.write(U"\u2139"); + REQUIRE(screen.cursor().position.column.value == 1); + CHECK(c0.codepoints() == U"\u2139"); + CHECK(c0.width() == 1); + + // append into last cell screen.write(U"\uFE0F"); - screen.write(U"X"); + REQUIRE(screen.cursor().position.column.value == 2); + CHECK(c0.codepoints() == U"\u2139\uFE0F"); + CHECK(c0.width() == 2); - // double-width emoji with VS16 - auto const& c1 = screen.write(1, 1); - CHECK(c1.codepoints() == U"\u2139\uFE0F"); - CHECK(c1.width() == 2); + // write into 3rd cell + screen.write(U"X"); // unused cell - auto const& c2 = screen.write(1, 2); - CHECK(c2.codepointCount() == 0); + auto const& c1 = screen.at(LineOffset(0), ColumnOffset(1)); + CHECK(c1.empty()); + + // X-cell + auto const& c2 = screen.at(LineOffset(0), ColumnOffset(2)); + CHECK(c2.codepoints() == U"X"); CHECK(c2.width() == 1); // character after the emoji - auto const& c3 = screen.write(1, 3); - CHECK(c3.codepoints() == U"X"); - CHECK(c3.width() == 1); + auto const& c3 = screen.at(LineOffset(0), ColumnOffset(3)); + CHECK(c3.empty()); + + auto const& c4 = screen.at(LineOffset(0), ColumnOffset(4)); + CHECK(c4.empty()); } -#endif TEST_CASE("AppendChar.emoji_family", "[screen]") { auto screen = MockScreen{PageSize{LineCount(1), ColumnCount(5)}}; + auto const& c0 = screen.at(LineOffset(0), ColumnOffset(0)); + + REQUIRE(screen.cursorPosition().column.value == 0); // print letter-like symbol `i` with forced emoji presentation style. screen.write(U"\U0001F468"); - REQUIRE(screen.cursorPosition() == Coordinate{1, 3}); + CHECK(c0.codepoints() == U"\U0001F468"); + REQUIRE(screen.cursorPosition().column.value == 2); screen.write(U"\u200D"); - REQUIRE(screen.cursorPosition() == Coordinate{1, 3}); + CHECK(c0.codepoints() == U"\U0001F468\u200D"); + REQUIRE(screen.cursorPosition().column.value == 2); screen.write(U"\U0001F468"); - REQUIRE(screen.cursorPosition() == Coordinate{1, 3}); + CHECK(c0.codepoints() == U"\U0001F468\u200D\U0001F468"); + REQUIRE(screen.cursorPosition().column.value == 2); screen.write(U"\u200D"); - REQUIRE(screen.cursorPosition() == Coordinate{1, 3}); + CHECK(c0.codepoints() == U"\U0001F468\u200D\U0001F468\u200D"); + REQUIRE(screen.cursorPosition().column.value == 2); screen.write(U"\U0001F467"); - REQUIRE(screen.cursorPosition() == Coordinate{1, 3}); + CHECK(c0.codepoints() == U"\U0001F468\u200D\U0001F468\u200D\U0001F467"); + REQUIRE(screen.cursorPosition().column.value == 2); screen.write(U"X"); - REQUIRE(screen.cursorPosition() == Coordinate{1, 4}); + REQUIRE(screen.cursorPosition().column.value == 3); // double-width emoji with VS16 - auto const& c1 = screen.at({1, 1}); + auto const& c1 = screen.at(LineOffset(0), ColumnOffset(0)); CHECK(c1.codepoints() == U"\U0001F468\u200D\U0001F468\u200D\U0001F467"); CHECK(c1.width() == 2); // unused cell - auto const& c2 = screen.at({1, 2}); + auto const& c2 = screen.at(LineOffset(0), ColumnOffset(1)); CHECK(c2.codepointCount() == 0); CHECK(c2.width() == 1); // character after the emoji - auto const& c3 = screen.at({1, 3}); + auto const& c3 = screen.at(LineOffset(0), ColumnOffset(2)); CHECK(c3.codepoints() == U"X"); CHECK(c3.width() == 1); } -TEST_CASE("AppendChar.emoji_zwj1", "[screen]") +TEST_CASE("AppendChar.emoji_zwj_1", "[screen]") { auto screen = MockScreen{PageSize{LineCount(1), ColumnCount(5)}}; @@ -252,19 +374,21 @@ TEST_CASE("AppendChar.emoji_zwj1", "[screen]") // https://emojipedia.org/man-facepalming-medium-light-skin-tone/ auto const emoji = u32string_view{U"\U0001F926\U0001F3FC\u200D\u2642\uFE0F"}; screen.write(emoji); - // TODO: provide native UTF-32 write function (not emulated through UTF-8 -> UTF-32...) - auto const& c1 = screen.at({1, 1}); - CHECK(c1.codepoints() == emoji); - CHECK(c1.width() == 2); + auto const& c0 = screen.at(LineOffset(0), ColumnOffset(0)); + CHECK(c0.codepoints() == emoji); + CHECK(c0.width() == 2); // other columns remain untouched - CHECK(screen.at({1, 2}).codepointCount() == 0); - CHECK(screen.at({1, 3}).codepointCount() == 0); - CHECK(screen.at({1, 4}).codepointCount() == 0); - CHECK(screen.at({1, 5}).codepointCount() == 0); + CHECK(screen.at(LineOffset(0), ColumnOffset(1)).empty()); + CHECK(screen.at(LineOffset(0), ColumnOffset(2)).empty()); + CHECK(screen.at(LineOffset(0), ColumnOffset(3)).empty()); + CHECK(screen.at(LineOffset(0), ColumnOffset(4)).empty()); - CHECK(U"\U0001F926\U0001F3FC\u200D\u2642\uFE0F " == unicode::from_utf8(screen.renderTextLine(1))); + auto const s8 = screen.grid().renderTextLine(LineOffset(0)); + auto const s32 = unicode::from_utf8(s8); + CHECK(U"\U0001F926\U0001F3FC\u200D\u2642\uFE0F" == c0.codepoints()); + CHECK(U"\U0001F926\U0001F3FC\u200D\u2642\uFE0F " == s32); } TEST_CASE("AppendChar.emoji_1", "[screen]") @@ -273,21 +397,21 @@ TEST_CASE("AppendChar.emoji_1", "[screen]") screen.write(U"\U0001F600"); - auto const& c1 = screen.at({1, 1}); + auto const& c1 = screen.at(LineOffset(0), ColumnOffset(0)); CHECK(c1.codepoints() == U"\U0001F600"); CHECK(c1.width() == 2); - REQUIRE(screen.cursorPosition() == Coordinate{1, 3}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(2)}); - CHECK(screen.at({1, 2}).codepointCount() == 0); - CHECK(screen.at({1, 3}).codepointCount() == 0); + CHECK(screen.at(LineOffset(0), ColumnOffset(1)).codepointCount() == 0); + CHECK(screen.at(LineOffset(0), ColumnOffset(2)).codepointCount() == 0); screen.write("B"); - auto const& c2 = screen.at({1, 2}); + auto const& c2 = screen.at(LineOffset(0), ColumnOffset(1)); CHECK(c2.codepointCount() == 0); CHECK(c2.codepoints().empty()); CHECK(c2.width() == 1); - auto const& c3 = screen.at({1, 3}); + auto const& c3 = screen.at(LineOffset(0), ColumnOffset(2)); CHECK(c3.codepointCount() == 1); CHECK(c3.codepoint(0) == 'B'); CHECK(c3.width() == 1); @@ -298,7 +422,7 @@ TEST_CASE("AppendChar_WideChar", "[screen]") auto screen = MockScreen{PageSize{LineCount(2), ColumnCount(3)}}; screen.setMode(DECMode::AutoWrap, true); screen.write(U"\U0001F600"); - CHECK(screen.cursorPosition() == Coordinate{1, 3}); + CHECK(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(2)}); } TEST_CASE("AppendChar_AutoWrap", "[screen]") @@ -307,21 +431,21 @@ TEST_CASE("AppendChar_AutoWrap", "[screen]") screen.setMode(DECMode::AutoWrap, true); screen.write("ABC"); - REQUIRE("ABC" == screen.renderTextLine(1)); - REQUIRE(" " == screen.renderTextLine(2)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 3}); + REQUIRE("ABC" == screen.grid().renderTextLine(LineOffset(0))); + REQUIRE(" " == screen.grid().renderTextLine(LineOffset(1))); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(2)}); screen.write("D"); - REQUIRE("ABC" == screen.renderTextLine(1)); - REQUIRE("D " == screen.renderTextLine(2)); + REQUIRE("ABC" == screen.grid().renderTextLine(LineOffset(0))); + REQUIRE("D " == screen.grid().renderTextLine(LineOffset(1))); screen.write("EF"); - REQUIRE("ABC" == screen.renderTextLine(1)); - REQUIRE("DEF" == screen.renderTextLine(2)); + REQUIRE("ABC" == screen.grid().renderTextLine(LineOffset(0))); + REQUIRE("DEF" == screen.grid().renderTextLine(LineOffset(1))); screen.write("G"); - REQUIRE("DEF" == screen.renderTextLine(1)); - REQUIRE("G " == screen.renderTextLine(2)); + REQUIRE("DEF" == screen.grid().renderTextLine(LineOffset(0))); + REQUIRE("G " == screen.grid().renderTextLine(LineOffset(1))); } TEST_CASE("AppendChar_AutoWrap_LF", "[screen]") @@ -332,43 +456,43 @@ TEST_CASE("AppendChar_AutoWrap_LF", "[screen]") INFO("write ABC"); screen.write("ABC"); logScreenText(screen); - REQUIRE("ABC" == screen.renderTextLine(1)); - REQUIRE(" " == screen.renderTextLine(2)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 3}); + REQUIRE("ABC" == screen.grid().renderTextLine(LineOffset(0))); + REQUIRE(" " == screen.grid().renderTextLine(LineOffset(1))); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(2)}); INFO("write CRLF"); screen.write("\r\n"); logScreenText(screen, "after writing LF"); - REQUIRE(screen.cursorPosition() == Coordinate{2, 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(0)}); INFO("write 'D'"); screen.write("D"); logScreenText(screen); - REQUIRE("ABC" == screen.renderTextLine(1)); - REQUIRE("D " == screen.renderTextLine(2)); - REQUIRE(screen.cursorPosition() == Coordinate{2, 2}); + REQUIRE("ABC" == screen.grid().renderTextLine(LineOffset(0))); + REQUIRE("D " == screen.grid().renderTextLine(LineOffset(1))); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(1)}); } TEST_CASE("Backspace", "[screen]") { auto screen = MockScreen{PageSize{LineCount(2), ColumnCount(3)}}; - REQUIRE(screen.cursorPosition() == Coordinate{1, 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); screen.write("12"); - CHECK("12 " == screen.renderTextLine(1)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 3}); + CHECK("12 " == screen.grid().renderTextLine(LineOffset(0))); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(2)}); screen.write("\b"); - CHECK("12 " == screen.renderTextLine(1)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 2}); + CHECK("12 " == screen.grid().renderTextLine(LineOffset(0))); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(1)}); screen.write("\b"); - CHECK("12 " == screen.renderTextLine(1)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 1}); + CHECK("12 " == screen.grid().renderTextLine(LineOffset(0))); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); screen.write("\b"); - CHECK("12 " == screen.renderTextLine(1)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 1}); + CHECK("12 " == screen.grid().renderTextLine(LineOffset(0))); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); } TEST_CASE("Linefeed", "[screen]") @@ -376,26 +500,26 @@ TEST_CASE("Linefeed", "[screen]") auto screen = MockScreen{PageSize{LineCount(2), ColumnCount(2)}}; SECTION("with scroll-up") { INFO("init:"); - INFO(fmt::format(" line 1: '{}'", screen.renderTextLine(1))); - INFO(fmt::format(" line 2: '{}'", screen.renderTextLine(2))); + INFO(fmt::format(" line 1: '{}'", screen.grid().renderTextLine(LineOffset(0)))); + INFO(fmt::format(" line 2: '{}'", screen.grid().renderTextLine(LineOffset(1)))); screen.write("1\r\n2"); INFO("after writing '1\\n2':"); - INFO(fmt::format(" line 1: '{}'", screen.renderTextLine(1))); - INFO(fmt::format(" line 2: '{}'", screen.renderTextLine(2))); + INFO(fmt::format(" line 1: '{}'", screen.grid().renderTextLine(LineOffset(0)))); + INFO(fmt::format(" line 2: '{}'", screen.grid().renderTextLine(LineOffset(1)))); - REQUIRE("1 " == screen.renderTextLine(1)); - REQUIRE("2 " == screen.renderTextLine(2)); + REQUIRE("1 " == screen.grid().renderTextLine(LineOffset(0))); + REQUIRE("2 " == screen.grid().renderTextLine(LineOffset(1))); screen.write("\r\n3"); // line 3 INFO("After writing '\\n3':"); - INFO(fmt::format(" line 1: '{}'", screen.renderTextLine(1))); - INFO(fmt::format(" line 2: '{}'", screen.renderTextLine(2))); + INFO(fmt::format(" line 1: '{}'", screen.grid().renderTextLine(LineOffset(0)))); + INFO(fmt::format(" line 2: '{}'", screen.grid().renderTextLine(LineOffset(1)))); - REQUIRE("2 " == screen.renderTextLine(1)); - REQUIRE("3 " == screen.renderTextLine(2)); + REQUIRE("2 " == screen.grid().renderTextLine(LineOffset(0))); + REQUIRE("3 " == screen.grid().renderTextLine(LineOffset(1))); } } @@ -404,18 +528,18 @@ TEST_CASE("ClearToEndOfScreen", "[screen]") auto screen = MockScreen{PageSize{LineCount(3), ColumnCount(3)}}; screen.write("ABC\r\nDEF\r\nGHI"); - REQUIRE("ABC" == screen.renderTextLine(1)); - REQUIRE("DEF" == screen.renderTextLine(2)); - REQUIRE("GHI" == screen.renderTextLine(3)); - REQUIRE(screen.cursorPosition() == Coordinate{3, 3}); + REQUIRE("ABC" == screen.grid().renderTextLine(LineOffset(0))); + REQUIRE("DEF" == screen.grid().renderTextLine(LineOffset(1))); + REQUIRE("GHI" == screen.grid().renderTextLine(LineOffset(2))); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(2), ColumnOffset(2)}); - screen.moveCursorTo({2, 2}); + screen.moveCursorTo(LineOffset{1}, ColumnOffset{1}); screen.clearToEndOfScreen(); - CHECK("ABC" == screen.renderTextLine(1)); - CHECK("D " == screen.renderTextLine(2)); - CHECK(" " == screen.renderTextLine(3)); - REQUIRE(screen.cursorPosition() == Coordinate{2, 2}); + CHECK("ABC" == screen.grid().renderTextLine(LineOffset(0))); + CHECK("D " == screen.grid().renderTextLine(LineOffset(1))); + CHECK(" " == screen.grid().renderTextLine(LineOffset(2))); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(1)}); } TEST_CASE("ClearToBeginOfScreen", "[screen]") @@ -423,18 +547,18 @@ TEST_CASE("ClearToBeginOfScreen", "[screen]") auto screen = MockScreen{PageSize{LineCount(3), ColumnCount(3)}}; screen.write("ABC\r\nDEF\r\nGHI"); - REQUIRE("ABC" == screen.renderTextLine(1)); - REQUIRE("DEF" == screen.renderTextLine(2)); - REQUIRE("GHI" == screen.renderTextLine(3)); - REQUIRE(screen.cursorPosition() == Coordinate{3, 3}); + REQUIRE("ABC" == screen.grid().renderTextLine(LineOffset(0))); + REQUIRE("DEF" == screen.grid().renderTextLine(LineOffset(1))); + REQUIRE("GHI" == screen.grid().renderTextLine(LineOffset(2))); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(2), ColumnOffset(2)}); - screen.moveCursorTo({2, 2}); + screen.moveCursorTo(LineOffset(1), ColumnOffset(1)); screen.clearToBeginOfScreen(); - CHECK(" " == screen.renderTextLine(1)); - CHECK(" F" == screen.renderTextLine(2)); - CHECK("GHI" == screen.renderTextLine(3)); - CHECK(screen.cursorPosition() == Coordinate{2, 2}); + CHECK(" " == screen.grid().renderTextLine(LineOffset(0))); + CHECK(" F" == screen.grid().renderTextLine(LineOffset(1))); + CHECK("GHI" == screen.grid().renderTextLine(LineOffset(2))); + CHECK(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(1)}); } TEST_CASE("ClearScreen", "[screen]") @@ -442,19 +566,19 @@ TEST_CASE("ClearScreen", "[screen]") auto screen = MockScreen{PageSize{LineCount(2), ColumnCount(2)}}; screen.write("AB\r\nC"); screen.clearScreen(); - CHECK(" " == screen.renderTextLine(1)); - CHECK(" " == screen.renderTextLine(2)); + CHECK(" " == screen.grid().renderTextLine(LineOffset(0))); + CHECK(" " == screen.grid().renderTextLine(LineOffset(1))); } TEST_CASE("ClearToEndOfLine", "[screen]") { auto screen = MockScreen{PageSize{LineCount(1), ColumnCount(3)}}; screen.write("ABC"); - REQUIRE("ABC" == screen.renderTextLine(1)); + REQUIRE("ABC" == screen.grid().renderTextLine(LineOffset(0))); - screen.moveCursorToColumn(ColumnPosition(2)); + screen.moveCursorToColumn(ColumnOffset(1)); screen.clearToEndOfLine(); - CHECK("A " == screen.renderTextLine(1)); + CHECK("A " == screen.grid().renderTextLine(LineOffset(0))); } TEST_CASE("ClearToBeginOfLine", "[screen]") @@ -462,11 +586,11 @@ TEST_CASE("ClearToBeginOfLine", "[screen]") auto screen = MockScreen{PageSize{LineCount(1), ColumnCount(3)}}; screen.setMode(DECMode::AutoWrap, false); screen.write("ABC"); - REQUIRE("ABC" == screen.renderTextLine(1)); + REQUIRE("ABC" == screen.grid().renderTextLine(LineOffset(0))); - screen.moveCursorToColumn(ColumnPosition(2)); + screen.moveCursorToColumn(ColumnOffset(1)); screen.clearToBeginOfLine(); - CHECK(" C" == screen.renderTextLine(1)); + CHECK(" C" == screen.grid().renderTextLine(LineOffset(0))); } TEST_CASE("ClearLine", "[screen]") @@ -474,10 +598,10 @@ TEST_CASE("ClearLine", "[screen]") auto screen = MockScreen{PageSize{LineCount(1), ColumnCount(3)}}; screen.setMode(DECMode::AutoWrap, false); screen.write("ABC"); - REQUIRE("ABC" == screen.renderTextLine(1)); + REQUIRE("ABC" == screen.grid().renderTextLine(LineOffset(0))); screen.clearLine(); - CHECK(" " == screen.renderTextLine(1)); + CHECK(" " == screen.grid().renderTextLine(LineOffset(0))); } TEST_CASE("InsertColumns", "[screen]") @@ -487,100 +611,108 @@ TEST_CASE("InsertColumns", "[screen]") screen.write("12345\r\n67890\r\nABCDE\r\nFGHIJ\r\nKLMNO"); screen.setMode(DECMode::LeftRightMargin, true); - screen.setLeftRightMargin(2, 4); - screen.setTopBottomMargin(2, 4); + screen.setLeftRightMargin(ColumnOffset(1), ColumnOffset(3)); + screen.setTopBottomMargin(LineOffset(1), LineOffset(3)); - REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{1, 1}); + REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); SECTION("outside margins: top left") { - screen.moveCursorTo({1, 1}); + screen.moveCursorTo({}, {}); screen.insertColumns(ColumnCount(1)); - REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); + REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); } SECTION("outside margins: bottom right") { - screen.moveCursorTo({5, 5}); + screen.moveCursorTo(LineOffset(4), ColumnOffset(4)); screen.insertColumns(ColumnCount(1)); - REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); + REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); } SECTION("inside margins") { - screen.moveCursorTo({2, 3}); - REQUIRE(screen.cursorPosition() == Coordinate{2, 3}); + screen.moveCursorTo(LineOffset{1}, ColumnOffset{2}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(2)}); SECTION("DECIC-0") { screen.insertColumns(ColumnCount(0)); - REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); + REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); } SECTION("DECIC-1") { screen.insertColumns(ColumnCount(1)); - REQUIRE("12345\n67 80\nAB CE\nFG HJ\nKLMNO\n" == screen.renderText()); + REQUIRE("12345\n67 80\nAB CE\nFG HJ\nKLMNO\n" == screen.renderMainPageText()); } SECTION("DECIC-2") { screen.insertColumns(ColumnCount(2)); - REQUIRE("12345\n67 0\nAB E\nFG J\nKLMNO\n" == screen.renderText()); + REQUIRE("12345\n67 0\nAB E\nFG J\nKLMNO\n" == screen.renderMainPageText()); + } + + SECTION("DECIC-2 (another)") { + screen.moveCursorTo(LineOffset{1}, ColumnOffset{1}); + screen.insertColumns(ColumnCount(2)); + REQUIRE("12345\n6 70\nA BE\nF GJ\nKLMNO\n" == screen.renderMainPageText()); } SECTION("DECIC-3-clamped") { screen.insertColumns(ColumnCount(3)); - REQUIRE("12345\n67 0\nAB E\nFG J\nKLMNO\n" == screen.renderText()); + REQUIRE("12345\n67 0\nAB E\nFG J\nKLMNO\n" == screen.renderMainPageText()); } } SECTION("inside margins - repeative") { - screen.moveCursorTo({2, 2}); + screen.moveCursorTo(LineOffset{1}, ColumnOffset{1}); screen.insertColumns(ColumnCount(1)); - REQUIRE("12345\n6 780\nA BCE\nF GHJ\nKLMNO\n" == screen.renderText()); + REQUIRE("12345\n6 780\nA BCE\nF GHJ\nKLMNO\n" == screen.renderMainPageText()); screen.insertColumns(ColumnCount(1)); - REQUIRE("12345\n6 70\nA BE\nF GJ\nKLMNO\n" == screen.renderText()); + REQUIRE("12345\n6 70\nA BE\nF GJ\nKLMNO\n" == screen.renderMainPageText()); } } TEST_CASE("InsertCharacters", "[screen]") { auto screen = MockScreen{PageSize{LineCount(2), ColumnCount(5)}}; - screen.write("12345\r\n67890"); + screen.write("12345\r\n678"); + screen.write("90"); + screen.setMode(DECMode::LeftRightMargin, true); - screen.setLeftRightMargin(2, 4); - REQUIRE("12345\n67890\n" == screen.renderText()); + screen.setLeftRightMargin(ColumnOffset(1), ColumnOffset(3)); + REQUIRE("12345\n67890\n" == screen.renderMainPageText()); SECTION("outside margins: left") { - screen.moveCursorTo({1, 1}); + screen.moveCursorTo(LineOffset(0), ColumnOffset(0)); screen.insertCharacters(ColumnCount(1)); - REQUIRE("12345\n67890\n" == screen.renderText()); + REQUIRE("12345\n67890\n" == screen.renderMainPageText()); } SECTION("outside margins: right") { - screen.moveCursorTo({1, 5}); + screen.moveCursorTo(LineOffset{0}, ColumnOffset{4}); screen.insertCharacters(ColumnCount(1)); - REQUIRE("12345\n67890\n" == screen.renderText()); + REQUIRE("12345\n67890\n" == screen.renderMainPageText()); } SECTION("inside margins") { - screen.moveCursorTo({1, 3}); - REQUIRE(screen.cursorPosition() == Coordinate{1, 3}); + screen.moveCursorTo(LineOffset(0), ColumnOffset(2)); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(2)}); SECTION("no-op") { screen.insertCharacters(ColumnCount(0)); - REQUIRE(screen.renderText() == "12345\n67890\n"); + CHECK(screen.renderMainPageText() == "12345\n67890\n"); } SECTION("ICH-1") { screen.insertCharacters(ColumnCount(1)); - REQUIRE(screen.renderText() == "12 35\n67890\n"); + CHECK(screen.renderMainPageText() == "12 35\n67890\n"); } SECTION("ICH-2") { screen.insertCharacters(ColumnCount(2)); - REQUIRE(screen.renderText() == "12 5\n67890\n"); + CHECK(screen.renderMainPageText() == "12 5\n67890\n"); } SECTION("ICH-3-clamped") { screen.insertCharacters(ColumnCount(3)); - REQUIRE(screen.renderText() == "12 5\n67890\n"); + REQUIRE(screen.renderMainPageText() == "12 5\n67890\n"); } } } @@ -589,26 +721,26 @@ TEST_CASE("InsertLines", "[screen]") { auto screen = MockScreen{PageSize{LineCount(6), ColumnCount(4)}}; screen.write("1234\r\n5678\r\nABCD\r\nEFGH\r\nIJKL\r\nMNOP"); - REQUIRE("1234\n5678\nABCD\nEFGH\nIJKL\nMNOP\n" == screen.renderText()); + REQUIRE("1234\n5678\nABCD\nEFGH\nIJKL\nMNOP\n" == screen.renderMainPageText()); SECTION("old") { auto screen = MockScreen{PageSize{LineCount(3), ColumnCount(2)}}; screen.write("AB\r\nCD"); - REQUIRE("AB" == screen.renderTextLine(1)); - REQUIRE("CD" == screen.renderTextLine(2)); - REQUIRE(" " == screen.renderTextLine(3)); + REQUIRE("AB" == screen.grid().renderTextLine(LineOffset(0))); + REQUIRE("CD" == screen.grid().renderTextLine(LineOffset(1))); + REQUIRE(" " == screen.grid().renderTextLine(LineOffset(2))); screen.insertLines(LineCount(1)); - CHECK("AB" == screen.renderTextLine(1)); - CHECK(" " == screen.renderTextLine(2)); - CHECK("CD" == screen.renderTextLine(3)); + CHECK("AB" == screen.grid().renderTextLine(LineOffset(0))); + CHECK(" " == screen.grid().renderTextLine(LineOffset(1))); + CHECK("CD" == screen.grid().renderTextLine(LineOffset(2))); - screen.moveCursorTo({1, 1}); + screen.moveCursorTo(LineOffset{0}, ColumnOffset{0}); screen.insertLines(LineCount(1)); - CHECK(" " == screen.renderTextLine(1)); - CHECK("AB" == screen.renderTextLine(2)); - CHECK(" " == screen.renderTextLine(3)); + CHECK(" " == screen.grid().renderTextLine(LineOffset(0))); + CHECK("AB" == screen.grid().renderTextLine(LineOffset(1))); + CHECK(" " == screen.grid().renderTextLine(LineOffset(2))); } // TODO: test with (top/bottom and left/right) margins enabled } @@ -619,73 +751,82 @@ TEST_CASE("DeleteLines", "[screen]") screen.write("AB\r\nCD\r\nEF"); logScreenText(screen, "initial"); - REQUIRE("AB" == screen.renderTextLine(1)); - REQUIRE("CD" == screen.renderTextLine(2)); - REQUIRE("EF" == screen.renderTextLine(3)); + REQUIRE("AB" == screen.grid().renderTextLine(LineOffset(0))); + REQUIRE("CD" == screen.grid().renderTextLine(LineOffset(1))); + REQUIRE("EF" == screen.grid().renderTextLine(LineOffset(2))); - screen.moveCursorTo({2, 1}); - REQUIRE(screen.cursorPosition() == Coordinate{2, 1}); + screen.moveCursorTo(LineOffset(1), ColumnOffset(0)); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(0)}); SECTION("no-op") { screen.deleteLines(LineCount(0)); - REQUIRE("AB" == screen.renderTextLine(1)); - REQUIRE("CD" == screen.renderTextLine(2)); - REQUIRE("EF" == screen.renderTextLine(3)); + CHECK("AB" == screen.grid().renderTextLine(LineOffset(0))); + CHECK("CD" == screen.grid().renderTextLine(LineOffset(1))); + CHECK("EF" == screen.grid().renderTextLine(LineOffset(2))); } SECTION("in-range") { screen.deleteLines(LineCount(1)); logScreenText(screen, "After EL(1)"); - REQUIRE("AB" == screen.renderTextLine(1)); - REQUIRE("EF" == screen.renderTextLine(2)); - REQUIRE(" " == screen.renderTextLine(3)); + CHECK("AB" == screen.grid().renderTextLine(LineOffset(0))); + CHECK("EF" == screen.grid().renderTextLine(LineOffset(1))); + CHECK(" " == screen.grid().renderTextLine(LineOffset(2))); } SECTION("clamped") { - screen.moveCursorTo({2, 2}); + screen.moveCursorTo(LineOffset(1), ColumnOffset(1)); screen.deleteLines(LineCount(5)); - logScreenText(screen, "After clamped EL(5)"); - REQUIRE("AB" == screen.renderTextLine(1)); - REQUIRE(" " == screen.renderTextLine(2)); - REQUIRE(" " == screen.renderTextLine(3)); + //logScreenText(screen, "After clamped EL(5)"); + CHECK("AB" == screen.grid().renderTextLine(LineOffset(0))); + CHECK(" " == screen.grid().renderTextLine(LineOffset(1))); + CHECK(" " == screen.grid().renderTextLine(LineOffset(2))); } } +TEST_CASE("FillArea", "[screen]") +{ + auto screen = MockScreen{PageSize{LineCount(5), ColumnCount(5)}}; + screen.write("12345\r\n67890\r\nABCDE\r\nFGHIJ\r\nKLMNO"); + + screen.fillArea(L'.', 1, 1, 3, 3); + CHECK(mainPageText(screen) == "123456...0A...EF...JKLMNO"); +} + TEST_CASE("DeleteColumns", "[screen]") { auto screen = MockScreen{PageSize{LineCount(5), ColumnCount(5)}}; screen.write("12345\r\n67890\r\nABCDE\r\nFGHIJ\r\nKLMNO"); screen.setMode(DECMode::LeftRightMargin, true); - screen.setLeftRightMargin(2, 4); - screen.setTopBottomMargin(2, 4); + screen.setLeftRightMargin(ColumnOffset(1), ColumnOffset(3)); + screen.setTopBottomMargin(LineOffset(1), LineOffset(3)); - REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{1, 1}); + REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); SECTION("outside margin") { screen.deleteColumns(ColumnCount(1)); - REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); + REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); } SECTION("inside margin") { - screen.moveCursorTo({ 2, 3 }); - REQUIRE(screen.cursorPosition() == Coordinate{2, 3}); + screen.moveCursorTo(LineOffset(1), ColumnOffset(2)); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(2)}); SECTION("DECDC-0") { screen.deleteColumns(ColumnCount(0)); - REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); + REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); } SECTION("DECDC-1") { screen.deleteColumns(ColumnCount(1)); - REQUIRE("12345\n679 0\nABD E\nFGI J\nKLMNO\n" == screen.renderText()); + REQUIRE("12345\n679 0\nABD E\nFGI J\nKLMNO\n" == screen.renderMainPageText()); } SECTION("DECDC-2") { screen.deleteColumns(ColumnCount(2)); - REQUIRE("12345\n67 0\nAB E\nFG J\nKLMNO\n" == screen.renderText()); + REQUIRE("12345\n67 0\nAB E\nFG J\nKLMNO\n" == screen.renderMainPageText()); } SECTION("DECDC-3-clamped") { screen.deleteColumns(ColumnCount(4)); - REQUIRE("12345\n67 0\nAB E\nFG J\nKLMNO\n" == screen.renderText()); + REQUIRE("12345\n67 0\nAB E\nFG J\nKLMNO\n" == screen.renderMainPageText()); } } } @@ -694,100 +835,104 @@ TEST_CASE("DeleteCharacters", "[screen]") { auto screen = MockScreen{PageSize{LineCount(2), ColumnCount(5)}}; screen.write("12345\r\n67890\033[1;2H"); - REQUIRE("12345\n67890\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{1, 2}); + REQUIRE("12345\n67890\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(1)}); SECTION("outside margin") { screen.setMode(DECMode::LeftRightMargin, true); - screen.setLeftRightMargin(2, 4); - screen.moveCursorTo({ 1, 1 }); + screen.setLeftRightMargin(ColumnOffset(1), ColumnOffset(3)); + screen.moveCursorTo(LineOffset(0), ColumnOffset(0)); screen.deleteCharacters(ColumnCount(1)); - REQUIRE("12345\n67890\n" == screen.renderText()); + REQUIRE("12345\n67890\n" == screen.renderMainPageText()); } SECTION("without horizontal margin") { SECTION("no-op") { screen.deleteCharacters(ColumnCount(0)); - REQUIRE("12345\n67890\n" == screen.renderText()); + REQUIRE("12345\n67890\n" == screen.renderMainPageText()); } SECTION("in-range-1") { screen.deleteCharacters(ColumnCount(1)); - REQUIRE("1345 \n67890\n" == screen.renderText()); + REQUIRE("1345 \n67890\n" == screen.renderMainPageText()); } SECTION("in-range-2") { screen.deleteCharacters(ColumnCount(2)); - REQUIRE("145 \n67890\n" == screen.renderText()); + REQUIRE("145 \n67890\n" == screen.renderMainPageText()); } SECTION("in-range-4") { screen.deleteCharacters(ColumnCount(4)); - REQUIRE("1 \n67890\n" == screen.renderText()); + REQUIRE("1 \n67890\n" == screen.renderMainPageText()); } SECTION("clamped") { screen.deleteCharacters(ColumnCount(5)); - REQUIRE("1 \n67890\n" == screen.renderText()); + REQUIRE("1 \n67890\n" == screen.renderMainPageText()); } } SECTION("with horizontal margin") { screen.setMode(DECMode::LeftRightMargin, true); - screen.setLeftRightMargin(1, 4 ); - screen.moveCursorTo({ 1, 2 }); - REQUIRE(screen.cursorPosition() == Coordinate{1, 2}); + screen.setLeftRightMargin(ColumnOffset(0), ColumnOffset(3)); + screen.moveCursorTo(LineOffset(0), ColumnOffset(1)); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(1)}); SECTION("no-op") { screen.deleteCharacters(ColumnCount(0)); - REQUIRE("12345\n67890\n" == screen.renderText()); + REQUIRE("12345\n67890\n" == screen.renderMainPageText()); } SECTION("in-range-1") { - REQUIRE("12345\n67890\n" == screen.renderText()); + REQUIRE("12345\n67890\n" == screen.renderMainPageText()); screen.deleteCharacters(ColumnCount(1)); - REQUIRE("134 5\n67890\n" == screen.renderText()); + REQUIRE("134 5\n67890\n" == screen.renderMainPageText()); } SECTION("in-range-2") { screen.deleteCharacters(ColumnCount(2)); - REQUIRE("14 5\n67890\n" == screen.renderText()); + REQUIRE("14 5\n67890\n" == screen.renderMainPageText()); } SECTION("clamped") { screen.deleteCharacters(ColumnCount(4)); - REQUIRE("1 5\n67890\n" == screen.renderText()); + REQUIRE("1 5\n67890\n" == screen.renderMainPageText()); } } } TEST_CASE("ClearScrollbackBuffer", "[screen]") { - auto screen = MockScreen{PageSize{LineCount(5), ColumnCount(5)}}; + auto screen = MockScreen{PageSize{LineCount(5), ColumnCount(5)}, LineCount(1)}; screen.write("12345\r\n67890\r\nABCDE\r\nFGHIJ\r\nKLMNO\r\nPQRST\033[H"); - REQUIRE("67890\nABCDE\nFGHIJ\nKLMNO\nPQRST\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{1, 1}); - REQUIRE(size_t{1} == screen.scrollbackLines().size()); - REQUIRE("12345" == screen.renderHistoryTextLine(1)); + REQUIRE("67890\nABCDE\nFGHIJ\nKLMNO\nPQRST\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); + REQUIRE(1 == screen.scrollbackLines().size()); + REQUIRE("12345" == screen.grid().renderTextLine(LineOffset(-1))); + + screen.grid().clearHistory(); + REQUIRE(screen.scrollbackLines().size() == 0); + REQUIRE(screen.historyLineCount() == LineCount(0)); } TEST_CASE("EraseCharacters", "[screen]") { auto screen = MockScreen{PageSize{LineCount(5), ColumnCount(5)}}; screen.write("12345\r\n67890\r\nABCDE\r\nFGHIJ\r\nKLMNO\033[H"); - REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{1, 1}); + REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); SECTION("ECH-0 equals ECH-1") { screen.eraseCharacters(ColumnCount(0)); - REQUIRE(" 2345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); + REQUIRE(" 2345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); } SECTION("ECH-1") { screen.eraseCharacters(ColumnCount(1)); - REQUIRE(" 2345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); + REQUIRE(" 2345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); } SECTION("ECH-5") { screen.eraseCharacters(ColumnCount(5)); - REQUIRE(" \n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); + REQUIRE(" \n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); } SECTION("ECH-6-clamped") { screen.eraseCharacters(ColumnCount(6)); - REQUIRE(" \n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); + REQUIRE(" \n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); } } @@ -797,33 +942,33 @@ TEST_CASE("ScrollUp", "[screen]") screen.write("ABC\r\n"); screen.write("DEF\r\n"); screen.write("GHI"); - REQUIRE("ABC\nDEF\nGHI\n" == screen.renderText()); + REQUIRE("ABC\nDEF\nGHI\n" == screen.renderMainPageText()); SECTION("no-op") { INFO("begin:"); screen.scrollUp(LineCount(0)); INFO("end:"); - REQUIRE("ABC\nDEF\nGHI\n" == screen.renderText()); + REQUIRE("ABC\nDEF\nGHI\n" == screen.renderMainPageText()); } SECTION("by-1") { screen.scrollUp(LineCount(1)); - REQUIRE("DEF\nGHI\n \n" == screen.renderText()); + REQUIRE("DEF\nGHI\n \n" == screen.renderMainPageText()); } SECTION("by-2") { screen.scrollUp(LineCount(2)); - REQUIRE("GHI\n \n \n" == screen.renderText()); + REQUIRE("GHI\n \n \n" == screen.renderMainPageText()); } SECTION("by-3") { screen.scrollUp(LineCount(3)); - REQUIRE(" \n \n \n" == screen.renderText()); + REQUIRE(" \n \n \n" == screen.renderMainPageText()); } SECTION("clamped") { screen.scrollUp(LineCount(4)); - REQUIRE(" \n \n \n" == screen.renderText()); + REQUIRE(" \n \n \n" == screen.renderMainPageText()); } } @@ -831,42 +976,42 @@ TEST_CASE("ScrollDown", "[screen]") { auto screen = MockScreen{PageSize{LineCount(5), ColumnCount(5)}}; screen.write("12345\r\n67890\r\nABCDE\r\nFGHIJ\r\nKLMNO"); - REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); + REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); SECTION("scroll fully inside margins") { screen.setMode(DECMode::LeftRightMargin, true); - screen.setLeftRightMargin(2, 4); - screen.setTopBottomMargin(2, 4); + screen.setLeftRightMargin(ColumnOffset{1}, ColumnOffset{3}); + screen.setTopBottomMargin(LineOffset{1}, LineOffset{3}); screen.setMode(DECMode::Origin, true); - SECTION("SD 1") { - screen.scrollDown(LineCount(1)); - CHECK("12345\n6 0\nA789E\nFBCDJ\nKLMNO\n" == screen.renderText()); - } - - SECTION("SD 2") { - screen.scrollDown(LineCount(2)); - CHECK( - "12345\n" - "6 0\n" - "A E\n" - "F789J\n" - "KLMNO\n" == screen.renderText()); - } - - SECTION("SD 3") { - screen.scrollDown(LineCount(3)); - CHECK( - "12345\n" - "6 0\n" - "A E\n" - "F J\n" - "KLMNO\n" == screen.renderText()); - } + // SECTION("SD 1") { + // screen.scrollDown(LineCount(1)); + // CHECK("12345\n6 0\nA789E\nFBCDJ\nKLMNO\n" == screen.renderMainPageText()); + // } + + // SECTION("SD 2") { + // screen.scrollDown(LineCount(2)); + // CHECK( + // "12345\n" + // "6 0\n" + // "A E\n" + // "F789J\n" + // "KLMNO\n" == screen.renderMainPageText()); + // } + // + // SECTION("SD 3") { + // screen.scrollDown(LineCount(3)); + // CHECK( + // "12345\n" + // "6 0\n" + // "A E\n" + // "F J\n" + // "KLMNO\n" == screen.renderMainPageText()); + // } } SECTION("vertical margins") { - screen.setTopBottomMargin(2, 4); + screen.setTopBottomMargin(LineOffset{1}, LineOffset{3}); SECTION("SD 0") { screen.scrollDown(LineCount(0)); REQUIRE( @@ -874,7 +1019,7 @@ TEST_CASE("ScrollDown", "[screen]") "67890\n" "ABCDE\n" "FGHIJ\n" - "KLMNO\n" == screen.renderText()); + "KLMNO\n" == screen.renderMainPageText()); } SECTION("SD 1") { @@ -884,7 +1029,7 @@ TEST_CASE("ScrollDown", "[screen]") " \n" "67890\n" "ABCDE\n" - "KLMNO\n" == screen.renderText()); + "KLMNO\n" == screen.renderMainPageText()); } SECTION("SD 3") { @@ -894,7 +1039,7 @@ TEST_CASE("ScrollDown", "[screen]") " \n" " \n" " \n" - "KLMNO\n" == screen.renderText()); + "KLMNO\n" == screen.renderMainPageText()); } SECTION("SD 4 clamped") { @@ -904,7 +1049,7 @@ TEST_CASE("ScrollDown", "[screen]") " \n" " \n" " \n" - "KLMNO\n" == screen.renderText()); + "KLMNO\n" == screen.renderMainPageText()); } } @@ -916,7 +1061,7 @@ TEST_CASE("ScrollDown", "[screen]") "67890\n" "ABCDE\n" "FGHIJ\n" - "KLMNO\n" == screen.renderText()); + "KLMNO\n" == screen.renderMainPageText()); } SECTION("SD 1") { screen.scrollDown(LineCount(1)); @@ -926,7 +1071,7 @@ TEST_CASE("ScrollDown", "[screen]") "67890\n" "ABCDE\n" "FGHIJ\n" - == screen.renderText()); + == screen.renderMainPageText()); } SECTION("SD 5") { screen.scrollDown(LineCount(5)); @@ -936,7 +1081,7 @@ TEST_CASE("ScrollDown", "[screen]") " \n" " \n" " \n" - == screen.renderText()); + == screen.renderMainPageText()); } SECTION("SD 6 clamped") { screen.scrollDown(LineCount(6)); @@ -946,7 +1091,7 @@ TEST_CASE("ScrollDown", "[screen]") " \n" " \n" " \n" - == screen.renderText()); + == screen.renderMainPageText()); } } } @@ -955,46 +1100,46 @@ TEST_CASE("MoveCursorUp", "[screen]") { auto screen = MockScreen{PageSize{LineCount(5), ColumnCount(5)}}; screen.write("12345\r\n67890\r\nABCDE\r\nFGHIJ\r\nKLMNO"); - REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); - screen.moveCursorTo({3, 2}); - REQUIRE(screen.cursorPosition() == Coordinate{3, 2}); + REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); + screen.moveCursorTo(LineOffset{2}, ColumnOffset{1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(2), ColumnOffset(1)}); SECTION("no-op") { screen.moveCursorUp(LineCount(0)); - REQUIRE(screen.cursorPosition() == Coordinate{3, 2}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(2), ColumnOffset(1)}); } SECTION("in-range") { screen.moveCursorUp(LineCount(1)); - REQUIRE(screen.cursorPosition() == Coordinate{2, 2}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(1)}); } SECTION("overflow") { screen.moveCursorUp(LineCount(5)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 2}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(1)}); } SECTION("with margins") { - screen.setTopBottomMargin(2, 4); - screen.moveCursorTo({3, 2}); - REQUIRE(screen.cursorPosition() == Coordinate{3, 2}); + screen.setTopBottomMargin(LineOffset{1}, LineOffset{3}); + screen.moveCursorTo(LineOffset{2}, ColumnOffset{1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(2), ColumnOffset(1)}); SECTION("in-range") { screen.moveCursorUp(LineCount(1)); - REQUIRE(screen.cursorPosition() == Coordinate{2, 2}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(1)}); } SECTION("overflow") { screen.moveCursorUp(LineCount(5)); - REQUIRE(screen.cursorPosition() == Coordinate{2, 2}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(1)}); } } SECTION("cursor already above margins") { - screen.setTopBottomMargin(3, 4); - screen.moveCursorTo({2, 3}); + screen.setTopBottomMargin(LineOffset{2}, LineOffset{3}); + screen.moveCursorTo(LineOffset{1}, ColumnOffset{2}); screen.moveCursorUp(LineCount(1)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 3}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(2)}); } } @@ -1002,44 +1147,44 @@ TEST_CASE("MoveCursorDown", "[screen]") { auto screen = MockScreen{PageSize{LineCount(3), ColumnCount(2)}}; screen.write("A"); - REQUIRE(screen.cursorPosition() == Coordinate{1, 2}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(1)}); // no-op screen.moveCursorDown(LineCount(0)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 2}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(1)}); // in-range screen.moveCursorDown(LineCount(1)); - REQUIRE(screen.cursorPosition() == Coordinate{2, 2}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(1)}); // overflow screen.moveCursorDown(LineCount(5)); - REQUIRE(screen.cursorPosition() == Coordinate{3, 2}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(2), ColumnOffset(1)}); } TEST_CASE("MoveCursorForward", "[screen]") { auto screen = MockScreen{PageSize{LineCount(3), ColumnCount(3)}}; - REQUIRE(screen.cursorPosition() == Coordinate{1, 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); SECTION("no-op") { screen.moveCursorForward(ColumnCount(0)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); } SECTION("CUF-1") { screen.moveCursorForward(ColumnCount(1)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 2}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(1)}); } SECTION("CUF-3 (to right border)") { screen.moveCursorForward(screen.size().columns); - REQUIRE(screen.cursorPosition() == Coordinate{1, static_cast(screen.size().columns.value)}); + REQUIRE(screen.cursorPosition().column.value == screen.size().columns.value - 1); } SECTION("CUF-overflow") { screen.moveCursorForward(screen.size().columns + ColumnCount(1)); - REQUIRE(screen.cursorPosition() == Coordinate{1, static_cast(screen.size().columns.value)}); + REQUIRE(screen.cursorPosition().column.value == screen.size().columns.value - 1); } } @@ -1047,116 +1192,115 @@ TEST_CASE("MoveCursorBackward", "[screen]") { auto screen = MockScreen{PageSize{LineCount(3), ColumnCount(3)}}; screen.write("ABC"); - REQUIRE(screen.cursorPosition() == Coordinate{1, 3}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(2)}); // no-op screen.moveCursorBackward(ColumnCount(0)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 3}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(2)}); // in-range screen.moveCursorBackward(ColumnCount(1)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 2}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(1)}); // overflow screen.moveCursorBackward(ColumnCount(5)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); } TEST_CASE("HorizontalPositionAbsolute", "[screen]") { auto screen = MockScreen{PageSize{LineCount(3), ColumnCount(3)}}; - REQUIRE(screen.cursorPosition() == Coordinate{1, 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); // no-op - screen.moveCursorToColumn(ColumnPosition(1)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 1}); + screen.moveCursorToColumn(ColumnOffset(0)); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); // in-range - screen.moveCursorToColumn(ColumnPosition(3)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 3}); + screen.moveCursorToColumn(ColumnOffset(2)); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(2)}); - screen.moveCursorToColumn(ColumnPosition(2)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 2}); + screen.moveCursorToColumn(ColumnOffset(1)); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(1)}); // overflow - screen.moveCursorToColumn(ColumnPosition(5)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 3 /*clamped*/}); + screen.moveCursorToColumn(ColumnOffset(4)); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(2)/*clamped*/}); } TEST_CASE("HorizontalPositionRelative", "[screen]") { auto screen = MockScreen{PageSize{LineCount(3), ColumnCount(3)}}; - REQUIRE(screen.cursorPosition() == Coordinate{1, 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); SECTION("no-op") { screen.moveCursorForward(ColumnCount(0)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); } SECTION("HPR-1") { screen.moveCursorForward(ColumnCount(1)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 2}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(1)}); } SECTION("HPR-3 (to right border)") { - screen.moveCursorForward(screen.size().columns); - REQUIRE(screen.cursorPosition() == Coordinate{1, unbox(screen.size().columns)}); + screen.moveCursorForward(screen.size().columns - 1); + REQUIRE(screen.cursorPosition().column.value == screen.size().columns.value - 1); } SECTION("HPR-overflow") { - screen.moveCursorForward(screen.size().columns + ColumnCount(1)); - REQUIRE(screen.cursorPosition() == Coordinate{1, unbox(screen.size().columns)}); + screen.moveCursorForward(screen.size().columns); + REQUIRE(screen.cursorPosition().column.value == screen.size().columns.value - 1); } } - TEST_CASE("MoveCursorToColumn", "[screen]") { auto screen = MockScreen{PageSize{LineCount(3), ColumnCount(3)}}; - REQUIRE(screen.cursorPosition() == Coordinate{1, 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); // no-op - screen.moveCursorToColumn(ColumnPosition(1)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 1}); + screen.moveCursorToColumn(ColumnOffset(0)); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); // in-range - screen.moveCursorToColumn(ColumnPosition(3)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 3}); + screen.moveCursorToColumn(ColumnOffset(2)); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(2)}); - screen.moveCursorToColumn(ColumnPosition(2)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 2}); + screen.moveCursorToColumn(ColumnOffset(1)); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(1)}); // overflow - screen.moveCursorToColumn(ColumnPosition(5)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 3 /*clamped*/}); + screen.moveCursorToColumn(ColumnOffset(3)); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(2) /*clamped*/}); SECTION("with wide character") { - screen.moveCursorTo({1, 1}); - REQUIRE(screen.cursorPosition() == Coordinate{1, 1}); + screen.moveCursorTo({}, {}); + REQUIRE(screen.cursorPosition().column.value == 0); screen.write(U"\u26A1"); // ⚡ :flash: (double width) - REQUIRE(screen.cursorPosition() == Coordinate{1, 3}); + REQUIRE(screen.cursorPosition().column.value == 2); } } TEST_CASE("MoveCursorToLine", "[screen]") { auto screen = MockScreen{PageSize{LineCount(3), ColumnCount(3)}}; - REQUIRE(screen.cursorPosition() == Coordinate{1, 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); // no-op - screen.moveCursorToLine(LinePosition(0)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 1}); + screen.moveCursorToLine(LineOffset(0)); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); // in-range - screen.moveCursorToLine(LinePosition(3)); - REQUIRE(screen.cursorPosition() == Coordinate{3, 1}); + screen.moveCursorToLine(LineOffset(2)); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(2), ColumnOffset(0)}); - screen.moveCursorToLine(LinePosition(2)); - REQUIRE(screen.cursorPosition() == Coordinate{2, 1}); + screen.moveCursorToLine(LineOffset(1)); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(0)}); // overflow - screen.moveCursorToLine(LinePosition(5)); - REQUIRE(screen.cursorPosition() == Coordinate{3, 1/*clamped*/}); + screen.moveCursorToLine(LineOffset(3)); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(2/*clamped*/), ColumnOffset(0)}); } TEST_CASE("MoveCursorToBeginOfLine", "[screen]") @@ -1164,51 +1308,51 @@ TEST_CASE("MoveCursorToBeginOfLine", "[screen]") auto screen = MockScreen{PageSize{LineCount(3), ColumnCount(3)}}; screen.write("\r\nAB"); - REQUIRE(screen.cursorPosition() == Coordinate{2, 3}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(2)}); screen.moveCursorToBeginOfLine(); - REQUIRE(screen.cursorPosition() == Coordinate{2, 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(0)}); } TEST_CASE("MoveCursorTo", "[screen]") { auto screen = MockScreen{PageSize{LineCount(5), ColumnCount(5)}}; screen.write("12345\r\n67890\r\nABCDE\r\nFGHIJ\r\nKLMNO"); - REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); + REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); SECTION("origin mode disabled") { SECTION("in range") { - screen.moveCursorTo({3, 2}); - REQUIRE(screen.cursorPosition() == Coordinate{3, 2}); + screen.moveCursorTo(LineOffset{2}, ColumnOffset{1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(2), ColumnOffset(1)}); } SECTION("origin") { - screen.moveCursorTo({1, 1}); - REQUIRE(screen.cursorPosition() == Coordinate{1, 1}); + screen.moveCursorTo(LineOffset{0}, ColumnOffset{0}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); } SECTION("clamped") { - screen.moveCursorTo({6, 7}); - REQUIRE(screen.cursorPosition() == Coordinate{5, 5}); + screen.moveCursorTo(LineOffset{5}, ColumnOffset{5}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(4), ColumnOffset(4)}); } } SECTION("origin-mode enabled") { - constexpr auto TopMargin = 2; - constexpr auto BottomMargin = 4; - constexpr auto LeftMargin = 2; - constexpr auto RightMargin = 4; + constexpr auto TopMargin = LineOffset(1); + constexpr auto BottomMargin = LineOffset(3); + constexpr auto LeftMargin = ColumnOffset(1); + constexpr auto RightMargin = ColumnOffset(3); screen.setMode(DECMode::LeftRightMargin, true); screen.setLeftRightMargin(LeftMargin, RightMargin); screen.setTopBottomMargin(TopMargin, BottomMargin); screen.setMode(DECMode::Origin, true); SECTION("move to origin") { - screen.moveCursorTo({1, 1}); - CHECK(Coordinate{1, 1} == screen.cursorPosition()); - CHECK(Coordinate{2, 2} == screen.realCursorPosition()); - CHECK('7' == (char)screen.at({1 + (TopMargin - 1), 1 + (LeftMargin - 1)}).codepoint(0)); - CHECK('I' == (char)screen.at({3 + (TopMargin - 1), 3 + (LeftMargin - 1)}).codepoint(0)); + screen.moveCursorTo({}, {}); + CHECK(Coordinate{LineOffset(0), ColumnOffset(0)} == screen.cursorPosition()); + CHECK(Coordinate{LineOffset(1), ColumnOffset(1)} == screen.realCursorPosition()); + CHECK('7' == (char)screen.at({TopMargin + 0, LeftMargin + 0}).codepoint(0)); + CHECK('I' == (char)screen.at({TopMargin + 2, LeftMargin + 2}).codepoint(0)); } } } @@ -1218,28 +1362,28 @@ TEST_CASE("MoveCursorToNextTab", "[screen]") auto constexpr TabWidth = 8; auto screen = MockScreen{PageSize{LineCount(3), ColumnCount(20)}}; screen.moveCursorToNextTab(); - REQUIRE(screen.cursorPosition() == Coordinate{1, 1 * TabWidth + 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(1 * TabWidth + 0)}); - screen.moveCursorToColumn(ColumnPosition(TabWidth - 1)); + screen.moveCursorToColumn(ColumnOffset(TabWidth - 1)); screen.moveCursorToNextTab(); - REQUIRE(screen.cursorPosition() == Coordinate{1, 1 * TabWidth + 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(1 * TabWidth + 0)}); - screen.moveCursorToColumn(ColumnPosition(TabWidth)); + screen.moveCursorToColumn(ColumnOffset(TabWidth - 1)); screen.moveCursorToNextTab(); - REQUIRE(screen.cursorPosition() == Coordinate{1, 1 * TabWidth + 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(1 * TabWidth + 0)}); screen.moveCursorToNextTab(); - REQUIRE(screen.cursorPosition() == Coordinate{1, 2 * TabWidth + 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(2 * TabWidth + 0)}); screen.moveCursorToNextTab(); - REQUIRE(screen.cursorPosition() == Coordinate{1, 20}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(19)}); screen.setMode(DECMode::AutoWrap, true); screen.write("A"); // 'A' is being written at the right margin screen.write("B"); // force wrap to next line, writing 'B' at the beginning of the line screen.moveCursorToNextTab(); - REQUIRE(screen.cursorPosition() == Coordinate{2, 9}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(8)}); } // TODO: HideCursor @@ -1252,13 +1396,13 @@ TEST_CASE("SaveCursor and RestoreCursor", "[screen]") screen.saveCursor(); // mutate the cursor's position, autowrap and origin flags - screen.moveCursorTo({3, 3}); + screen.moveCursorTo(LineOffset{2}, ColumnOffset{2}); screen.setMode(DECMode::AutoWrap, true); screen.setMode(DECMode::Origin, true); // restore cursor and see if the changes have been reverted screen.restoreCursor(); - CHECK(screen.cursorPosition() == Coordinate{1, 1}); + CHECK(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); CHECK_FALSE(screen.isModeEnabled(DECMode::AutoWrap)); CHECK_FALSE(screen.isModeEnabled(DECMode::Origin)); } @@ -1268,28 +1412,28 @@ TEST_CASE("Index_outside_margin", "[screen]") auto screen = MockScreen{PageSize{LineCount(6), ColumnCount(4)}}; screen.write("1234\r\n5678\r\nABCD\r\nEFGH\r\nIJKL\r\nMNOP"); logScreenText(screen, "initial"); - REQUIRE("1234\n5678\nABCD\nEFGH\nIJKL\nMNOP\n" == screen.renderText()); - screen.setTopBottomMargin(2, 4); + REQUIRE("1234\n5678\nABCD\nEFGH\nIJKL\nMNOP\n" == screen.renderMainPageText()); + screen.setTopBottomMargin(LineOffset{1}, LineOffset{3}); // with cursor above top margin - screen.moveCursorTo({1, 3}); - REQUIRE(screen.cursorPosition() == Coordinate{1, 3}); + screen.moveCursorTo(LineOffset{0}, ColumnOffset{2}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(2)}); screen.index(); - REQUIRE("1234\n5678\nABCD\nEFGH\nIJKL\nMNOP\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{2, 3}); + REQUIRE("1234\n5678\nABCD\nEFGH\nIJKL\nMNOP\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(2)}); // with cursor below bottom margin and above bottom screen (=> only moves cursor one down) - screen.moveCursorTo({5, 3}); + screen.moveCursorTo(LineOffset{4}, ColumnOffset{2}); screen.index(); - REQUIRE("1234\n5678\nABCD\nEFGH\nIJKL\nMNOP\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{6, 3}); + REQUIRE("1234\n5678\nABCD\nEFGH\nIJKL\nMNOP\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(5), ColumnOffset(2)}); // with cursor below bottom margin and at bottom screen (=> no-op) - screen.moveCursorTo({6, 3}); + screen.moveCursorTo(LineOffset{5}, ColumnOffset{2}); screen.index(); - REQUIRE("1234\n5678\nABCD\nEFGH\nIJKL\nMNOP\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{6, 3}); + REQUIRE("1234\n5678\nABCD\nEFGH\nIJKL\nMNOP\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(5), ColumnOffset(2)}); } TEST_CASE("Index_inside_margin", "[screen]") @@ -1299,12 +1443,12 @@ TEST_CASE("Index_inside_margin", "[screen]") logScreenText(screen, "initial setup"); // test IND when cursor is within margin range (=> move cursor down) - screen.setTopBottomMargin(2, 4); - screen.moveCursorTo({3, 2}); + screen.setTopBottomMargin(LineOffset{1}, LineOffset{3}); + screen.moveCursorTo(LineOffset{2}, ColumnOffset{1}); screen.index(); logScreenText(screen, "IND while cursor at line 3"); - REQUIRE(screen.cursorPosition() == Coordinate{4, 2}); - REQUIRE("11\n22\n33\n44\n55\n66\n" == screen.renderText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(3), ColumnOffset(1)}); + REQUIRE("11\n22\n33\n44\n55\n66\n" == screen.renderMainPageText()); } TEST_CASE("Index_at_bottom_margin", "[screen]") @@ -1312,29 +1456,29 @@ TEST_CASE("Index_at_bottom_margin", "[screen]") auto screen = MockScreen{PageSize{LineCount(5), ColumnCount(5)}}; screen.write("12345\r\n67890\r\nABCDE\r\nFGHIJ\r\nKLMNO"); logScreenText(screen, "initial setup"); - REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); + REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); - screen.setTopBottomMargin(2, 4); + screen.setTopBottomMargin(LineOffset{1}, LineOffset{3}); SECTION("cursor at bottom margin and full horizontal margins") { - screen.moveCursorTo({4, 2}); + screen.moveCursorTo(LineOffset{3}, ColumnOffset{1}); screen.index(); logScreenText(screen, "IND while cursor at bottom margin"); - REQUIRE(screen.cursorPosition() == Coordinate{4, 2}); - REQUIRE("12345\nABCDE\nFGHIJ\n \nKLMNO\n" == screen.renderText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(3), ColumnOffset(1)}); + REQUIRE("12345\nABCDE\nFGHIJ\n \nKLMNO\n" == screen.renderMainPageText()); } SECTION("cursor at bottom margin and NOT full horizontal margins") { - screen.moveCursorTo({1, 1}); + screen.moveCursorTo(LineOffset{0}, ColumnOffset{0}); screen.setMode(DECMode::LeftRightMargin, true); - screen.setLeftRightMargin(2, 4); - screen.setTopBottomMargin(2, 4); - screen.moveCursorTo({4, 2}); // cursor at bottom margin - REQUIRE(screen.cursorPosition() == Coordinate{4, 2}); + screen.setLeftRightMargin(ColumnOffset{1}, ColumnOffset{3}); + screen.setTopBottomMargin(LineOffset{1}, LineOffset{3}); + screen.moveCursorTo(LineOffset{3}, ColumnOffset{1}); // cursor at bottom margin + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(3), ColumnOffset(1)}); screen.index(); - CHECK("12345\n6BCD0\nAGHIE\nF J\nKLMNO\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{4, 2}); + CHECK("12345\n6BCD0\nAGHIE\nF J\nKLMNO\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(3), ColumnOffset(1)}); } } @@ -1343,31 +1487,31 @@ TEST_CASE("ReverseIndex_without_custom_margins", "[screen]") auto screen = MockScreen{PageSize{LineCount(5), ColumnCount(5)}}; screen.write("12345\r\n67890\r\nABCDE\r\nFGHIJ\r\nKLMNO"); logScreenText(screen, "initial"); - REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); + REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); // at bottom screen - screen.moveCursorTo({5, 2}); + screen.moveCursorTo(LineOffset{4}, ColumnOffset{1}); screen.reverseIndex(); - REQUIRE(screen.cursorPosition() == Coordinate{4, 2}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(3), ColumnOffset(1)}); screen.reverseIndex(); - REQUIRE(screen.cursorPosition() == Coordinate{3, 2}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(2), ColumnOffset(1)}); screen.reverseIndex(); - REQUIRE(screen.cursorPosition() == Coordinate{2, 2}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(1)}); screen.reverseIndex(); - REQUIRE(screen.cursorPosition() == Coordinate{1, 2}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(1)}); screen.reverseIndex(); logScreenText(screen, "RI at top screen"); - REQUIRE(" \n12345\n67890\nABCDE\nFGHIJ\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{1, 2}); + REQUIRE(" \n12345\n67890\nABCDE\nFGHIJ\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(1)}); screen.reverseIndex(); logScreenText(screen, "RI at top screen"); - REQUIRE(" \n \n12345\n67890\nABCDE\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{1, 2}); + REQUIRE(" \n \n12345\n67890\nABCDE\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(1)}); } TEST_CASE("ReverseIndex_with_vertical_margin", "[screen]") @@ -1375,52 +1519,52 @@ TEST_CASE("ReverseIndex_with_vertical_margin", "[screen]") auto screen = MockScreen{PageSize{LineCount(5), ColumnCount(5)}}; screen.write("12345\r\n67890\r\nABCDE\r\nFGHIJ\r\nKLMNO"); logScreenText(screen, "initial"); - REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); + REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); - screen.setTopBottomMargin(2, 4); + screen.setTopBottomMargin(LineOffset{1}, LineOffset{3}); // below bottom margin - screen.moveCursorTo({5, 2}); + screen.moveCursorTo(LineOffset{4}, ColumnOffset{1}); screen.reverseIndex(); logScreenText(screen, "RI below bottom margin"); - REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{4, 2}); + REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(3), ColumnOffset(1)}); // at bottom margin screen.reverseIndex(); logScreenText(screen, "RI at bottom margin"); - REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{3, 2}); + REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(2), ColumnOffset(1)}); screen.reverseIndex(); logScreenText(screen, "RI middle margin"); - REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{2, 2}); + REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(1)}); // at top margin screen.reverseIndex(); logScreenText(screen, "RI at top margin #1"); - REQUIRE("12345\n \n67890\nABCDE\nKLMNO\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{2, 2}); + REQUIRE("12345\n \n67890\nABCDE\nKLMNO\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(1)}); // at top margin (again) screen.reverseIndex(); logScreenText(screen, "RI at top margin #2"); - REQUIRE("12345\n \n \n67890\nKLMNO\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{2, 2}); + REQUIRE("12345\n \n \n67890\nKLMNO\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(1)}); // above top margin - screen.moveCursorTo({1, 2}); + screen.moveCursorTo(LineOffset{0}, ColumnOffset{1}); screen.reverseIndex(); logScreenText(screen, "RI above top margin"); - REQUIRE("12345\n \n \n67890\nKLMNO\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{1, 2}); + REQUIRE("12345\n \n \n67890\nKLMNO\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(1)}); // above top margin (top screen) => no-op screen.reverseIndex(); logScreenText(screen, "RI above top margin (top-screen)"); - REQUIRE("12345\n \n \n67890\nKLMNO\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{1, 2}); + REQUIRE("12345\n \n \n67890\nKLMNO\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(1)}); } TEST_CASE("ReverseIndex_with_vertical_and_horizontal_margin", "[screen]") @@ -1428,70 +1572,70 @@ TEST_CASE("ReverseIndex_with_vertical_and_horizontal_margin", "[screen]") auto screen = MockScreen{PageSize{LineCount(5), ColumnCount(5)}}; screen.write("12345\r\n67890\r\nABCDE\r\nFGHIJ\r\nKLMNO"); logScreenText(screen, "initial"); - REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); + REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); screen.setMode(DECMode::LeftRightMargin, true); - screen.setLeftRightMargin(2, 4); - screen.setTopBottomMargin(2, 4); + screen.setLeftRightMargin(ColumnOffset{1}, ColumnOffset{3}); + screen.setTopBottomMargin(LineOffset{1}, LineOffset{3}); // below bottom margin - screen.moveCursorTo({5, 2}); + screen.moveCursorTo(LineOffset{4}, ColumnOffset{1}); screen.reverseIndex(); - REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{4, 2}); + REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(3), ColumnOffset(1)}); // at bottom margin screen.reverseIndex(); logScreenText(screen, "after RI at bottom margin"); - REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{3, 2}); + REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(2), ColumnOffset(1)}); screen.reverseIndex(); logScreenText(screen, "after RI at bottom margin (again)"); - REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{2, 2}); + REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(1)}); // at top margin screen.reverseIndex(); logScreenText(screen, "after RI at top margin"); - REQUIRE(screen.cursorPosition() == Coordinate{2, 2}); - REQUIRE("12345\n6 0\nA789E\nFBCDJ\nKLMNO\n" == screen.renderText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(1)}); + REQUIRE("12345\n6 0\nA789E\nFBCDJ\nKLMNO\n" == screen.renderMainPageText()); // at top margin (again) screen.reverseIndex(); logScreenText(screen, "after RI at top margin (again)"); - REQUIRE("12345\n6 0\nA E\nF789J\nKLMNO\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{2, 2}); + REQUIRE("12345\n6 0\nA E\nF789J\nKLMNO\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(1)}); // above top margin - screen.moveCursorTo({1, 2}); + screen.moveCursorTo(LineOffset{0}, ColumnOffset{1}); screen.reverseIndex(); - REQUIRE("12345\n6 0\nA E\nF789J\nKLMNO\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{1, 2}); + REQUIRE("12345\n6 0\nA E\nF789J\nKLMNO\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(1)}); } TEST_CASE("ScreenAlignmentPattern", "[screen]") { auto screen = MockScreen{PageSize{LineCount(5), ColumnCount(5)}}; screen.write("12345\r\n67890\r\nABCDE\r\nFGHIJ\r\nKLMNO"); - screen.setTopBottomMargin(2, 4); - REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); + screen.setTopBottomMargin(LineOffset{1}, LineOffset{3}); + REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); - REQUIRE(screen.cursorPosition() == Coordinate{1, 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); - REQUIRE(2 == screen.margin().vertical.from); - REQUIRE(4 == screen.margin().vertical.to); + REQUIRE(1 == *screen.margin().vertical.from); + REQUIRE(3 == *screen.margin().vertical.to); SECTION("test") { screen.screenAlignmentPattern(); - REQUIRE("EEEEE\nEEEEE\nEEEEE\nEEEEE\nEEEEE\n" == screen.renderText()); + REQUIRE("EEEEE\nEEEEE\nEEEEE\nEEEEE\nEEEEE\n" == screen.renderMainPageText()); - REQUIRE(screen.cursorPosition() == Coordinate{1, 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); - REQUIRE(1 == screen.margin().horizontal.from); - REQUIRE(5 == screen.margin().horizontal.to); - REQUIRE(1 == screen.margin().vertical.from); - REQUIRE(5 == screen.margin().vertical.to); + REQUIRE(0 == *screen.margin().horizontal.from); + REQUIRE(4 == *screen.margin().horizontal.to); + REQUIRE(0 == *screen.margin().vertical.from); + REQUIRE(4 == *screen.margin().vertical.to); } } @@ -1499,49 +1643,49 @@ TEST_CASE("CursorNextLine", "[screen]") { auto screen = MockScreen{PageSize{LineCount(5), ColumnCount(5)}}; screen.write("12345\r\n67890\r\nABCDE\r\nFGHIJ\r\nKLMNO"); - screen.moveCursorTo({2, 3}); + screen.moveCursorTo(LineOffset{1}, ColumnOffset{2}); - REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{2, 3}); + REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(2)}); SECTION("without margins") { SECTION("normal") { screen.moveCursorToNextLine(LineCount(1)); - REQUIRE(screen.cursorPosition() == Coordinate{3, 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(2), ColumnOffset(0)}); } SECTION("clamped") { screen.moveCursorToNextLine(LineCount(5)); - REQUIRE(screen.cursorPosition() == Coordinate{5, 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(4), ColumnOffset(0)}); } } SECTION("with margins") { screen.setMode(DECMode::LeftRightMargin, true); - screen.setLeftRightMargin(2, 4); - screen.setTopBottomMargin(2, 4); + screen.setLeftRightMargin(ColumnOffset{1}, ColumnOffset{3}); + screen.setTopBottomMargin(LineOffset{1}, LineOffset{3}); screen.setMode(DECMode::Origin, true); - screen.moveCursorTo({1, 2}); + screen.moveCursorTo(LineOffset{0}, ColumnOffset{1}); REQUIRE(screen.currentCell().toUtf8() == "8"); SECTION("normal-1") { screen.moveCursorToNextLine(LineCount(1)); - REQUIRE(screen.cursorPosition() == Coordinate{2, 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(0)}); } SECTION("normal-2") { screen.moveCursorToNextLine(LineCount(2)); - REQUIRE(screen.cursorPosition() == Coordinate{3, 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(2), ColumnOffset(0)}); } SECTION("normal-3") { screen.moveCursorToNextLine(LineCount(3)); - REQUIRE(screen.cursorPosition() == Coordinate{4, 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(3), ColumnOffset(0)}); } SECTION("clamped-1") { screen.moveCursorToNextLine(LineCount(4)); - REQUIRE(screen.cursorPosition() == Coordinate{4, 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(3), ColumnOffset(0)}); } } } @@ -1551,42 +1695,42 @@ TEST_CASE("CursorPreviousLine", "[screen]") auto screen = MockScreen{PageSize{LineCount(5), ColumnCount(5)}}; screen.write("12345\r\n67890\r\nABCDE\r\nFGHIJ\r\nKLMNO"); - REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{5, 5}); + REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(4), ColumnOffset(4)}); SECTION("without margins") { SECTION("normal") { screen.moveCursorToPrevLine(LineCount(1)); - REQUIRE(screen.cursorPosition() == Coordinate{4, 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(3), ColumnOffset(0)}); } SECTION("clamped") { screen.moveCursorToPrevLine(LineCount(5)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); } } SECTION("with margins") { screen.setMode(DECMode::LeftRightMargin, true); - screen.setLeftRightMargin(2, 4); - screen.setTopBottomMargin(2, 4); + screen.setLeftRightMargin(ColumnOffset{1}, ColumnOffset{3}); + screen.setTopBottomMargin(LineOffset{1}, LineOffset{3}); screen.setMode(DECMode::Origin, true); - screen.moveCursorTo({3, 3}); - REQUIRE(screen.cursorPosition() == Coordinate{3, 3}); + screen.moveCursorTo(LineOffset{2}, ColumnOffset{2}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(2), ColumnOffset(2)}); SECTION("normal-1") { screen.moveCursorToPrevLine(LineCount(1)); - REQUIRE(screen.cursorPosition() == Coordinate{2, 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(0)}); } SECTION("normal-2") { screen.moveCursorToPrevLine(LineCount(2)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); } SECTION("clamped") { screen.moveCursorToPrevLine(LineCount(3)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 1}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); } } } @@ -1595,11 +1739,11 @@ TEST_CASE("ReportCursorPosition", "[screen]") { auto screen = MockScreen{PageSize{LineCount(5), ColumnCount(5)}}; screen.write("12345\r\n67890\r\nABCDE\r\nFGHIJ\r\nKLMNO"); - screen.moveCursorTo({2, 3}); + screen.moveCursorTo(LineOffset{1}, ColumnOffset{2}); - REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); + REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); REQUIRE("" == screen.replyData); - REQUIRE(screen.cursorPosition() == Coordinate{2, 3}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(2)}); SECTION("with Origin mode disabled") { screen.reportCursorPosition(); @@ -1608,10 +1752,10 @@ TEST_CASE("ReportCursorPosition", "[screen]") SECTION("with margins and origin mode enabled") { screen.setMode(DECMode::LeftRightMargin, true); - screen.setTopBottomMargin(2, 4); - screen.setLeftRightMargin(2, 4); + screen.setLeftRightMargin(ColumnOffset{1}, ColumnOffset{3}); + screen.setTopBottomMargin(LineOffset{1}, LineOffset{3}); screen.setMode(DECMode::Origin, true); - screen.moveCursorTo({3, 2}); + screen.moveCursorTo(LineOffset{2}, ColumnOffset{1}); screen.reportCursorPosition(); CHECK("\033[3;2R" == screen.replyData); @@ -1622,11 +1766,11 @@ TEST_CASE("ReportExtendedCursorPosition", "[screen]") { auto screen = MockScreen{PageSize{LineCount(5), ColumnCount(5)}}; screen.write("12345\r\n67890\r\nABCDE\r\nFGHIJ\r\nKLMNO"); - screen.moveCursorTo({2, 3}); + screen.moveCursorTo(LineOffset{1}, ColumnOffset{2}); - REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderText()); + REQUIRE("12345\n67890\nABCDE\nFGHIJ\nKLMNO\n" == screen.renderMainPageText()); REQUIRE("" == screen.replyData); - REQUIRE(screen.cursorPosition() == Coordinate{2, 3}); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(2)}); SECTION("with Origin mode disabled") { screen.reportExtendedCursorPosition(); @@ -1635,10 +1779,10 @@ TEST_CASE("ReportExtendedCursorPosition", "[screen]") SECTION("with margins and origin mode enabled") { screen.setMode(DECMode::LeftRightMargin, true); - screen.setTopBottomMargin(2, 4); - screen.setLeftRightMargin(2, 4); + screen.setLeftRightMargin(ColumnOffset{1}, ColumnOffset{3}); + screen.setTopBottomMargin(LineOffset{1}, LineOffset{3}); screen.setMode(DECMode::Origin, true); - screen.moveCursorTo({3, 2}); + screen.moveCursorTo(LineOffset{2}, ColumnOffset{1}); screen.reportExtendedCursorPosition(); CHECK("\033[3;2;1R" == screen.replyData); @@ -1650,13 +1794,13 @@ TEST_CASE("SetMode", "[screen]") { auto screen = MockScreen{PageSize{LineCount(5), ColumnCount(5)}}; screen.setMode(AnsiMode::AutomaticNewLine, true); screen.write("12345\n67890\nABCDE\nFGHIJ\nKLMNO"); - REQUIRE(screen.renderText() == "12345\n67890\nABCDE\nFGHIJ\nKLMNO\n"); + REQUIRE(screen.renderMainPageText() == "12345\n67890\nABCDE\nFGHIJ\nKLMNO\n"); } SECTION("Auto NewLine Mode: Disabled") { auto screen = MockScreen{PageSize{LineCount(3), ColumnCount(3)}}; screen.write("A\nB\nC"); - REQUIRE(screen.renderText() == "A \n B \n C\n"); + REQUIRE(screen.renderMainPageText() == "A \n B \n C\n"); } } @@ -1705,94 +1849,95 @@ TEST_CASE("RequestMode", "[screen]") TEST_CASE("peek into history", "[screen]") { - auto screen = MockScreen{PageSize{LineCount(2), ColumnCount(3)}}; + auto screen = MockScreen{PageSize{LineCount(2), ColumnCount(3)}, LineCount{5}}; screen.write("123\r\n456\r\nABC\r\nDEF"); - REQUIRE("ABC\nDEF\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{2, 3}); + REQUIRE("ABC\nDEF\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(2)}); // first line in history - auto const m1 = screen.renderTextLine(-1); - CHECK(screen.renderTextLine(-1) == "123"); + auto const m1 = screen.grid().renderTextLine(LineOffset(-2)); + CHECK(screen.grid().renderTextLine(LineOffset(-2)) == "123"); // second line in history - CHECK(screen.renderTextLine(0) == "456"); + CHECK(screen.grid().renderTextLine(LineOffset(-1)) == "456"); // first line on screen buffer - CHECK(screen.renderTextLine(1) == "ABC"); + CHECK(screen.grid().renderTextLine(LineOffset(0)) == "ABC"); // second line on screen buffer - CHECK(screen.renderTextLine(2) == "DEF"); + CHECK(screen.grid().renderTextLine(LineOffset(1)) == "DEF"); // out-of-range corner cases - // CHECK_THROWS(screen.at({3, 1})); - // CHECK_THROWS(screen.at({2, 4})); - // CHECK_THROWS(screen.at({2, 0})); + // CHECK_THROWS(screen.at(LineOffset(2), ColumnOffset(0))); + // CHECK_THROWS(screen.at(LineOffset(1), ColumnOffset(3))); + // CHECK_THROWS(screen.at({LineOffset()), ColumnOffset()-1))); // XXX currently not checked, as they're intentionally using assert() instead. } TEST_CASE("captureBuffer", "[screen]") { - auto screen = MockScreen{PageSize{LineCount(2), ColumnCount(5)}}; + auto screen = MockScreen{PageSize{LineCount(2), ColumnCount(5)}, LineCount{5}}; // [... history ... ...][main page area] screen.write("12345\r\n67890\r\nABCDE\r\nFGHIJ\r\nKLMNO"); SECTION("lines: 0") { screen.captureBuffer(0, false); - INFO(crispy::escape(screen.replyData)); - CHECK(screen.replyData == "\033]314;\033\\"); + INFO(e(screen.replyData)); + CHECK(e(screen.replyData) == e("\033]314;\033\\")); } SECTION("lines: 1") { screen.captureBuffer(1, false); - INFO(crispy::escape(screen.replyData)); - CHECK(screen.replyData == "\033]314;KLMNO\n\033\\\033]314;\033\\"); + INFO(e(screen.replyData)); + CHECK(e(screen.replyData) == e("\033]314;KLMNO\n\033\\\033]314;\033\\")); } SECTION("lines: 2") { screen.captureBuffer(2, false); - INFO(crispy::escape(screen.replyData)); - CHECK(screen.replyData == "\033]314;FGHIJ\nKLMNO\n\033\\\033]314;\033\\"); + INFO(e(screen.replyData)); + CHECK(e(screen.replyData) == e("\033]314;FGHIJ\nKLMNO\n\033\\\033]314;\033\\")); } SECTION("lines: 3") { screen.captureBuffer(3, false); - INFO(crispy::escape(screen.replyData)); - CHECK(screen.replyData == "\033]314;ABCDE\nFGHIJ\nKLMNO\n\033\\\033]314;\033\\"); + INFO(e(screen.replyData)); + CHECK(e(screen.replyData) == e("\033]314;ABCDE\nFGHIJ\nKLMNO\n\033\\\033]314;\033\\")); } SECTION("lines: 4") { screen.captureBuffer(4, false); - INFO(crispy::escape(screen.replyData)); - CHECK(screen.replyData == "\033]314;67890\nABCDE\nFGHIJ\nKLMNO\n\033\\\033]314;\033\\"); + INFO(e(screen.replyData)); + CHECK(e(screen.replyData) == e("\033]314;67890\nABCDE\nFGHIJ\nKLMNO\n\033\\\033]314;\033\\")); } SECTION("lines: 5") { screen.captureBuffer(5, false); - INFO(crispy::escape(screen.replyData)); - CHECK(screen.replyData == "\033]314;12345\n67890\nABCDE\nFGHIJ\nKLMNO\n\033\\\033]314;\033\\"); + INFO(e(screen.replyData)); + CHECK(e(screen.replyData) == e("\033]314;12345\n67890\nABCDE\nFGHIJ\nKLMNO\n\033\\\033]314;\033\\")); } SECTION("lines: 5 (+1 overflow)") { screen.captureBuffer(5, false); - INFO(crispy::escape(screen.replyData)); - CHECK(screen.replyData == "\033]314;12345\n67890\nABCDE\nFGHIJ\nKLMNO\n\033\\\033]314;\033\\"); + INFO(e(screen.replyData)); + CHECK(e(screen.replyData) == e("\033]314;12345\n67890\nABCDE\nFGHIJ\nKLMNO\n\033\\\033]314;\033\\")); } } TEST_CASE("render into history", "[screen]") { - auto screen = MockScreen{PageSize{LineCount(2), ColumnCount(5)}}; + auto screen = MockScreen{PageSize{LineCount(2), ColumnCount(5)}, LineCount{5}}; screen.write("12345\r\n67890\r\nABCDE\r\nFGHIJ\r\nKLMNO"); - REQUIRE("FGHIJ\nKLMNO\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{2, 5}); + REQUIRE("FGHIJ\nKLMNO\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(4)}); REQUIRE(screen.historyLineCount() == LineCount{3}); string renderedText; - renderedText.resize(static_cast((screen.size().columns.value + 1) * screen.size().lines.value)); - auto const renderer = [&](Coordinate pos, Cell const& cell) { - auto const offset = static_cast( - (pos.row - 1) * static_cast(*screen.size().columns + 1) - + (pos.column - 1) - ); - renderedText.at(offset) = static_cast(cell.codepoint(0)); - if (pos.column == static_cast(*screen.size().columns)) + renderedText.resize((screen.size().columns + 1).as() + * screen.size().lines.as()); + auto const renderer = [&](Cell const& cell, LineOffset _row, ColumnOffset _column) { + auto const offset = _row.as() * (screen.size().columns + 1).as() + + _column.as(); + renderedText.at(offset) = cell.codepointCount() + ? static_cast(cell.codepoint(0)) + : ' '; + if (_column == (screen.size().columns - 1).as()) renderedText.at(offset + 1) = '\n'; }; @@ -1802,17 +1947,17 @@ TEST_CASE("render into history", "[screen]") } SECTION("1 line into history") { - screen.render(renderer, StaticScrollbackPosition{2}); + screen.render(renderer, ScrollOffset{1}); REQUIRE("ABCDE\nFGHIJ\n" == renderedText); } SECTION("2 lines into history") { - screen.render(renderer, StaticScrollbackPosition{1}); + screen.render(renderer, ScrollOffset{2}); REQUIRE("67890\nABCDE\n" == renderedText); } SECTION("3 lines into history") { - screen.render(renderer, StaticScrollbackPosition{0}); + screen.render(renderer, ScrollOffset{3}); REQUIRE("12345\n67890\n" == renderedText); } } @@ -1825,41 +1970,41 @@ TEST_CASE("HorizontalTabClear.AllTabs", "[screen]") screen.writeText('X'); screen.moveCursorToNextTab(); screen.writeText('Y'); - REQUIRE("X Y" == screen.renderTextLine(1)); + REQUIRE("X Y" == screen.grid().renderTextLine(LineOffset(0))); screen.moveCursorToNextTab(); screen.writeText('Z'); - REQUIRE("X Y" == screen.renderTextLine(1)); - REQUIRE("Z " == screen.renderTextLine(2)); + REQUIRE("X Y" == screen.grid().renderTextLine(LineOffset(0))); + REQUIRE("Z " == screen.grid().renderTextLine(LineOffset(1))); screen.moveCursorToNextTab(); screen.writeText('A'); - REQUIRE("X Y" == screen.renderTextLine(1)); - REQUIRE("Z A" == screen.renderTextLine(2)); + REQUIRE("X Y" == screen.grid().renderTextLine(LineOffset(0))); + REQUIRE("Z A" == screen.grid().renderTextLine(LineOffset(1))); } TEST_CASE("HorizontalTabClear.UnderCursor", "[screen]") { auto screen = MockScreen{PageSize{LineCount(3), ColumnCount(10)}}; - screen.setTabWidth(4); + screen.setTabWidth(ColumnCount(4)); // clear tab at column 4 - screen.moveCursorTo({1, 4}); + screen.moveCursorTo(LineOffset{0}, ColumnOffset{3}); screen.horizontalTabClear(HorizontalTabClear::UnderCursor); - screen.moveCursorTo({1, 1}); + screen.moveCursorTo({}, {}); screen.writeText('A'); screen.moveCursorToNextTab(); screen.writeText('B'); // 1234567890 - REQUIRE("A B " == screen.renderTextLine(1)); - REQUIRE(" " == screen.renderTextLine(2)); + REQUIRE("A B " == screen.grid().renderTextLine(LineOffset(0))); + REQUIRE(" " == screen.grid().renderTextLine(LineOffset(1))); screen.moveCursorToNextTab(); screen.writeText('C'); - CHECK("A B C" == screen.renderTextLine(1)); - CHECK(" " == screen.renderTextLine(2)); + CHECK("A B C" == screen.grid().renderTextLine(LineOffset(0))); + CHECK(" " == screen.grid().renderTextLine(LineOffset(1))); } TEST_CASE("HorizontalTabSet", "[screen]") @@ -1867,13 +2012,13 @@ TEST_CASE("HorizontalTabSet", "[screen]") auto screen = MockScreen{PageSize{LineCount(3), ColumnCount(10)}}; screen.horizontalTabClear(HorizontalTabClear::AllTabs); - screen.moveCursorToColumn(ColumnPosition(3)); + screen.moveCursorToColumn(ColumnOffset(2)); screen.horizontalTabSet(); - screen.moveCursorToColumn(ColumnPosition(5)); + screen.moveCursorToColumn(ColumnOffset(4)); screen.horizontalTabSet(); - screen.moveCursorToColumn(ColumnPosition(8)); + screen.moveCursorToColumn(ColumnOffset(7)); screen.horizontalTabSet(); screen.moveCursorToBeginOfLine(); @@ -1890,29 +2035,29 @@ TEST_CASE("HorizontalTabSet", "[screen]") screen.writeText('8'); screen.moveCursorToNextTab(); // capped - screen.writeText('A'); // writes B at right margin, flags for autowrap + screen.writeText('A'); // writes B at right margin, flags for autowrap - REQUIRE("1 3 5 8 A" == screen.renderTextLine(1)); + REQUIRE("1 3 5 8 A" == screen.grid().renderTextLine(LineOffset(0))); screen.moveCursorToNextTab(); // wrapped - screen.writeText('B'); // writes B at left margin + screen.writeText('B'); // writes B at left margin // 1234567890 - REQUIRE("1 3 5 8 A" == screen.renderTextLine(1)); + REQUIRE("1 3 5 8 A" == screen.grid().renderTextLine(LineOffset(0))); screen.moveCursorToNextTab(); // 1 -> 3 (overflow) screen.moveCursorToNextTab(); // 3 -> 5 screen.moveCursorToNextTab(); // 5 -> 8 screen.writeText('C'); // 1234567890 - CHECK("1 3 5 8 A" == screen.renderTextLine(1)); - CHECK("B C " == screen.renderTextLine(2)); + CHECK("1 3 5 8 A" == screen.grid().renderTextLine(LineOffset(0))); + CHECK("B C " == screen.grid().renderTextLine(LineOffset(1))); } TEST_CASE("CursorBackwardTab.fixedTabWidth", "[screen]") { auto screen = MockScreen{PageSize{LineCount(3), ColumnCount(10)}}; - screen.setTabWidth(4); // 5, 9 + screen.setTabWidth(ColumnCount(4)); // 5, 9 screen.writeText('a'); @@ -1923,43 +2068,43 @@ TEST_CASE("CursorBackwardTab.fixedTabWidth", "[screen]") screen.writeText('c'); // -> 9 // "1234567890" - REQUIRE("a b c " == screen.renderTextLine(1)); - REQUIRE(screen.cursorPosition() == Coordinate{1, 10}); + REQUIRE("a b c " == screen.grid().renderTextLine(LineOffset(0))); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(9)}); - SECTION("oveflow") { - screen.cursorBackwardTab(TabStopCount(4)); - CHECK(screen.cursorPosition() == Coordinate{1, 1}); - screen.writeText('X'); - CHECK("X b c " == screen.renderTextLine(1)); + SECTION("no op") { + screen.cursorBackwardTab(TabStopCount(0)); + CHECK(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(9)}); } - SECTION("exact") { - screen.cursorBackwardTab(TabStopCount(3)); - CHECK(screen.cursorPosition() == Coordinate{1, 1}); + SECTION("inside 1") { + screen.cursorBackwardTab(TabStopCount(1)); + CHECK(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(8)}); screen.writeText('X'); // "1234567890" - CHECK("X b c " == screen.renderTextLine(1)); + CHECK("a b X " == screen.grid().renderTextLine(LineOffset(0))); } SECTION("inside 2") { screen.cursorBackwardTab(TabStopCount(2)); - CHECK(screen.cursorPosition() == Coordinate{1, 5}); + CHECK(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(4)}); screen.writeText('X'); // "1234567890" - CHECK("a X c " == screen.renderTextLine(1)); + CHECK("a X c " == screen.grid().renderTextLine(LineOffset(0))); } - SECTION("inside 1") { - screen.cursorBackwardTab(TabStopCount(1)); - CHECK(screen.cursorPosition() == Coordinate{1, 9}); + SECTION("exact") { + screen.cursorBackwardTab(TabStopCount(3)); + CHECK(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); screen.writeText('X'); // "1234567890" - CHECK("a b X " == screen.renderTextLine(1)); + CHECK("X b c " == screen.grid().renderTextLine(LineOffset(0))); } - SECTION("no op") { - screen.cursorBackwardTab(TabStopCount(0)); - CHECK(screen.cursorPosition() == Coordinate{1, 10}); + SECTION("oveflow") { + screen.cursorBackwardTab(TabStopCount(4)); + CHECK(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); + screen.writeText('X'); + CHECK("X b c " == screen.grid().renderTextLine(LineOffset(0))); } } @@ -1967,225 +2112,155 @@ TEST_CASE("CursorBackwardTab.manualTabs", "[screen]") { auto screen = MockScreen{PageSize{LineCount(3), ColumnCount(10)}}; - screen.moveCursorToColumn(ColumnPosition(5)); + screen.moveCursorToColumn(ColumnOffset(4)); screen.horizontalTabSet(); - screen.moveCursorToColumn(ColumnPosition(9)); + screen.moveCursorToColumn(ColumnOffset(8)); screen.horizontalTabSet(); screen.moveCursorToBeginOfLine(); screen.writeText('a'); - screen.moveCursorToNextTab(); // -> 5 + screen.moveCursorToNextTab(); // -> 4 screen.writeText('b'); screen.moveCursorToNextTab(); - screen.writeText('c'); // -> 9 + screen.writeText('c'); // -> 8 // "1234567890" - REQUIRE("a b c " == screen.renderTextLine(1)); - REQUIRE(screen.cursorPosition().column == 10); + REQUIRE(screen.cursorPosition().column.value == 9); + REQUIRE("a b c " == screen.grid().renderTextLine(LineOffset(0))); SECTION("oveflow") { screen.cursorBackwardTab(TabStopCount(4)); - CHECK(screen.cursorPosition() == Coordinate{1, 1}); + CHECK(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); screen.writeText('X'); - CHECK("X b c " == screen.renderTextLine(1)); + CHECK("X b c " == screen.grid().renderTextLine(LineOffset(0))); } SECTION("exact") { screen.cursorBackwardTab(TabStopCount(3)); - CHECK(screen.cursorPosition() == Coordinate{1, 1}); + CHECK(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(0)}); screen.writeText('X'); // "1234567890" - CHECK("X b c " == screen.renderTextLine(1)); + CHECK("X b c " == screen.grid().renderTextLine(LineOffset(0))); } SECTION("inside 2") { screen.cursorBackwardTab(TabStopCount(2)); - CHECK(screen.cursorPosition() == Coordinate{1, 5}); + CHECK(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(4)}); screen.writeText('X'); // "1234567890" - CHECK("a X c " == screen.renderTextLine(1)); + CHECK("a X c " == screen.grid().renderTextLine(LineOffset(0))); } SECTION("inside 1") { screen.cursorBackwardTab(TabStopCount(1)); - CHECK(screen.cursorPosition() == Coordinate{1, 9}); + CHECK(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(8)}); screen.writeText('X'); // "1234567890" - CHECK("a b X " == screen.renderTextLine(1)); + CHECK("a b X " == screen.grid().renderTextLine(LineOffset(0))); } SECTION("no op") { screen.cursorBackwardTab(TabStopCount(0)); - CHECK(screen.cursorPosition() == Coordinate{1, 10}); - } -} - -// TEST_CASE("findNextMarker", "[screen]") -// { -// auto screen = MockScreen{PageSize{LineCount(2), ColumnCount(4)}}; -// -// //REQUIRE_FALSE(screen.findNextMarker(0).has_value()); -// -// SECTION("no marks") { -// screen.write("1abc\r\n"s); -// screen.write("2def\r\n"s); -// screen.write("3ghi\r\n"s); -// screen.write("4jkl\r\n"s); -// screen.write("5mno\r\n"s); -// -// auto marker = screen.findNextMarker(0); -// REQUIRE(marker.has_value()); -// CHECK(marker.value() == 0); -// -// // CHECK(screen.findNextMarker(0).value() == 0); -// // CHECK(screen.findNextMarker(1).value() == 0); -// // CHECK(screen.findNextMarker(2).value() == 0); -// // CHECK(screen.findNextMarker(3).value() == 0); -// // CHECK(screen.findNextMarker(4).value() == 0); -// // CHECK(screen.findNextMarker(5).value() == 0); -// } -// -// SECTION("with marks") { -// // history area -// screen.setMark(); -// screen.write("1abc\r\n"s); // 2 -// screen.setMark(); -// screen.write("2def\r\n"s); // 1 -// screen.setMark(); -// screen.write("3ghi\r\n"s); // 0 -// -// // screen area -// screen.setMark(); -// screen.write("4jkl\r\n"s); -// screen.write("5mno\r\n"s); -// -// REQUIRE(screen.renderTextLine(1) == "5mno"); -// REQUIRE(screen.renderTextLine(2) == " "); -// -// auto marker = screen.findNextMarker(0); -// CHECK(marker.value() == 0); -// -// marker = screen.findNextMarker(1); -// CHECK(marker.has_value()); -// CHECK(marker.value() == 0); // 3ghi -// -// marker = screen.findNextMarker(2); -// CHECK(marker.has_value()); -// CHECK(marker.value() == 1); // 2def -// } -// } - -TEST_CASE("findMarkerForward", "[screen]") -{ - auto screen = MockScreen{PageSize{LineCount(3), ColumnCount(4)}}; - REQUIRE_FALSE(screen.findMarkerForward(0).has_value()); // peak into history - REQUIRE_FALSE(screen.findMarkerForward(1).has_value()); - REQUIRE_FALSE(screen.findMarkerForward(2).has_value()); - REQUIRE_FALSE(screen.findMarkerForward(3).has_value()); - REQUIRE_FALSE(screen.findMarkerForward(4).has_value()); // overflow + CHECK(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(9)}); + } +} + +TEST_CASE("findMarkerDownwards", "[screen]") +{ + auto screen = MockScreen{PageSize{LineCount(3), ColumnCount(4)}, LineCount(10)}; + REQUIRE_FALSE(screen.findMarkerDownwards(ScrollOffset(0)).has_value()); + REQUIRE_FALSE(screen.findMarkerDownwards(ScrollOffset(1)).has_value()); // history bottom + REQUIRE_FALSE(screen.findMarkerDownwards(ScrollOffset(2)).has_value()); + REQUIRE_FALSE(screen.findMarkerDownwards(ScrollOffset(3)).has_value()); // history top + REQUIRE_FALSE(screen.findMarkerDownwards(ScrollOffset(4)).has_value()); // overflow SECTION("no marks") { - screen.write("1abc"sv); // 0: + - screen.write("2def"sv); // 1: | history - screen.write("3ghi"sv); // 2: + - screen.write("4jkl"sv); // 3: + - screen.write("5mno"sv); // 4: | main screen - screen.write("6pqr"sv); // 5: + + screen.write("1abc"sv); // -3: + + screen.write("2def"sv); // -2: | history + screen.write("3ghi"sv); // -1: + + screen.write("4jkl"sv); // 0: + + screen.write("5mno"sv); // 1: | main screen + screen.write("6pqr"sv); // 2: + REQUIRE(screen.historyLineCount() == LineCount{3}); - // test bottom line - auto mark = screen.findMarkerForward(5); + // overflow: one above scroll-top + auto mark = screen.findMarkerDownwards(ScrollOffset(4)); REQUIRE_FALSE(mark.has_value()); - // test one line beyond history line count - mark = screen.findMarkerForward(-1); + // scroll-top + mark = screen.findMarkerDownwards(ScrollOffset(3)); REQUIRE_FALSE(mark.has_value()); - // test last history line - mark = screen.findMarkerForward(0); + mark = screen.findMarkerDownwards(ScrollOffset(2)); REQUIRE_FALSE(mark.has_value()); - // test second-last history line - mark = screen.findMarkerForward(1); + mark = screen.findMarkerDownwards(ScrollOffset(1)); REQUIRE_FALSE(mark.has_value()); - // test first history line - mark = screen.findMarkerForward(2); + // underflow: one below scroll buttom + mark = screen.findMarkerDownwards(ScrollOffset(0)); REQUIRE_FALSE(mark.has_value()); + } SECTION("with marks") { // saved lines - screen.setMark(); // 0 + screen.setMark(); // 0 (-3) screen.write("1abc\r\n"sv); - screen.write("2def\r\n"sv); // 1 + screen.write("2def\r\n"sv); // 1 (-2) screen.setMark(); - screen.write("3ghi\r\n"sv); // 2 + screen.write("3ghi\r\n"sv); // 2 (-1) // visibile screen - screen.setMark(); // 3 + screen.setMark(); // 3 (0) screen.write("4jkl\r\n"sv); - screen.write("5mno\r\n"sv); // 4 - screen.setMark(); // 5 + screen.write("5mno\r\n"sv); // 4 (1) + screen.setMark(); // 5 (2) screen.write("6pqr"sv); - REQUIRE(screen.renderTextLine(-2) == "1abc"); - REQUIRE(screen.renderTextLine(-1) == "2def"); - REQUIRE(screen.renderTextLine(0) == "3ghi"); + logScreenTextAlways(screen); - REQUIRE(screen.renderTextLine(1) == "4jkl"); - REQUIRE(screen.renderTextLine(2) == "5mno"); - REQUIRE(screen.renderTextLine(3) == "6pqr"); + // {{{ pre-expectations + REQUIRE(screen.grid().renderTextLine(LineOffset(-3)) == "1abc"); + REQUIRE(screen.grid().renderTextLine(LineOffset(-2)) == "2def"); + REQUIRE(screen.grid().renderTextLine(LineOffset(-1)) == "3ghi"); - // ====================================================== - - // 0: -> 2 - auto marker = screen.findMarkerForward(0); - CHECK(marker.has_value()); - if (marker.has_value()) - CHECK(marker.value() == 2); // 3ghi + REQUIRE(screen.grid().renderTextLine(LineOffset(0)) == "4jkl"); + REQUIRE(screen.grid().renderTextLine(LineOffset(1)) == "5mno"); + REQUIRE(screen.grid().renderTextLine(LineOffset(2)) == "6pqr"); + // }}} - // 1: -> 2 - marker = screen.findMarkerForward(1); - CHECK(marker.has_value()); - if (marker.has_value()) - CHECK(marker.value() == 2); // 3ghi - - // 2: -> 3 - marker = screen.findMarkerForward(2); - CHECK(marker.has_value()); - if (marker.has_value()) - CHECK(marker.value() == 3); // 4jkl + // ====================================================== - // 3: -> 5 - marker = screen.findMarkerForward(3); - CHECK(marker.has_value()); - if (marker.has_value()) - CHECK(marker.value() == 5); // 6pqn + // overflow: one above scroll top -> scroll bottom + // gracefully clamps to scroll-top + auto marker = screen.findMarkerDownwards(ScrollOffset(4)); + REQUIRE(marker.has_value()); + REQUIRE(*marker.value() == 1); - // 4: -> 5 - marker = screen.findMarkerForward(4); - CHECK(marker.has_value()); - if (marker.has_value()) - CHECK(marker.value() == 5); // 6pqn + // scroll top -> scroll bottom + marker = screen.findMarkerDownwards(ScrollOffset(3)); + REQUIRE(marker.has_value()); + REQUIRE(*marker.value() == 1); - // 5: -> NONE (bottom of screen already) - marker = screen.findMarkerForward(5); - CHECK_FALSE(marker.has_value()); + // scroll bottom -> NONE + marker = screen.findMarkerDownwards(ScrollOffset(1)); + REQUIRE(marker.has_value()); + REQUIRE(*marker.value() == 0); } } -TEST_CASE("findMarkerBackward", "[screen]") +TEST_CASE("findMarkerUpwards", "[screen]") { - auto screen = MockScreen{PageSize{LineCount(3), ColumnCount(4)}}; - REQUIRE_FALSE(screen.findMarkerBackward(0).has_value()); // peak into history - REQUIRE_FALSE(screen.findMarkerBackward(1).has_value()); - REQUIRE_FALSE(screen.findMarkerBackward(2).has_value()); - REQUIRE_FALSE(screen.findMarkerBackward(3).has_value()); - REQUIRE_FALSE(screen.findMarkerBackward(4).has_value()); // overflow + auto screen = MockScreen{PageSize{LineCount(3), ColumnCount(4)}, LineCount(10)}; + REQUIRE_FALSE(screen.findMarkerUpwards(ScrollOffset(-1)).has_value()); // peak into history + REQUIRE_FALSE(screen.findMarkerUpwards(ScrollOffset(0)).has_value()); + REQUIRE_FALSE(screen.findMarkerUpwards(ScrollOffset(1)).has_value()); + REQUIRE_FALSE(screen.findMarkerUpwards(ScrollOffset(2)).has_value()); + REQUIRE_FALSE(screen.findMarkerUpwards(ScrollOffset(3)).has_value()); // overflow SECTION("no marks") { screen.write("1abc"sv); @@ -2197,83 +2272,65 @@ TEST_CASE("findMarkerBackward", "[screen]") REQUIRE(screen.historyLineCount() == LineCount{3}); - auto mark = screen.findMarkerBackward(static_cast(*screen.size().lines)); + auto mark = screen.findMarkerUpwards(ScrollOffset(0)); REQUIRE_FALSE(mark.has_value()); - // test one line beyond history line count - mark = screen.findMarkerBackward(-1); + // bottom line in history + mark = screen.findMarkerUpwards(ScrollOffset(1)); REQUIRE_FALSE(mark.has_value()); - // test last history line - mark = screen.findMarkerBackward(0); + // one above bottom line in history + mark = screen.findMarkerUpwards(ScrollOffset(2)); REQUIRE_FALSE(mark.has_value()); - // test second-last history line - mark = screen.findMarkerBackward(1); + // top history line + mark = screen.findMarkerUpwards(ScrollOffset(3)); REQUIRE_FALSE(mark.has_value()); - // test first history line - mark = screen.findMarkerBackward(2); + // one above history top + mark = screen.findMarkerUpwards(ScrollOffset(4)); REQUIRE_FALSE(mark.has_value()); } SECTION("with marks") { // saved lines - screen.setMark(); // 0 + screen.setMark(); // 0 (-3) screen.write("1abc\r\n"sv); - screen.write("2def\r\n"sv); // 1 + screen.write("2def\r\n"sv); // 1 (-2) screen.setMark(); - screen.write("3ghi\r\n"sv); // 2 + screen.write("3ghi\r\n"sv); // 2 (-1) // visibile screen - screen.setMark(); // 3 + screen.setMark(); // 3 (0) screen.write("4jkl\r\n"sv); - screen.write("5mno\r\n"sv); // 4 - screen.setMark(); // 5 + screen.write("5mno\r\n"sv); // 4 (1) + screen.setMark(); // 5 (2) screen.write("6pqr"sv); - REQUIRE(screen.renderTextLine(-2) == "1abc"); - REQUIRE(screen.renderTextLine(-1) == "2def"); - REQUIRE(screen.renderTextLine(0) == "3ghi"); + // {{{ pre-checks + REQUIRE(screen.grid().renderTextLine(LineOffset(-3)) == "1abc"); // marked + REQUIRE(screen.grid().renderTextLine(LineOffset(-2)) == "2def"); + REQUIRE(screen.grid().renderTextLine(LineOffset(-1)) == "3ghi"); // marked - REQUIRE(screen.renderTextLine(1) == "4jkl"); - REQUIRE(screen.renderTextLine(2) == "5mno"); - REQUIRE(screen.renderTextLine(3) == "6pqr"); + REQUIRE(screen.grid().renderTextLine(LineOffset(0)) == "4jkl"); // marked + REQUIRE(screen.grid().renderTextLine(LineOffset(1)) == "5mno"); + REQUIRE(screen.grid().renderTextLine(LineOffset(2)) == "6pqr"); // marked + // }}} // ====================================================== - - // 5: -> 3 - auto marker = screen.findMarkerBackward(5); + // main page top (0) -> scroll offset 1 + auto marker = screen.findMarkerUpwards(ScrollOffset(0)); REQUIRE(marker.has_value()); - CHECK(marker.value() == 3); // 4jkl + REQUIRE(marker.value().value == 1); // 3ghi - // 4: -> 3 - marker = screen.findMarkerBackward(4); + // scroll offset 1 -> scroll offset 3 + marker = screen.findMarkerUpwards(ScrollOffset(1)); REQUIRE(marker.has_value()); - CHECK(marker.value() == 3); // 4jkl - - // 3: -> 2 - marker = screen.findMarkerBackward(3); - REQUIRE(marker.has_value()); - CHECK(marker.value() == 2); // 3gh - - // 2: -> 0 - marker = screen.findMarkerBackward(2); - REQUIRE(marker.has_value()); - CHECK(marker.value() == 0); // 1abc - - // 1: -> 0 - marker = screen.findMarkerBackward(1); - REQUIRE(marker.has_value()); - CHECK(marker.value() == 0); // 1abc - - // 0: -> NONE - marker = screen.findMarkerBackward(0); - CHECK_FALSE(marker.has_value()); + REQUIRE(marker.value().value == 3); // 1abc - // -1: -> NONE (one off edge case) - marker = screen.findMarkerBackward(-1); - CHECK_FALSE(marker.has_value()); + // one-off + marker = screen.findMarkerUpwards(ScrollOffset(3)); + REQUIRE(!marker.has_value()); } } @@ -2283,32 +2340,32 @@ TEST_CASE("DECTABSR", "[screen]") SECTION("default tabstops") { screen.requestTabStops(); - CHECK(screen.replyData == "\033P2$u1/9/17/25/33\033\\"); + CHECK(e(screen.replyData) == e("\033P2$u1/9/17/25/33\033\\")); } SECTION("cleared tabs") { screen.horizontalTabClear(HorizontalTabClear::AllTabs); screen.requestTabStops(); - CHECK(screen.replyData == "\033P2$u1/9/17/25/33\033\\"); + CHECK(e(screen.replyData) == e("\033P2$u1/9/17/25/33\033\\")); } SECTION("custom tabstops") { screen.horizontalTabClear(HorizontalTabClear::AllTabs); - screen.moveCursorToColumn(ColumnPosition(2)); + screen.moveCursorToColumn(ColumnOffset(1)); screen.horizontalTabSet(); - screen.moveCursorToColumn(ColumnPosition(4)); + screen.moveCursorToColumn(ColumnOffset(3)); screen.horizontalTabSet(); - screen.moveCursorToColumn(ColumnPosition(8)); + screen.moveCursorToColumn(ColumnOffset(7)); screen.horizontalTabSet(); - screen.moveCursorToColumn(ColumnPosition(16)); + screen.moveCursorToColumn(ColumnOffset(15)); screen.horizontalTabSet(); screen.requestTabStops(); - CHECK(screen.replyData == "\033P2$u2/4/8/16\033\\"); + CHECK(e(screen.replyData) == e("\033P2$u2/4/8/16\033\\")); } } @@ -2350,105 +2407,105 @@ TEST_CASE("XTGETTCAP") // TODO: CHECK(...) } +#if 0 // {{{ TODO(pr) // TODO: resize test (should be in Grid_test.cpp?) TEST_CASE("resize", "[screen]") { auto screen = MockScreen{PageSize{LineCount(2), ColumnCount(2)}}; screen.write("ABCD"); - REQUIRE("AB\nCD\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{2, 2}); - REQUIRE(screen.wrapPending() == 1); + REQUIRE("AB\nCD\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(1)}); screen.setMaxHistoryLineCount(LineCount(10)); SECTION("no-op") { screen.resize({LineCount(2), ColumnCount(2)}); - CHECK("AB\nCD\n" == screen.renderText()); + CHECK("AB\nCD\n" == screen.renderMainPageText()); } SECTION("grow lines") { screen.resize({LineCount(3), ColumnCount(2)}); - REQUIRE("AB\nCD\n \n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{2, 2}); + REQUIRE("AB\nCD\n \n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(1)}); screen.write("\r\n"); screen.write("E"); - REQUIRE("AB\nCD\nE \n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{3, 2}); + REQUIRE("AB\nCD\nE \n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(2), ColumnOffset(1)}); screen.write("F"); - REQUIRE("AB\nCD\nEF\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{3, 2}); + REQUIRE("AB\nCD\nEF\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(2), ColumnOffset(1)}); } SECTION("shrink lines") { screen.resize({LineCount(1), ColumnCount(2)}); - CHECK("CD\n" == screen.renderText()); + CHECK("CD\n" == screen.renderMainPageText()); CHECK("AB" == screen.renderHistoryTextLine(1)); - CHECK(screen.cursorPosition() == Coordinate{1, 2}); + CHECK(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(1)}); } SECTION("grow columns") { screen.resize({LineCount(2), ColumnCount(3)}); - CHECK("AB \nCD \n" == screen.renderText()); - CHECK(screen.cursorPosition() == Coordinate{2, 3}); - CHECK(screen.wrapPending() == 0); + CHECK("AB \nCD \n" == screen.renderMainPageText()); + CHECK(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(2)}); } SECTION("shrink columns") { screen.resize({LineCount(2), ColumnCount(1)}); - CHECK("A\nC\n" == screen.renderText()); - CHECK(screen.cursorPosition() == Coordinate{2, 1}); + CHECK("A\nC\n" == screen.renderMainPageText()); + CHECK(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(0)}); } SECTION("regrow columns") { // 1.) grow screen.resize({LineCount(2), ColumnCount(3)}); - CHECK(screen.cursorPosition() == Coordinate{2, 3}); + CHECK(screen.cursorPosition() == Coordinate{LineOffset(1), ColumnOffset(2)}); // 2.) fill screen.writeText('Y'); - REQUIRE("AB \nCDY\n" == screen.renderText()); + REQUIRE("AB \nCDY\n" == screen.renderMainPageText()); screen.moveCursorTo({1, 3}); screen.writeText('X'); - REQUIRE("ABX\nCDY\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{1, 3}); + REQUIRE("ABX\nCDY\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(2)}); // 3.) shrink screen.resize({LineCount(2), ColumnCount(2)}); - REQUIRE("AB\nCD\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{1, 2}); + REQUIRE("AB\nCD\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(1)}); // 4.) regrow (and see if pre-filled data were retained) screen.resize({LineCount(2), ColumnCount(3)}); - REQUIRE("ABX\nCDY\n" == screen.renderText()); - REQUIRE(screen.cursorPosition() == Coordinate{1, 3}); + REQUIRE("ABX\nCDY\n" == screen.renderMainPageText()); + REQUIRE(screen.cursorPosition() == Coordinate{LineOffset(0), ColumnOffset(2)}); } SECTION("grow rows, grow columns") { screen.resize({LineCount(3), ColumnCount(3)}); - REQUIRE("AB \nCD \n \n" == screen.renderText()); + REQUIRE("AB \nCD \n \n" == screen.renderMainPageText()); screen.write("1\r\n234"); - REQUIRE("AB \nCD1\n234\n" == screen.renderText()); + REQUIRE("AB \nCD1\n234\n" == screen.renderMainPageText()); } SECTION("grow rows, shrink columns") { screen.resize({LineCount(3), ColumnCount(1)}); - REQUIRE("A\nC\n \n" == screen.renderText()); + REQUIRE("A\nC\n \n" == screen.renderMainPageText()); } SECTION("shrink rows, grow columns") { screen.resize({LineCount(1), ColumnCount(3)}); - REQUIRE("CD \n" == screen.renderText()); + REQUIRE("CD \n" == screen.renderMainPageText()); } SECTION("shrink rows, shrink columns") { screen.resize({LineCount(1), ColumnCount(1)}); - REQUIRE("C\n" == screen.renderText()); + REQUIRE("C\n" == screen.renderMainPageText()); } // TODO: what do we want to do when re resize to {0, y}, {x, 0}, {0, 0}? } +#endif // }}} // {{{ DECCRA // TODO: also verify attributes have been copied @@ -2475,7 +2532,7 @@ MockScreen screenForDECRA() "GHIJKL\n" "ghijkl\n"; - CHECK(screen.renderText() == initialText); + CHECK(screen.renderMainPageText() == initialText); return screen; } @@ -2488,7 +2545,7 @@ TEST_CASE("DECCRA.DownLeft.intersecting", "[screen]") "123456\n" "GHIJKL\n" "ghijkl\n"; - CHECK(screen.renderText() == initialText); + CHECK(screen.renderMainPageText() == initialText); auto constexpr page = 0; @@ -2515,7 +2572,7 @@ TEST_CASE("DECCRA.DownLeft.intersecting", "[screen]") tTop, tLeft, page); screen.write(deccraSeq); - auto const resultText = screen.renderText(); + auto const resultText = screen.renderMainPageText(); CHECK(resultText == expectedText); } @@ -2529,7 +2586,7 @@ TEST_CASE("DECCRA.Right.intersecting", "[screen]") "123456\n" "GHIJKL\n" "ghijkl\n"; - CHECK(screen.renderText() == initialText); + REQUIRE(screen.renderMainPageText() == initialText); auto const expectedText = "ABCDEF\n" "abbcdf\n" "122346\n" @@ -2537,17 +2594,17 @@ TEST_CASE("DECCRA.Right.intersecting", "[screen]") "ghijkl\n"; auto constexpr page = 0; - auto constexpr sTopLeft = Coordinate{2, 2}; - auto constexpr sBottomRight = Coordinate{4, 4}; - auto constexpr tTopLeft = Coordinate{2, 3}; + auto constexpr sTopLeft = Coordinate{LineOffset(1), ColumnOffset(1)}; + auto constexpr sBottomRight = Coordinate{LineOffset(3), ColumnOffset(3)}; + auto constexpr tTopLeft = Coordinate{LineOffset(1), ColumnOffset(2)}; auto const deccraSeq = fmt::format("\033[{};{};{};{};{};{};{};{}$v", - sTopLeft.row, sTopLeft.column, - sBottomRight.row, sBottomRight.column, page, - tTopLeft.row, tTopLeft.column, page); + sTopLeft.row + 1, sTopLeft.column + 1, + sBottomRight.row + 1, sBottomRight.column + 1, page, + tTopLeft.row + 1, tTopLeft.column + 1, page); screen.write(deccraSeq); - auto const resultText = screen.renderText(); + auto const resultText = screen.renderMainPageText(); CHECK(resultText == expectedText); } @@ -2560,7 +2617,7 @@ TEST_CASE("DECCRA.Left.intersecting", "[screen]") "123456\n" "GHIJKL\n" "ghijkl\n"; - CHECK(screen.renderText() == initialText); + CHECK(screen.renderMainPageText() == initialText); auto const expectedText = "ABCDEF\n" "abdeff\n" @@ -2569,17 +2626,17 @@ TEST_CASE("DECCRA.Left.intersecting", "[screen]") "ghijkl\n"; auto constexpr page = 0; - auto constexpr sTopLeft = Coordinate{2, 4}; - auto constexpr sBottomRight = Coordinate{3, 6}; - auto constexpr tTopLeft = Coordinate{2, 3}; + auto constexpr sTopLeft = Coordinate{LineOffset(1), ColumnOffset(3)}; + auto constexpr sBottomRight = Coordinate{LineOffset(2), ColumnOffset(5)}; + auto constexpr tTopLeft = Coordinate{LineOffset(1), ColumnOffset(2)}; auto const deccraSeq = fmt::format("\033[{};{};{};{};{};{};{};{}$v", - sTopLeft.row, sTopLeft.column, - sBottomRight.row, sBottomRight.column, page, - tTopLeft.row, tTopLeft.column, page); + sTopLeft.row + 1, sTopLeft.column + 1, + sBottomRight.row + 1, sBottomRight.column + 1, page, + tTopLeft.row + 1, tTopLeft.column + 1, page); screen.write(deccraSeq); - auto const resultText = screen.renderText(); + auto const resultText = screen.renderMainPageText(); CHECK(resultText == expectedText); } // }}} diff --git a/src/terminal/Selector.cpp b/src/terminal/Selector.cpp index d7cc3b62ce..06d3256fcf 100644 --- a/src/terminal/Selector.cpp +++ b/src/terminal/Selector.cpp @@ -38,18 +38,19 @@ Selector::Selector(Mode _mode, from_{_from}, to_{_from} { +#if 0 // TODO(pr) if (_mode == Mode::FullLine) { - extend({from_.row, 1}); + extend(from_.row, ColumnOffset(0)); swapDirection(); - extend({from_.row, columnCount_.as()}); + extend(from_.row, columnCount_.as()); // backward - while (from_.row > 0 && wrapped_(from_.row)) + while (from_.row > 0 && wrapped_(LineOffset::cast_from(from_.row))) from_.row--; // forward - while (to_.row < *_totalRowCount && wrapped_(to_.row + 1)) + while (to_.row < *_totalRowCount && wrapped_(LineOffset::cast_from(to_.row + 1))) to_.row++; } else if (isWordWiseSelection()) @@ -60,6 +61,7 @@ Selector::Selector(Mode _mode, swapDirection(); extendSelectionForward(); } +#endif } Selector::Selector(Mode _mode, @@ -68,18 +70,18 @@ Selector::Selector(Mode _mode, Coordinate _from) : Selector{ _mode, - [screen = std::ref(_screen)](Coordinate _pos) -> Cell const* { - assert(_pos.row >= 0 && "must be absolute coordinate"); + [screen = std::ref(_screen)](LineOffset _line, ColumnOffset _column) -> Cell const* { + Expects(_line.value >= 0 && "must be absolute coordinate"); auto const& buffer = screen.get(); // convert line number from absolute line to relative line number. - auto const row = _pos.row - unbox(buffer.historyLineCount()) + 1; - if (row <= *buffer.size().lines) - return &buffer.at({row, _pos.column}); + auto const row = _line + buffer.historyLineCount().as() + LineOffset(1); + if (row < buffer.size().lines.as()) + return &buffer.at(row, _column); else return nullptr; }, - [screen = std::ref(_screen)](int _line) -> bool { - return screen.get().lineWrapped(_line); + [screen = std::ref(_screen)](LineOffset _line) -> bool { + return screen.get().isLineWrapped(_line); }, _wordDelimiters, _screen.size().lines + _screen.historyLineCount(), @@ -91,6 +93,7 @@ Selector::Selector(Mode _mode, Coordinate Selector::stretchedColumn(Coordinate _coord) const noexcept { +#if 0 // TODO(pr) Coordinate stretched = _coord; if (Cell const* cell = at(_coord); cell && cell->width() > 1) { @@ -117,16 +120,18 @@ Coordinate Selector::stretchedColumn(Coordinate _coord) const noexcept } return stretched; +#else + return {}; +#endif } -bool Selector::extend(Coordinate const& _coord) +bool Selector::extend(LineOffset _line, ColumnOffset _column) { +#if 0 // TODO(pr) assert(state_ != State::Complete && "In order extend a selection, the selector must be active (started)."); - auto const coord = Coordinate{ - _coord.row, - clamp(_coord.column, 1, unbox(columnCount_)) - }; + auto column = clamp(_column, ColumnOffset(0), columnCount_.as()); + auto const coord = Coordinate{_line, column}; state_ = State::InProgress; @@ -169,11 +174,13 @@ bool Selector::extend(Coordinate const& _coord) } // TODO: indicates whether or not a scroll action must take place. +#endif return false; } void Selector::extendSelectionBackward() { +#if 0 // TODO(br) auto const isWordDelimiterAt = [this](Coordinate const& _coord) -> bool { Cell const* cell = at(_coord); return !cell || cell->empty() || wordDelimiters_.find(cell->codepoint(0)) != wordDelimiters_.npos; @@ -205,11 +212,13 @@ void Selector::extendSelectionBackward() } else to_ = last; +#endif } void Selector::extendSelectionForward() { - auto const isWordDelimiterAt = [this](Coordinate const& _coord) -> bool { +#if 0 // TODO(pr) + auto const isWordDelimiterAt = [this](Coordinate _coord) -> bool { Cell const* cell = at(_coord); return !cell || cell->empty() || wordDelimiters_.find(cell->codepoint(0)) != wordDelimiters_.npos; }; @@ -242,6 +251,7 @@ void Selector::extendSelectionForward() } to_ = stretchedColumn(last); +#endif } void Selector::stop() @@ -261,8 +271,10 @@ tuple, Coordinate const, Coordinate const> prepare(Selec return pair{_selector.from(), _selector.to()}; }(); +#if 0//TODO(pr) auto const numLines = to.row - from.row + 1; result.resize(numLines); +#endif return {move(result), from, to}; } @@ -284,6 +296,7 @@ vector Selector::selection() const vector Selector::linear() const { +#if 0 // TODO(pr) auto [result, from, to] = prepare(*this); switch (result.size()) @@ -311,10 +324,14 @@ vector Selector::linear() const } return result; +#else + return {}; +#endif } vector Selector::lines() const { +#if 0 // TODO(pr) auto [result, from, to] = prepare(*this); for (int row = 0; row < static_cast(result.size()); ++row) @@ -327,10 +344,14 @@ vector Selector::lines() const } return result; +#else + return {}; +#endif } vector Selector::rectangular() const { +#if 0 // TODO(pr) auto [result, from, to] = prepare(*this); for (int row = 0; row < static_cast(result.size()); ++row) @@ -343,6 +364,9 @@ vector Selector::rectangular() const } return result; +#else + return {}; +#endif } } // namespace terminal diff --git a/src/terminal/Selector.h b/src/terminal/Selector.h index 5a8afb0efc..d662c12e27 100644 --- a/src/terminal/Selector.h +++ b/src/terminal/Selector.h @@ -74,8 +74,8 @@ class Selector { enum class Mode { Linear, LinearWordWise, FullLine, Rectangular }; - using GetCellAt = std::function; - using GetWrappedFlag = std::function; + using GetCellAt = std::function; + using GetWrappedFlag = std::function; Selector(Mode _mode, GetCellAt _at, @@ -99,7 +99,12 @@ class Selector { /// /// @retval true TerminalView requires scrolling offset adjustments. /// @retval false TerminalView's scrolling offset does not need adjustments. - bool extend(Coordinate const& _to); + bool extend(Coordinate _to) + { + return extend(_to.row, _to.column); + } + + bool extend(LineOffset _line, ColumnOffset _column); /// Marks the selection as completed. void stop(); @@ -157,10 +162,19 @@ class Selector { template void render(Renderer&& _render) const { - for (auto const& range : selection()) - for (auto const col : crispy::times(range.fromColumn, range.length())) - if (Cell const* cell = at({range.line, col}); cell != nullptr) +#if 0 // TODO(pr) + for (auto const& range: selection()) + { + for (auto const col: crispy::times(range.fromColumn, range.length())) + { + auto line = LineOffset::cast_from(range.line); + auto column = ColumnOffset::cast_from(col); + Cell const* cell = at(line, column); + if (cell != nullptr) _render(Coordinate{range.line, col}, *cell); + } + } +#endif } private: @@ -175,12 +189,21 @@ class Selector { } } - Cell const* at(Coordinate const& _pos) const { return getCellAt_(_pos); } + Cell const* at(Coordinate _coord) const noexcept + { + return getCellAt_(_coord.row, _coord.column); + } + + Cell const* at(LineOffset _line, ColumnOffset _column) const noexcept + { + return getCellAt_(_line, _column); + } void extendSelectionBackward(); void extendSelectionForward(); - private: + // private fields + // State state_{State::Waiting}; Mode mode_; GetCellAt getCellAt_; diff --git a/src/terminal/Selector_test.cpp b/src/terminal/Selector_test.cpp index 277977fece..01c0983487 100644 --- a/src/terminal/Selector_test.cpp +++ b/src/terminal/Selector_test.cpp @@ -28,6 +28,7 @@ using namespace terminal; // - multiple lines from history into main buffer // all of the above with and without scrollback != 0. +#if 0 // TODO(Pr) namespace { struct TextSelection { @@ -207,3 +208,4 @@ TEST_CASE("Selector.Rectangular", "[selector]") { // TODO } +#endif diff --git a/src/terminal/Sequencer.cpp b/src/terminal/Sequencer.cpp index cca2add713..ea6504a095 100644 --- a/src/terminal/Sequencer.cpp +++ b/src/terminal/Sequencer.cpp @@ -135,7 +135,7 @@ namespace impl // {{{ some command generator helpers } } - optional toDECMode(int _value) + optional toDECMode(unsigned _value) { switch (_value) { @@ -922,9 +922,9 @@ void Sequencer::error(std::string_view const& _errorString) void Sequencer::print(char32_t _char) { - precedingGraphicCharacter_ = _char; instructionCounter_++; screen_.writeText(_char); + precedingGraphicCharacter_ = _char; } void Sequencer::print(string_view _chars) @@ -932,9 +932,9 @@ void Sequencer::print(string_view _chars) if (_chars.empty()) return; - precedingGraphicCharacter_ = _chars.back(); instructionCounter_ += _chars.size(); screen_.writeText(_chars); + precedingGraphicCharacter_ = _chars.back(); } void Sequencer::execute(char _controlCode) @@ -1008,7 +1008,7 @@ void Sequencer::startOSC() void Sequencer::putOSC(char32_t _char) { - uint8_t u8[4]; + char u8[4]; size_t const count = distance(u8, unicode::encoder{}(_char, u8)); if (sequence_.intermediateCharacters().size() + count < Sequence::MaxOscLength) for (size_t i = 0; i < count; ++i) @@ -1340,7 +1340,7 @@ ApplyResult Sequencer::apply(FunctionDefinition const& _function, Sequence const // CSI case ANSISYSSC: screen_.restoreCursor(); break; case CBT: screen_.cursorBackwardTab(TabStopCount(_seq.param_or(0, Sequence::Parameter{1}))); break; - case CHA: screen_.moveCursorToColumn(ColumnPosition(_seq.param_or(0, Sequence::Parameter{1}))); break; + case CHA: screen_.moveCursorToColumn(_seq.param_or(0, ColumnOffset{1})); break; case CHT: screen_.cursorForwardTab(TabStopCount(_seq.param_or(0, Sequence::Parameter{1}))); break; case CNL: screen_.moveCursorToNextLine(LineCount(_seq.param_or(0, Sequence::Parameter{1}))); break; case CPL: screen_.moveCursorToPrevLine(LineCount(_seq.param_or(0, Sequence::Parameter{1}))); break; @@ -1348,26 +1348,31 @@ ApplyResult Sequencer::apply(FunctionDefinition const& _function, Sequence const case CUB: screen_.moveCursorBackward(_seq.param_or(0, ColumnCount{1})); break; case CUD: screen_.moveCursorDown(_seq.param_or(0, LineCount{1})); break; case CUF: screen_.moveCursorForward(_seq.param_or(0, ColumnCount{1})); break; - case CUP: screen_.moveCursorTo(Coordinate{ _seq.param_or(0, 1), _seq.param_or(1, 1)}); break; + case CUP: + screen_.moveCursorTo( + LineOffset::cast_from(_seq.param_or(0, 1) - 1), + ColumnOffset::cast_from(_seq.param_or(1, 1) - 1) + ); + break; case CUU: screen_.moveCursorUp(_seq.param_or(0, LineCount{1})); break; case DA1: screen_.sendDeviceAttributes(); break; case DA2: screen_.sendTerminalId(); break; case DA3: return ApplyResult::Unsupported; - case DCH: screen_.deleteCharacters(_seq.param_or(0, ColumnCount{1})); break; + case DCH: screen_.deleteCharacters(_seq.param_or(0, ColumnCount{0})); break; case DECCRA: { // The coordinates of the rectangular area are affected by the setting of origin mode (DECOM). // DECCRA is not affected by the page margins. auto const origin = screen_.origin(); - auto const top = _seq.param_or(0, origin.row); - auto const left = _seq.param_or(1, origin.column); - auto const bottom = _seq.param_or(2, unbox(screen_.size().lines)); - auto const right = _seq.param_or(3, unbox(screen_.size().columns)); - auto const page = _seq.param_or(4, 0); + auto const top = _seq.param_or(0, 1 + *origin.row) - 1; + auto const left = _seq.param_or(1, 1 + *origin.column) - 1; + auto const bottom = _seq.param_or(2, *screen_.size().lines) - 1; + auto const right = _seq.param_or(3, *screen_.size().columns) - 1; + auto const page = _seq.param_or(4, 0); - auto const targetTop = _seq.param_or(5, origin.row); - auto const targetLeft = _seq.param_or(6, origin.column); - auto const targetPage = _seq.param_or(7, 0); + auto const targetTop = _seq.param_or(5, 1 + *origin.row) - 1; + auto const targetLeft = _seq.param_or(6, 1 + *origin.column) - 1; + auto const targetPage = _seq.param_or(7, 0); screen_.copyArea(top, left, bottom, right, page, targetTop, targetLeft, targetPage); @@ -1377,13 +1382,13 @@ ApplyResult Sequencer::apply(FunctionDefinition const& _function, Sequence const { // The coordinates of the rectangular area are affected by the setting of origin mode (DECOM). auto const origin = screen_.origin(); - auto const top = _seq.param_or(0, origin.row); - auto const left = _seq.param_or(1, origin.column); + auto const top = _seq.param_or(0, *origin.row); + auto const left = _seq.param_or(1, *origin.column); // If the value of Pt, Pl, Pb, or Pr exceeds the width or height of the active page, then the value is treated as the width or height of that page. auto const size = screen_.size(); - auto const bottom = min(_seq.param_or(2, unbox(size.lines)), unbox(size.lines)); - auto const right = min(_seq.param_or(3, unbox(size.columns)), unbox(size.columns)); + auto const bottom = min(_seq.param_or(2, unbox(size.lines)), unbox(size.lines)); + auto const right = min(_seq.param_or(3, unbox(size.columns)), unbox(size.columns)); screen_.eraseArea(top, left, bottom, right); } @@ -1393,19 +1398,19 @@ ApplyResult Sequencer::apply(FunctionDefinition const& _function, Sequence const auto const ch = _seq.param_or(0, Sequence::Parameter{ 0 }); // The coordinates of the rectangular area are affected by the setting of origin mode (DECOM). auto const origin = screen_.origin(); - auto const top = _seq.param_or(0, origin.row); - auto const left = _seq.param_or(1, origin.column); + auto const top = _seq.param_or(0, origin.row + 1); + auto const left = _seq.param_or(1, origin.column + 1); // If the value of Pt, Pl, Pb, or Pr exceeds the width or height of the active page, then the value is treated as the width or height of that page. auto const size = screen_.size(); - auto const bottom = min(_seq.param_or(2, unbox(size.lines)), unbox(size.lines)); - auto const right = min(_seq.param_or(3, unbox(size.columns)), unbox(size.columns)); + auto const bottom = min(_seq.param_or(2, *size.lines), *size.lines); + auto const right = min(_seq.param_or(3, *size.columns), *size.columns); - screen_.fillArea(ch, top, left, bottom, right); + screen_.fillArea(ch, *top - 1, *left - 1, bottom - 1, right - 1); } break; - case DECDC: screen_.deleteColumns(_seq.param_or(0, ColumnCount(1))); break; - case DECIC: screen_.insertColumns(_seq.param_or(0, ColumnCount(1))); break; + case DECDC: screen_.deleteColumns(_seq.param_or(0, ColumnCount(1))); break; + case DECIC: screen_.insertColumns(_seq.param_or(0, ColumnCount(1))); break; case DECRM: { ApplyResult r = ApplyResult::Ok; @@ -1440,7 +1445,15 @@ ApplyResult Sequencer::apply(FunctionDefinition const& _function, Sequence const case DECSNLS: screen_.resize(PageSize{screen_.size().lines, _seq.param(0)}); return ApplyResult::Ok; - case DECSLRM: screen_.setLeftRightMargin(_seq.param_opt(0), _seq.param_opt(1)); break; + case DECSLRM: + { + auto l = _seq.param_opt(0); + if (l.has_value()) + l = *l - ColumnOffset(1); + auto r = _seq.param_opt(1); + screen_.setLeftRightMargin(l, r); + } + break; case DECSM: { ApplyResult r = ApplyResult::Ok; @@ -1451,30 +1464,31 @@ ApplyResult Sequencer::apply(FunctionDefinition const& _function, Sequence const }); return r; } - case DECSTBM: screen_.setTopBottomMargin(_seq.param_opt(0), _seq.param_opt(1)); break; + case DECSTBM: screen_.setTopBottomMargin(_seq.param_opt(0), _seq.param_opt(1)); break; case DECSTR: screen_.resetSoft(); break; case DECXCPR: screen_.reportExtendedCursorPosition(); break; - case DL: screen_.deleteLines(_seq.param_or(0, LineCount(1))); break; - case ECH: screen_.eraseCharacters(_seq.param_or(0, ColumnCount(1))); break; + case DL: screen_.deleteLines(_seq.param_or(0, LineCount(1))); break; + case ECH: screen_.eraseCharacters(_seq.param_or(0, ColumnCount(1))); break; case ED: return impl::ED(_seq, screen_); case EL: return impl::EL(_seq, screen_); - case HPA: screen_.moveCursorToColumn(_seq.param(0)); break; + case HPA: screen_.moveCursorToColumn(_seq.param(0) - 1); break; case HPR: screen_.moveCursorForward(_seq.param(0)); break; case HVP: screen_.moveCursorTo(Coordinate{ - _seq.param_or(0, 1), - _seq.param_or(1, 1) + _seq.param_or(0, LineOffset(0)), + _seq.param_or(1, ColumnOffset(0)) }); break; // YES, it's like a CUP! - case ICH: screen_.insertCharacters(_seq.param_or(0, ColumnCount{1})); break; - case IL: screen_.insertLines(_seq.param_or(0, LineCount{1})); break; + case ICH: screen_.insertCharacters(_seq.param_or(0, ColumnCount{1})); break; + case IL: screen_.insertLines(_seq.param_or(0, LineCount{1})); break; case REP: if (precedingGraphicCharacter_) { - auto const requestedCount = _seq.param(0); - auto const availableColumns = screen_.margin().horizontal.to - screen_.cursor().position.column + 1; + auto const requestedCount = _seq.param(0); + auto const availableColumns = + (screen_.margin().horizontal.to - screen_.cursor().position.column).as(); auto const effectiveCount = min(requestedCount, availableColumns); - for (int i = 0; i < effectiveCount; i++) + for (size_t i = 0; i < effectiveCount; i++) screen_.writeText(precedingGraphicCharacter_); } break; @@ -1505,7 +1519,7 @@ ApplyResult Sequencer::apply(FunctionDefinition const& _function, Sequence const } case SU: screen_.scrollUp(_seq.param_or(0, LineCount(1))); break; case TBC: return impl::TBC(_seq, screen_); - case VPA: screen_.moveCursorToLine(_seq.param_or(0, LinePosition{1})); break; + case VPA: screen_.moveCursorToLine(_seq.param_or(0, LineOffset{0})); break; case WINMANIP: return impl::WINDOWMANIP(_seq, screen_); case DECMODERESTORE: return impl::restoreDECModes(_seq, screen_); case DECMODESAVE: return impl::saveDECModes(_seq, screen_); diff --git a/src/terminal/Sequencer.h b/src/terminal/Sequencer.h index 1669c67f13..166b984f35 100644 --- a/src/terminal/Sequencer.h +++ b/src/terminal/Sequencer.h @@ -473,6 +473,7 @@ class Sequencer : public ParserEvents { int64_t instructionCounter() const noexcept { return instructionCounter_; } void resetInstructionCounter() noexcept { instructionCounter_ = 0; } + char32_t precedingGraphicCharacter() const noexcept { return precedingGraphicCharacter_; } // ParserEvents // diff --git a/src/terminal/SixelParser.cpp b/src/terminal/SixelParser.cpp index d032226e70..4c187dd3b7 100644 --- a/src/terminal/SixelParser.cpp +++ b/src/terminal/SixelParser.cpp @@ -318,7 +318,7 @@ SixelImageBuilder::SixelImageBuilder(ImageSize _maxSize, colors_{ std::move(_colorPalette) }, size_{ _maxSize }, buffer_(*size_.width * *size_.height * 4), - sixelCursor_{ 0, 0 }, + sixelCursor_{}, currentColor_{0}, aspectRatio_{ _aspectVertical, _aspectHorizontal } { @@ -327,7 +327,7 @@ SixelImageBuilder::SixelImageBuilder(ImageSize _maxSize, void SixelImageBuilder::clear(RGBAColor _fillColor) { - sixelCursor_ = {0, 0}; + sixelCursor_ = {}; auto p = &buffer_[0]; for (int i = 0; i < *size_.width * *size_.height; ++i) @@ -341,8 +341,8 @@ void SixelImageBuilder::clear(RGBAColor _fillColor) RGBAColor SixelImageBuilder::at(Coordinate _coord) const noexcept { - auto const row = _coord.row % *size_.height; - auto const col = _coord.column % *size_.width; + auto const row = *_coord.row % *size_.height; + auto const col = *_coord.column % *size_.width; auto const base = row * *size_.width * 4 + col * 4; auto const color = &buffer_[base]; return RGBAColor{color[0], color[1], color[2], color[3]}; @@ -350,9 +350,9 @@ RGBAColor SixelImageBuilder::at(Coordinate _coord) const noexcept void SixelImageBuilder::write(Coordinate const& _coord, RGBColor const& _value) noexcept { - if (_coord.row >= 0 && _coord.row < *size_.height && _coord.column >= 0 && _coord.column < *size_.width) + if (*_coord.row >= 0 && *_coord.row < *size_.height && *_coord.column >= 0 && *_coord.column < *size_.width) { - auto const base = _coord.row * *size_.width * 4 + _coord.column * 4; + auto const base = *_coord.row * *size_.width * 4 + *_coord.column * 4; buffer_[base + 0] = _value.red; buffer_[base + 1] = _value.green; buffer_[base + 2] = _value.blue; @@ -372,15 +372,15 @@ void SixelImageBuilder::useColor(int _index) void SixelImageBuilder::rewind() { - sixelCursor_.column = 0; + sixelCursor_.column = {}; } void SixelImageBuilder::newline() { - sixelCursor_.column = 0; + sixelCursor_.column = {}; - if (sixelCursor_.row + 6 < *size_.height) - sixelCursor_.row += 6; + if (*sixelCursor_.row + 6 < *size_.height) + sixelCursor_.row.value += 6; } void SixelImageBuilder::setRaster(int _pan, int _pad, ImageSize _imageSize) @@ -397,7 +397,7 @@ void SixelImageBuilder::render(int8_t _sixel) { // TODO: respect aspect ratio! auto const x = sixelCursor_.column; - if (x < *size_.width) + if (*x < *size_.width) { for (int i = 0; i < 6; ++i) { diff --git a/src/terminal/SixelParser_test.cpp b/src/terminal/SixelParser_test.cpp index 5dae2e5101..e294dbf72a 100644 --- a/src/terminal/SixelParser_test.cpp +++ b/src/terminal/SixelParser_test.cpp @@ -19,6 +19,7 @@ using namespace terminal; +#if 0 // TODO(pr) SixelImageBuilder sixelImageBuilder(ImageSize _size, RGBAColor _defaultColor) { return SixelImageBuilder(_size, 1, 1, _defaultColor, std::make_shared(16, 256)); @@ -327,3 +328,4 @@ TEST_CASE("SixelParser.newline", "[sixel]") } } +#endif diff --git a/src/terminal/Terminal.cpp b/src/terminal/Terminal.cpp index f58ef8e7b4..b7cb0b6d53 100644 --- a/src/terminal/Terminal.cpp +++ b/src/terminal/Terminal.cpp @@ -76,7 +76,7 @@ namespace // {{{ helpers Terminal::Terminal(Pty& _pty, int _ptyReadBufferSize, Terminal::Events& _eventListener, - optional _maxHistoryLineCount, + LineCount _maxHistoryLineCount, chrono::milliseconds _cursorBlinkInterval, chrono::steady_clock::time_point _now, string const& _wordDelimiters, @@ -163,7 +163,8 @@ bool Terminal::processInputOnce() auto const timeout = renderBuffer_.state == RenderBufferState::WaitingForRefresh && !screenDirty_ ? std::chrono::seconds(4) - : refreshInterval_ // std::chrono::seconds(0) + //: refreshInterval_ : std::chrono::seconds(0) + : std::chrono::seconds(30) ; auto const bufOpt = pty_.read(ptyReadBufferSize_, timeout); @@ -267,16 +268,59 @@ void Terminal::refreshRenderBuffer(RenderBuffer& _output) refreshRenderBufferInternal(_output); } +RenderCell makeRenderCell(ColorPalette const& _colorPalette, + Cell const& _cell, + RGBColor fg, RGBColor bg, + LineOffset _line, ColumnOffset _column) +{ + RenderCell cell; + cell.backgroundColor = bg; + cell.foregroundColor = fg; + cell.decorationColor = _cell.attributes().getUnderlineColor(_colorPalette, fg); + cell.position.row = _line; + cell.position.column = _column; + cell.flags = _cell.attributes().styles; + + if (!_cell.codepoints().empty()) + { +#if defined(LIBTERMINAL_IMAGES) + assert(!_cell.imageFragment().has_value()); +#endif + cell.codepoints = _cell.codepoints(); + } +#if defined(LIBTERMINAL_IMAGES) + else if (optional const& fragment = _cell.imageFragment(); fragment.has_value()) + { + assert(_cell.codepoints().empty()); + cell.flags |= CellFlags::Image; // TODO: this should already be there. + cell.image = _cell.imageFragment(); + } +#endif + +#if defined(LIBTERMINAL_HYPERLINKS) + if (_cell.hyperlink()) + { + auto const& color = _cell.hyperlink()->state == HyperlinkState::Hover + ? _colorPalette.hyperlinkDecoration.hover + : _colorPalette.hyperlinkDecoration.normal; + // TODO(decoration): Move property into Terminal. + auto const decoration = _cell.hyperlink()->state == HyperlinkState::Hover + ? CellFlags::Underline // TODO: decorationRenderer_.hyperlinkHover() + : CellFlags::DottedUnderline; // TODO: decorationRenderer_.hyperlinkNormal(); + cell.flags |= decoration; // toCellStyle(decoration); + cell.decorationColor = color; + } +#endif + return cell; +} + void Terminal::refreshRenderBufferInternal(RenderBuffer& _output) { auto const reverseVideo = screen_.isModeEnabled(terminal::DECMode::ReverseVideo); - auto const baseLine = - viewport_.absoluteScrollOffset(). - value_or(boxed_cast(screen_.historyLineCount())). - as(); + auto const baseLine = viewport_.scrollOffset(); auto const renderHyperlinks = screen_.contains(currentMousePosition_); auto const currentMousePositionRel = Coordinate{ - currentMousePosition_.row - unbox(viewport_.relativeScrollOffset()), + currentMousePosition_.row - viewport_.scrollOffset().as(), currentMousePosition_.column }; @@ -284,61 +328,21 @@ void Terminal::refreshRenderBufferInternal(RenderBuffer& _output) ++lastFrameID_; _output.frameID = lastFrameID_; + #if defined(CONTOUR_PERF_STATS) if (crispy::debugtag::enabled(TerminalTag)) debuglog(TerminalTag).write("{}: Refreshing render buffer.\n", lastFrameID_.load()); #endif +#if defined(LIBTERMINAL_HYPERLINKS) if (renderHyperlinks) { auto& cellAtMouse = screen_.at(currentMousePositionRel); if (cellAtMouse.hyperlink()) cellAtMouse.hyperlink()->state = HyperlinkState::Hover; // TODO: Left-Ctrl pressed? } - - // {{{ void appendCell(pos, cell, fg, bg) - auto const appendCell = [&](Coordinate const& _pos, Cell const& _cell, - RGBColor fg, RGBColor bg) - { - RenderCell cell; - cell.backgroundColor = bg; - cell.foregroundColor = fg; - cell.decorationColor = _cell.attributes().getUnderlineColor(screen_.colorPalette(), fg); - cell.position = _pos; - cell.flags = _cell.attributes().styles; - - if (!_cell.codepoints().empty()) - { -#if defined(LIBTERMINAL_IMAGES) - assert(!_cell.imageFragment().has_value()); -#endif - cell.codepoints = _cell.codepoints(); - } -#if defined(LIBTERMINAL_IMAGES) - else if (optional const& fragment = _cell.imageFragment(); fragment.has_value()) - { - assert(_cell.codepoints().empty()); - cell.flags |= CellFlags::Image; // TODO: this should already be there. - cell.image = _cell.imageFragment(); - } #endif - if (_cell.hyperlink()) - { - auto const& color = _cell.hyperlink()->state == HyperlinkState::Hover - ? screen_.colorPalette().hyperlinkDecoration.hover - : screen_.colorPalette().hyperlinkDecoration.normal; - // TODO(decoration): Move property into Terminal. - auto const decoration = _cell.hyperlink()->state == HyperlinkState::Hover - ? CellFlags::Underline // TODO: decorationRenderer_.hyperlinkHover() - : CellFlags::DottedUnderline; // TODO: decorationRenderer_.hyperlinkNormal(); - cell.flags |= decoration; // toCellStyle(decoration); - cell.decorationColor = color; - } - - _output.screen.emplace_back(std::move(cell)); - }; // }}} - screenDirty_ = false; _output.clear(); @@ -346,13 +350,13 @@ void Terminal::refreshRenderBufferInternal(RenderBuffer& _output) Gap, Sequence, }; - State state = State::Gap; - int lineNr = 1; screen_.render( - [&](Coordinate const& _pos, Cell const& _cell) // mutable + [this, reverseVideo, &_output, state = State::Gap, lineNr = LineOffset(0)] + (Cell const& _cell, LineOffset _line, ColumnOffset _column) + mutable { - auto const absolutePos = Coordinate{baseLine + (_pos.row - 1), _pos.column}; + auto const absolutePos = Coordinate{_line + viewport_.scrollOffset().as(), _column}; auto const selected = isSelectedAbsolute(absolutePos); auto const [fg, bg] = makeColors(screen_.colorPalette(), _cell, reverseVideo, selected); @@ -365,10 +369,10 @@ void Terminal::refreshRenderBufferInternal(RenderBuffer& _output) || !!_cell.attributes().styles; bool isNewLine = false; - if (lineNr != _pos.row) + if (lineNr != _line) { isNewLine = true; - lineNr = _pos.row; + lineNr = _line; if (!_output.screen.empty()) _output.screen.back().flags |= CellFlags::CellSequenceEnd; } @@ -379,7 +383,9 @@ void Terminal::refreshRenderBufferInternal(RenderBuffer& _output) if (!cellEmpty || customBackground) { state = State::Sequence; - appendCell(_pos, _cell, fg, bg); + _output.screen.emplace_back( + makeRenderCell(screen_.colorPalette(), + _cell, fg, bg, _line, _column)); _output.screen.back().flags |= CellFlags::CellSequenceStart; } break; @@ -391,7 +397,9 @@ void Terminal::refreshRenderBufferInternal(RenderBuffer& _output) } else { - appendCell(_pos, _cell, fg, bg); + _output.screen.emplace_back( + makeRenderCell(screen_.colorPalette(), + _cell, fg, bg, _line, _column)); if (isNewLine) _output.screen.back().flags |= CellFlags::CellSequenceStart; @@ -399,15 +407,17 @@ void Terminal::refreshRenderBufferInternal(RenderBuffer& _output) break; } }, - viewport_.absoluteScrollOffset() + viewport_.scrollOffset() ); +#if defined(LIBTERMINAL_HYPERLINKS) if (renderHyperlinks) { auto& cellAtMouse = screen_.at(currentMousePositionRel); if (cellAtMouse.hyperlink()) cellAtMouse.hyperlink()->state = HyperlinkState::Inactive; } +#endif _output.cursor = renderCursor(); } @@ -428,10 +438,10 @@ optional Terminal::renderCursor() : CursorShape::Rectangle; return RenderCursor{ - Coordinate( - screen_.cursor().position.row + viewport_.relativeScrollOffset().as(), + Coordinate{ + screen_.cursor().position.row + viewport_.scrollOffset().as(), screen_.cursor().position.column - ), + }, shape, cursorCell.width() }; @@ -518,11 +528,11 @@ bool Terminal::sendMousePressEvent(MouseButton _button, Modifier _modifier, Time selectionMode, wordDelimiters_, screen_, - absoluteCoordinate(currentMousePosition_) + currentMousePosition_ )); if (selectionMode != Selector::Mode::Linear) - selector()->extend(absoluteCoordinate(currentMousePosition_)); + selector()->extend(currentMousePosition_); } else if (selector()->state() == Selector::State::Complete) clearSelection(); @@ -539,7 +549,7 @@ void Terminal::clearSelection() bool Terminal::sendMouseMoveEvent(int _row, int _column, Modifier _modifier, Timestamp /*_now*/) { - auto const newPosition = Coordinate{_row, _column}; + auto const newPosition = Coordinate{LineOffset(_row), ColumnOffset(_column)}; bool const positionChanged = newPosition != currentMousePosition_; currentMousePosition_ = newPosition; @@ -569,14 +579,14 @@ bool Terminal::sendMouseMoveEvent(int _row, int _column, Modifier _modifier, Tim Selector::Mode::Linear, wordDelimiters_, screen_, - absoluteCoordinate(currentMousePosition_) + currentMousePosition_ )); } if (selectionAvailable() && selector()->state() != Selector::State::Complete) { changed = true; - selector()->extend(absoluteCoordinate(newPosition)); + selector()->extend(newPosition); breakLoopAndRefreshRenderBuffer(); return true; } @@ -708,11 +718,15 @@ bool Terminal::updateCursorHoveringState() return false; auto const relCursorPos = terminal::Coordinate{ - currentMousePosition_.row - viewport_.relativeScrollOffset().as(), + currentMousePosition_.row + viewport_.scrollOffset().as(), currentMousePosition_.column }; - auto const newState = screen_.contains(currentMousePosition_) && screen_.at(relCursorPos).hyperlink(); + auto const newState = screen_.contains(currentMousePosition_) +#if defined(LIBTERMINAL_HYPERLINKS) + && screen_.at(relCursorPos).hyperlink() +#endif + ; auto const oldState = hoveringHyperlink_.exchange(newState); return newState != oldState; } @@ -758,16 +772,17 @@ void Terminal::setWordDelimiters(string const& _wordDelimiters) string Terminal::extractSelectionText() const { +#if 0 // TODO(pr) using namespace terminal; - int lastColumn = 0; + ColumnOffset lastColumn = {}; string text; string currentLine; renderSelection([&](Coordinate const& _pos, Cell const& _cell) { auto const _lock = scoped_lock{ *this }; auto const isNewLine = _pos.column <= lastColumn; - auto const isLineWrapped = lineWrapped(_pos.row); - bool const touchesRightPage = _pos.row > 0 + auto const isLineWrapped = screen().isLineWrapped(_pos.row); + bool const touchesRightPage = _pos.row.value > 0 && isSelectedAbsolute({_pos.row - 1, screen_.size().columns.as()}); if (isNewLine && (!isLineWrapped || !touchesRightPage)) { @@ -785,10 +800,14 @@ string Terminal::extractSelectionText() const text += currentLine; return text; +#else + return {}; +#endif } string Terminal::extractLastMarkRange() const { +#if 0 // TODO(pr) using terminal::Coordinate; using terminal::Cell; @@ -799,7 +818,7 @@ string Terminal::extractLastMarkRange() const auto const marker1 = optional{bottomLine}; - auto const marker0 = screen_.findMarkerBackward(marker1.value()); + auto const marker0 = screen_.findMarkerUpwards(marker1.value()); if (!marker0.has_value()) return {}; @@ -818,6 +837,9 @@ string Terminal::extractLastMarkRange() const } return text; +#else + return {}; +#endif } // {{{ ScreenEvents overrides @@ -974,13 +996,12 @@ void Terminal::discardImage(Image const& _image) eventListener_.discardImage(_image); } -void Terminal::markRegionDirty(LinePosition _line, ColumnPosition _column) +void Terminal::markRegionDirty(LineOffset _line, ColumnOffset _column) { if (!selector_) return; - auto const y = screen_.toAbsoluteLine(*_line); - auto const coord = Coordinate{y, *_column}; + auto const coord = Coordinate{_line, _column}; if (selector_->contains(coord)) clearSelection(); } diff --git a/src/terminal/Terminal.h b/src/terminal/Terminal.h index d72d64ace3..67bde90ae2 100644 --- a/src/terminal/Terminal.h +++ b/src/terminal/Terminal.h @@ -68,7 +68,7 @@ class Terminal : public ScreenEvents { Terminal(Pty& _pty, int _ptyReadBufferSize, Events& _eventListener, - std::optional _maxHistoryLineCount = std::nullopt, + LineCount _maxHistoryLineCount = LineCount(0), std::chrono::milliseconds _cursorBlinkInterval = std::chrono::milliseconds{500}, std::chrono::steady_clock::time_point _now = std::chrono::steady_clock::now(), std::string const& _wordDelimiters = "", @@ -112,17 +112,6 @@ class Terminal : public ScreenEvents { // }}} // {{{ screen proxy - /// @returns absolute coordinate of @p _pos with scroll offset and applied. - Coordinate absoluteCoordinate(Coordinate const& _pos) const noexcept - { - // TODO: unit test case me BEFORE merge, yo ! - auto const row = viewport_.absoluteScrollOffset().value_or( - boxed_cast(screen_.historyLineCount())) + - StaticScrollbackPosition::cast_from(_pos.row - 1); - auto const col = _pos.column; - return Coordinate{unbox(row), col}; - } - /// Writes a given VT-sequence to screen. void writeToScreen(std::string_view _text); @@ -214,7 +203,7 @@ class Terminal : public ScreenEvents { /// Only access this when having the terminal object locked. Screen& screen() noexcept { return screen_; } - bool lineWrapped(int _lineNumber) const { return screen_.lineWrapped(_lineNumber); } + bool isLineWrapped(LineOffset _lineNumber) const noexcept { return screen_.isLineWrapped(_lineNumber); } Coordinate const& currentMousePosition() const noexcept { return currentMousePosition_; } @@ -306,9 +295,9 @@ class Terminal : public ScreenEvents { bool updateCursorHoveringState(); template - void renderPass(Renderer const& pass, RemainingPasses... remainingPasses) const + void renderPass(Renderer && pass, RemainingPasses... remainingPasses) const { - screen_.render(pass, viewport_.absoluteScrollOffset()); + screen_.render(std::forward(pass), viewport_.scrollOffset()); if constexpr (sizeof...(RemainingPasses) != 0) renderPass(std::forward(remainingPasses)...); @@ -342,7 +331,7 @@ class Terminal : public ScreenEvents { void useApplicationCursorKeys(bool _enabled) override; void hardReset() override; void discardImage(Image const&) override; - void markRegionDirty(LinePosition _line, ColumnPosition _column) override; + void markRegionDirty(LineOffset _line, ColumnOffset _column) override; void synchronizedOutput(bool _enabled) override; // private data @@ -375,7 +364,7 @@ class Terminal : public ScreenEvents { std::chrono::steady_clock::time_point lastClick_{}; unsigned int speedClicks_ = 0; - terminal::Coordinate currentMousePosition_{0, 0}; // current mouse position + terminal::Coordinate currentMousePosition_{}; // current mouse position Modifier mouseProtocolBypassModifier_ = Modifier::Shift; bool respectMouseProtocol_ = true; // shift-click can disable that, button release sets it back to true bool leftMouseButtonPressed_ = false; // tracks left-mouse button pressed state (used for cell selection). diff --git a/src/terminal/Terminal_test.cpp b/src/terminal/Terminal_test.cpp index 2017ad6211..97a868469e 100644 --- a/src/terminal/Terminal_test.cpp +++ b/src/terminal/Terminal_test.cpp @@ -45,9 +45,9 @@ namespace // {{{ helpers for (terminal::RenderCell const& cell: renderBuffer.buffer.screen) { auto const gap = (cell.position.column + static_cast(lastCount) - 1) - lastPos.column; - auto& currentLine = lines.at(cell.position.row - 1); - if (gap > 0) // Did we jump? - currentLine.insert(currentLine.end(), gap - 1, ' '); + auto& currentLine = lines.at(*cell.position.row - 1); + if (*gap > 0) // Did we jump? + currentLine.insert(currentLine.end(), *gap - 1, ' '); currentLine += unicode::convert_to(u32string_view(cell.codepoints)); lastPos = cell.position; @@ -120,7 +120,7 @@ namespace // {{{ helpers UNSCOPED_INFO(headline + ":"); for (int row = 1; row <= unbox(terminal().screen().size().lines); ++row) - UNSCOPED_INFO(fmt::format("[{}] \"{}\"", row, terminal().screen().renderTextLine(row))); + UNSCOPED_INFO(fmt::format("[{}] \"{}\"", row, terminal().screen().grid().renderTextLine(terminal::LineOffset(row)))); } private: diff --git a/src/terminal/Viewport.h b/src/terminal/Viewport.h index a70a460d5a..d498f8fa98 100644 --- a/src/terminal/Viewport.h +++ b/src/terminal/Viewport.h @@ -25,7 +25,7 @@ class Screen; class Viewport { - public: +public: using ModifyEvent = std::function; explicit Viewport(Screen& _screen, ModifyEvent _onModify = {}) : @@ -33,11 +33,7 @@ class Viewport modified_{ _onModify ? std::move(_onModify) : []() {} } {} - /// Returns the absolute offset where 0 is the top of scrollback buffer, and the maximum value the bottom of the screeen (plus history). - std::optional absoluteScrollOffset() const noexcept - { - return scrollOffset_; - } + ScrollOffset scrollOffset() const noexcept { return scrollOffset_; } /// Tests if the viewport has been moved(/scrolled) off its main view position. /// @@ -45,53 +41,32 @@ class Viewport /// @retval false viewport has NOT been moved/scrolled and is still located at its main view position. bool scrolled() const noexcept { - return scrollOffset_.has_value(); - } - - /// @returns scroll offset relative to the main screen buffer - RelativeScrollbackPosition relativeScrollOffset() const noexcept - { - if (!scrollOffset_) - return RelativeScrollbackPosition{0}; - - return RelativeScrollbackPosition::cast_from( - historyLineCount().as() - scrollOffset_->as() - ); + return scrollOffset_.value != 0; } - bool isLineVisible(int _row) const noexcept + bool isLineVisible(LineOffset _line) const noexcept { - return crispy::ascending( - long{1} - *relativeScrollOffset(), - static_cast(_row), - static_cast(*screenLineCount()) - *relativeScrollOffset() - ); + auto const a = scrollOffset_.as(); + auto const b = _line.as(); + auto const c = screenLineCount().as() + scrollOffset_.as(); + return a <= b && b < c; } bool scrollUp(LineCount _numLines) { - auto const newOffset = std::max( - absoluteScrollOffset().value_or(boxed_cast(historyLineCount())) - boxed_cast(_numLines), - StaticScrollbackPosition(0) - ); - return scrollToAbsolute(newOffset); + scrollOffset_.value -= _numLines.value; + return scrollTo(scrollOffset_); } bool scrollDown(LineCount _numLines) { - auto const newOffset = - absoluteScrollOffset().value_or(boxed_cast(historyLineCount())) - + boxed_cast(_numLines); - - return scrollToAbsolute(newOffset); + scrollOffset_.value += _numLines.value; + return scrollTo(scrollOffset_); } bool scrollToTop() { - if (absoluteScrollOffset()) - return scrollToAbsolute(StaticScrollbackPosition{0}); - else - return false; + return scrollTo(-historyLineCount().as()); } bool scrollToBottom() @@ -107,26 +82,23 @@ class Viewport if (!scrollOffset_) return false; - scrollOffset_.reset(); + scrollOffset_ = {}; modified_(); return true; } - bool scrollToAbsolute(StaticScrollbackPosition _absoluteScrollOffset) + bool scrollTo(ScrollOffset _offset) { if (scrollingDisabled()) return false; - if (StaticScrollbackPosition{0} <= _absoluteScrollOffset && _absoluteScrollOffset < boxed_cast(historyLineCount())) + if (0 <= *_offset && _offset < historyLineCount().as()) { - scrollOffset_.emplace(_absoluteScrollOffset); + scrollOffset_ = _offset; modified_(); return true; } - if (_absoluteScrollOffset >= boxed_cast(historyLineCount())) - return forceScrollToBottom(); - return false; } @@ -135,13 +107,9 @@ class Viewport if (scrollingDisabled()) return false; - auto const newScrollOffset = screen_.findMarkerBackward( - absoluteScrollOffset(). - value_or(historyLineCount().as()). - as() - ); + auto const newScrollOffset = screen_.findMarkerUpwards(scrollOffset_); if (newScrollOffset.has_value()) - return scrollToAbsolute(StaticScrollbackPosition::cast_from(*newScrollOffset)); + return scrollTo(*newScrollOffset); return false; } @@ -151,12 +119,9 @@ class Viewport if (scrollingDisabled()) return false; - auto const newScrollOffset = screen_.findMarkerForward( - static_cast(*absoluteScrollOffset().value_or(boxed_cast(historyLineCount()))) - ); - - if (newScrollOffset.has_value()) - return scrollToAbsolute(StaticScrollbackPosition{static_cast(*newScrollOffset)}); + auto const newScrollOffset = screen_.findMarkerDownwards(scrollOffset_); + if (newScrollOffset) + return scrollTo(*newScrollOffset); else return forceScrollToBottom(); @@ -177,7 +142,7 @@ class Viewport // Screen& screen_; ModifyEvent modified_; - std::optional scrollOffset_; //!< scroll offset relative to scroll top (0) or nullopt if not scrolled into history + ScrollOffset scrollOffset_; //!< scroll offset relative to scroll top (0) or nullopt if not scrolled into history }; } diff --git a/src/terminal/bench-headless.cpp b/src/terminal/bench-headless.cpp index ab341afed9..48a3c5dd09 100644 --- a/src/terminal/bench-headless.cpp +++ b/src/terminal/bench-headless.cpp @@ -125,7 +125,7 @@ void benchmarkTerminal() cout << fmt::format("{:>12}: {}\n\n", "history size", - vt.screen().maxHistoryLineCount().value_or(terminal::LineCount(0))); + vt.screen().maxHistoryLineCount()); } int main(int argc, char const* argv[]) diff --git a/src/terminal/primitives.h b/src/terminal/primitives.h index 759b27fc30..9be4e4d71e 100644 --- a/src/terminal/primitives.h +++ b/src/terminal/primitives.h @@ -16,14 +16,13 @@ namespace detail::tags // {{{ { // column types struct ColumnCount{}; - struct Column{}; + struct ColumnOffset{}; + struct ColumnPosition{}; // line types struct LineCount{}; - struct LinePosition{}; - struct RelativeLinePosition{}; - struct RelativeScrollbackPosition{}; - struct StaticScrollbackPosition{}; + struct LineOffset{}; + struct ScrollOffset{}; // misc. struct TabStopCount{}; @@ -52,7 +51,9 @@ using ColumnCount = crispy::boxed; /// (usually the main page unless scrolled upwards). /// /// A column position starts at 1. -using ColumnPosition = crispy::boxed; +using ColumnPosition = crispy::boxed; + +using ColumnOffset = crispy::boxed; // }}} // {{{ Line types @@ -60,28 +61,13 @@ using ColumnPosition = crispy::boxed; /// LineCount represents a number of lines. using LineCount = crispy::boxed; -/// LinePosition is the 1-based line coordinate of the main-page area (or viewport). -using LinePosition = crispy::boxed; - -/// RelativeScrollbackPosition represents scroll offset relative to the main page buffer. -/// -/// A value of 0 means bottom most scrollback, one line above main page area. -/// And a value equal to the number of scrollback lines minus one means -/// the top-most scrollback line. -using RelativeScrollbackPosition = crispy::boxed; - -/// RelativeLinePosition combines LinePosition and RelativeScrollbackPosition -/// into one, whereas values from 1 upwards are main page area, and -/// values from 0 downwards represent the scrollback lines. -using RelativeLinePosition = crispy::boxed; - -/// StaticScrollbackPosition represents scroll offset relative to scroll top (0). +/// Represents the line offset relative to main-page top. /// -/// A value of 0 means scroll top, and -/// a value equal to the number of scrollback lines -/// is the scroll bottom (main page area). -using StaticScrollbackPosition = crispy::boxed; +/// * 0 is top-most line on main page +/// * -1 is the bottom most line in scrollback +using LineOffset = crispy::boxed; +using ScrollOffset = crispy::boxed; // }}} // {{{ Range @@ -140,10 +126,11 @@ constexpr bool operator!=(PageSize a, PageSize b) noexcept { return !(a == b); } // }}} // {{{ Coordinate types -struct ScreenCoordinate // or name CursorPosition? +// (0, 0) is home position +struct ScreenPosition { - LinePosition line; - ColumnPosition column; + LineOffset line; + ColumnOffset column; }; // }}} diff --git a/src/terminal_renderer/BoxDrawingRenderer.cpp b/src/terminal_renderer/BoxDrawingRenderer.cpp index 959b339614..ae4abbe001 100644 --- a/src/terminal_renderer/BoxDrawingRenderer.cpp +++ b/src/terminal_renderer/BoxDrawingRenderer.cpp @@ -448,8 +448,8 @@ void BoxDrawingRenderer::clearCache() textureAtlas_ = std::make_unique(renderTarget().monochromeAtlasAllocator()); } -bool BoxDrawingRenderer::render(LinePosition _line, - ColumnPosition _column, +bool BoxDrawingRenderer::render(LineOffset _line, + ColumnOffset _column, uint8_t _id, RGBColor _color) { @@ -457,7 +457,7 @@ bool BoxDrawingRenderer::render(LinePosition _line, if (!data) return false; - auto const pos = gridMetrics_.map(Coordinate{*_line, *_column}); + auto const pos = gridMetrics_.map(_line, _column); atlas::TextureInfo const& ti = get<0>(*data); auto const x = pos.x; auto const y = pos.y; diff --git a/src/terminal_renderer/BoxDrawingRenderer.h b/src/terminal_renderer/BoxDrawingRenderer.h index 202f4b3d28..a53ba6b6b2 100644 --- a/src/terminal_renderer/BoxDrawingRenderer.h +++ b/src/terminal_renderer/BoxDrawingRenderer.h @@ -38,7 +38,7 @@ class BoxDrawingRenderer : public Renderable { /// Renders boxdrawing character. /// /// @param _char the boxdrawing character's codepoint modulo 0x2500 (a value between 0x00 and 0x7F). - bool render(LinePosition _line, ColumnPosition _column, uint8_t _id, RGBColor _color); + bool render(LineOffset _line, ColumnOffset _column, uint8_t _id, RGBColor _color); private: using TextureAtlas = atlas::MetadataTextureAtlas; diff --git a/src/terminal_renderer/GridMetrics.h b/src/terminal_renderer/GridMetrics.h index 1a02333445..2425c0a732 100644 --- a/src/terminal_renderer/GridMetrics.h +++ b/src/terminal_renderer/GridMetrics.h @@ -58,17 +58,17 @@ struct GridMetrics /// @param row screen coordinate's line (between 1 and number of screen lines) /// /// @return 2D point into drawing coordinate system - constexpr crispy::Point map(int col, int row) const noexcept + constexpr crispy::Point map(LineOffset _line, ColumnOffset _column) const noexcept { - return map(Coordinate{row, col}); + auto const x = pageMargin.left + (*_column - 1) * cellSize.width.as(); + auto const y = pageMargin.bottom + (*pageSize.lines - *_line) * cellSize.height.as(); + + return {x, y}; } - constexpr crispy::Point map(Coordinate const& _pos) const noexcept + constexpr crispy::Point map(Coordinate _pos) const noexcept { - auto const x = pageMargin.left + (_pos.column - 1) * cellSize.width.as(); - auto const y = pageMargin.bottom + (pageSize.lines.as() - _pos.row) * cellSize.height.as(); - - return {x, y}; + return map(_pos.row, _pos.column); } }; diff --git a/src/terminal_renderer/ImageRenderer.h b/src/terminal_renderer/ImageRenderer.h index 9e4d66fb24..8a81769c18 100644 --- a/src/terminal_renderer/ImageRenderer.h +++ b/src/terminal_renderer/ImageRenderer.h @@ -63,8 +63,8 @@ namespace std using FNV = crispy::FNV; return FNV{}(FNV{}.basis(), _key.imageId, - static_cast(_key.offset.row), - static_cast(_key.offset.column), + _key.offset.row.as(), + _key.offset.column.as(), _key.size.width.as(), _key.size.height.as()); } diff --git a/src/terminal_renderer/Renderer.cpp b/src/terminal_renderer/Renderer.cpp index c3506e1c60..54c6e2b651 100644 --- a/src/terminal_renderer/Renderer.cpp +++ b/src/terminal_renderer/Renderer.cpp @@ -360,8 +360,9 @@ optional Renderer::renderCursor(Terminal const& _terminal) return RenderCursor{ gridMetrics_.map( - _terminal.screen().cursor().position.column, - _terminal.screen().cursor().position.row + _terminal.viewport().relativeScrollOffset().as() + _terminal.screen().cursor().position.row + + _terminal.viewport().scrollOffset().as(), + _terminal.screen().cursor().position.column ), cursorShape, cursorCell.width() diff --git a/src/terminal_renderer/TextRenderer.cpp b/src/terminal_renderer/TextRenderer.cpp index eba8108d13..13a52524f5 100644 --- a/src/terminal_renderer/TextRenderer.cpp +++ b/src/terminal_renderer/TextRenderer.cpp @@ -134,9 +134,9 @@ void TextRenderer::renderCell(RenderCell const& _cell) if (isBoxDrawingCharacter) { - boxDrawingRenderer_.render( - LinePosition::cast_from(_cell.position.row), - ColumnPosition::cast_from(_cell.position.column), + bool const couldRender = boxDrawingRenderer_.render( + _cell.position.row, + _cell.position.column, codepoints[0] % 0x2500, _cell.foregroundColor );