From 51120ec93dedd37d04a04247130580b6537312af Mon Sep 17 00:00:00 2001 From: Christian Parpart Date: Sat, 24 Sep 2022 17:12:03 +0200 Subject: [PATCH] Adds VT sequence DECSCA, DECSEL, DECSED and DECSERA to support protected grid areas during erase operations (#29, #30, #31). --- metainfo.xml | 3 +- src/terminal/Cell.h | 15 ++- src/terminal/CellFlags.h | 6 +- src/terminal/Functions.h | 8 ++ src/terminal/Screen.cpp | 186 ++++++++++++++++++++++++++++++++++- src/terminal/Screen.h | 47 +++++++++ src/terminal/Screen_test.cpp | 149 ++++++++++++++++++++++++++++ src/terminal/primitives.h | 78 ++++++++------- 8 files changed, 448 insertions(+), 44 deletions(-) diff --git a/metainfo.xml b/metainfo.xml index ba3753a4f9..4b6016d504 100644 --- a/metainfo.xml +++ b/metainfo.xml @@ -108,7 +108,8 @@
  • Fixes vertical cursor movement for Sixel graphics with only newlines (#822).
  • Fixes Sixel rendering for images with aspect ratios other than 1:1.
  • Fixes Sixel rendering for images that show below but should be rendered above text (#831).
  • -
  • Removes `images.sixel_cursor_conformance` config option.
  • +
  • Removes `images.sixel_cursor_conformance` config option.
  • +
  • Adds VT sequence DECSCA, DECSEL, DECSED and DECSERA to support protected grid areas during erase operations (#29, #30, #31).
  • diff --git a/src/terminal/Cell.h b/src/terminal/Cell.h index f03ee5d061..dbe4395871 100644 --- a/src/terminal/Cell.h +++ b/src/terminal/Cell.h @@ -120,6 +120,8 @@ class CONTOUR_PACKED Cell uint8_t _width, HyperlinkId _hyperlink) noexcept; + void writeTextOnly(char32_t _ch, uint8_t _width) noexcept; + std::u32string codepoints() const; char32_t codepoint(size_t i) const noexcept; std::size_t codepointCount() const noexcept; @@ -292,12 +294,11 @@ inline void Cell::write(GraphicsAttributes const& _attributes, uint8_t _width, HyperlinkId _hyperlink) noexcept { - setWidth(_width); - - codepoint_ = _ch; + writeTextOnly(_ch, _width); if (extra_) { extra_->codepoints.clear(); + // Writing text into a cell destroys the image fragment (as least for Sixels). extra_->imageFragment = {}; } @@ -314,6 +315,14 @@ inline void Cell::write(GraphicsAttributes const& _attributes, } } +inline void Cell::writeTextOnly(char32_t _ch, uint8_t _width) noexcept +{ + setWidth(_width); + codepoint_ = _ch; + if (extra_) + extra_->codepoints.clear(); +} + inline void Cell::reset(GraphicsAttributes const& _attributes, HyperlinkId _hyperlink) noexcept { codepoint_ = 0; diff --git a/src/terminal/CellFlags.h b/src/terminal/CellFlags.h index c35ed9c5dd..0e303502ea 100644 --- a/src/terminal/CellFlags.h +++ b/src/terminal/CellFlags.h @@ -23,7 +23,7 @@ namespace terminal { -enum class CellFlags : uint16_t +enum class CellFlags : uint32_t { None = 0, @@ -43,6 +43,7 @@ enum class CellFlags : uint16_t Encircled = (1 << 13), Overline = (1 << 14), RapidBlinking = (1 << 15), + CharacterProtected = (1 << 16), // Character is protected by selective erase operations. }; constexpr CellFlags& operator|=(CellFlags& a, CellFlags b) noexcept @@ -101,7 +102,7 @@ struct formatter template auto format(const terminal::CellFlags _flags, FormatContext& ctx) { - static const std::array, 16> nameMap = { + static const std::array, 17> nameMap = { std::pair { terminal::CellFlags::Bold, std::string_view("Bold") }, std::pair { terminal::CellFlags::Faint, std::string_view("Faint") }, std::pair { terminal::CellFlags::Italic, std::string_view("Italic") }, @@ -118,6 +119,7 @@ struct formatter std::pair { terminal::CellFlags::Framed, std::string_view("Framed") }, std::pair { terminal::CellFlags::Encircled, std::string_view("Encircled") }, std::pair { terminal::CellFlags::Overline, std::string_view("Overline") }, + std::pair { terminal::CellFlags::CharacterProtected, std::string_view("CharacterProtected") }, }; std::string s; for (auto const& mapping: nameMap) diff --git a/src/terminal/Functions.h b/src/terminal/Functions.h index 0fa09323a0..4a0cb46ea5 100644 --- a/src/terminal/Functions.h +++ b/src/terminal/Functions.h @@ -376,6 +376,10 @@ constexpr inline auto DECERA = detail::CSI(std::nullopt, 0, 4, '$', 'z', VT constexpr inline auto DECFRA = detail::CSI(std::nullopt, 0, 4, '$', 'x', VTType::VT420, "DECFRA", "Fill rectangular area"); constexpr inline auto DECDC = detail::CSI(std::nullopt, 0, 1, '\'', '~', VTType::VT420, "DECDC", "Delete column"); constexpr inline auto DECIC = detail::CSI(std::nullopt, 0, 1, '\'', '}', VTType::VT420, "DECIC", "Insert column"); +constexpr inline auto DECSCA = detail::CSI(std::nullopt, 0, 1, '"', 'q', VTType::VT240, "DECSCA", "Select Character Protection Attribute"); +constexpr inline auto DECSED = detail::CSI('?', 0, 1, std::nullopt, 'J', VTType::VT240, "DECSED", "Selective Erase in Display"); +constexpr inline auto DECSERA = detail::CSI(std::nullopt, 0, 4, '$', '{', VTType::VT240, "DECSERA", "Selective Erase in Rectangular Area"); +constexpr inline auto DECSEL = detail::CSI('?', 0, 1, std::nullopt, 'K', VTType::VT240, "DECSEL", "Selective Erase in Line"); constexpr inline auto XTRESTORE = detail::CSI('?', 0, ArgsMax, std::nullopt, 'r', VTExtension::XTerm, "XTRESTORE", "Restore DEC private modes."); constexpr inline auto XTSAVE = detail::CSI('?', 0, ArgsMax, std::nullopt, 's', VTExtension::XTerm, "XTSAVE", "Save DEC private modes."); constexpr inline auto DECRM = detail::CSI('?', 1, ArgsMax, std::nullopt, 'l', VTType::VT100, "DECRM", "Reset DEC-mode"); @@ -527,6 +531,10 @@ inline auto const& functions() noexcept DECERA, DECFRA, DECIC, + DECSCA, + DECSED, + DECSERA, + DECSEL, XTRESTORE, XTSAVE, DECPS, diff --git a/src/terminal/Screen.cpp b/src/terminal/Screen.cpp index 207bc01f9b..b1057fba55 100644 --- a/src/terminal/Screen.cpp +++ b/src/terminal/Screen.cpp @@ -838,6 +838,7 @@ void Screen::sendTerminalId() _terminal.reply("\033[>{};{};{}c", Pp, Pv, Pc); } +// {{{ ED template void Screen::clearToEndOfScreen() { @@ -871,6 +872,7 @@ void Screen::clearScreen() // up in case the content is still needed. scrollUp(_state.pageSize.lines); } +// }}} template void Screen::eraseCharacters(ColumnCount _n) @@ -890,6 +892,139 @@ void Screen::eraseCharacters(ColumnCount _n) line.useCellAt(_state.cursor.position.column + i).reset(_state.cursor.graphicsRendition); } +// {{{ DECSEL +template +void Screen::selectiveEraseToEndOfLine() +{ + if (_terminal.isFullHorizontalMargins() && _state.cursor.position.column.value == 0) + selectiveEraseLine(_state.cursor.position.line); + else + selectiveErase(_state.cursor.position.line, + _state.cursor.position.column, + ColumnOffset::cast_from(_state.pageSize.columns)); +} + +template +void Screen::selectiveEraseToBeginOfLine() +{ + if (_terminal.isFullHorizontalMargins() + && _state.cursor.position.column.value == _state.pageSize.columns.value) + selectiveEraseLine(_state.cursor.position.line); + else + selectiveErase(_state.cursor.position.line, ColumnOffset(0), _state.cursor.position.column + 1); +} + +template +void Screen::selectiveEraseLine(LineOffset line) +{ + if (containsProtectedCharacters(line, ColumnOffset(0), ColumnOffset::cast_from(_state.pageSize.columns))) + { + selectiveErase(line, ColumnOffset(0), ColumnOffset::cast_from(_state.pageSize.columns)); + return; + } + + currentLine().reset(grid().defaultLineFlags(), _state.cursor.graphicsRendition); + + auto const left = ColumnOffset(0); + auto const right = boxed_cast(_state.pageSize.columns - 1); + auto const area = Rect { Top(*line), Left(*left), Bottom(*line), Right(*right) }; + _terminal.markRegionDirty(area); +} + +template +void Screen::selectiveErase(LineOffset line, ColumnOffset begin, ColumnOffset end) +{ + Cell* i = &at(line, begin); + Cell const* e = i + unbox(end - begin); + while (i != e) + { + if (i->isFlagEnabled(CellFlags::CharacterProtected)) + { + ++i; + continue; + } + i->reset(_state.cursor.graphicsRendition); + ++i; + } + + auto const left = begin; + auto const right = end - 1; + auto const area = Rect { Top(*line), Left(*left), Bottom(*line), Right(*right) }; + _terminal.markRegionDirty(area); +} + +template +bool Screen::containsProtectedCharacters(LineOffset line, ColumnOffset begin, ColumnOffset end) const +{ + Cell const* i = &at(line, begin); + Cell const* e = i + unbox(end - begin); + while (i != e) + { + if (i->isFlagEnabled(CellFlags::CharacterProtected)) + return true; + ++i; + } + return false; +} +// }}} +// {{{ DECSED +template +void Screen::selectiveEraseToEndOfScreen() +{ + selectiveEraseToEndOfLine(); + + auto const lineStart = unbox(_state.cursor.position.line) + 1; + auto const lineEnd = unbox(_state.pageSize.lines); + + for (auto const lineOffset: ranges::views::iota(lineStart, lineEnd)) + selectiveEraseLine(LineOffset::cast_from(lineOffset)); +} + +template +void Screen::selectiveEraseToBeginOfScreen() +{ + selectiveEraseToBeginOfLine(); + + for (auto const lineOffset: ranges::views::iota(0, *_state.cursor.position.line)) + selectiveEraseLine(LineOffset::cast_from(lineOffset)); +} + +template +void Screen::selectiveEraseScreen() +{ + for (auto const lineOffset: ranges::views::iota(0, *_state.pageSize.lines)) + selectiveEraseLine(LineOffset::cast_from(lineOffset)); +} +// }}} +// {{{ DECSERA +template +void Screen::selectiveEraseArea(Rect area) +{ + auto const [top, left, bottom, right] = applyOriginMode(area).clampTo(_state.pageSize); + assert(unbox(right) <= unbox(_state.pageSize.columns)); + assert(unbox(bottom) <= unbox(_state.pageSize.lines)); + + if (top.value > bottom.value || left.value > right.value) + return; + + for (int y = top.value; y <= bottom.value; ++y) + { + for (Cell& cell: grid() + .lineAt(LineOffset::cast_from(y)) + .useRange(ColumnOffset::cast_from(left), + ColumnCount::cast_from(right.value - left.value + 1))) + { + if (!cell.isFlagEnabled(CellFlags::CharacterProtected)) + { + cell.writeTextOnly(L' ', 1); + cell.setHyperlink(HyperlinkId(0)); + } + } + } +} +// }}} + +// {{{ EL template void Screen::clearToEndOfLine() { @@ -943,6 +1078,7 @@ void Screen::clearLine() auto const area = Rect { Top(*line), Left(*left), Bottom(*line), Right(*right) }; _terminal.markRegionDirty(area); } +// }}} template void Screen::moveCursorToNextLine(LineCount _n) @@ -1821,9 +1957,11 @@ void Screen::requestStatusString(RequestStatusString _value) case RequestStatusString::DECSNLS: return fmt::format("{}*|", _state.pageSize.lines); case RequestStatusString::SGR: return fmt::format("0;{}m", vtSequenceParameterString(_state.cursor.graphicsRendition)); - case RequestStatusString::DECSCA: // TODO - errorlog()(fmt::format("Requesting device status for {} not implemented yet.", _value)); - break; + case RequestStatusString::DECSCA: { + auto const isProtected = + _state.cursor.graphicsRendition.styles & CellFlags::CharacterProtected; + return fmt::format("{}\"q", isProtected ? 1 : 2); + } case RequestStatusString::DECSASD: switch (_state.activeStatusDisplay) { @@ -3184,6 +3322,48 @@ ApplyResult Screen::apply(FunctionDefinition const& function, Sequence con break; case DECDC: deleteColumns(seq.param_or(0, ColumnCount(1))); break; case DECIC: insertColumns(seq.param_or(0, ColumnCount(1))); break; + case DECSCA: { + auto const Pc = seq.param_or(0, 0); + switch (Pc) + { + case 1: + _state.cursor.graphicsRendition.styles |= CellFlags::CharacterProtected; + return ApplyResult::Ok; + case 0: + case 2: + _state.cursor.graphicsRendition.styles &= ~CellFlags::CharacterProtected; + return ApplyResult::Ok; + default: return ApplyResult::Invalid; + } + } + case DECSED: { + switch (seq.param_or(0, Sequence::Parameter { 0 })) + { + case 0: selectiveEraseToEndOfScreen(); break; + case 1: selectiveEraseToBeginOfScreen(); break; + case 2: selectiveEraseScreen(); break; + default: return ApplyResult::Unsupported; + } + return ApplyResult::Ok; + } + case DECSERA: { + auto const top = seq.param_or(0, Top(1)) - 1; + auto const left = seq.param_or(1, Left(1)) - 1; + auto const bottom = seq.param_or(2, Bottom::cast_from(_state.pageSize.lines)) - 1; + auto const right = seq.param_or(3, Right::cast_from(_state.pageSize.columns)) - 1; + selectiveEraseArea(Rect { top, left, bottom, right }); + return ApplyResult::Ok; + } + case DECSEL: { + switch (seq.param_or(0, Sequence::Parameter { 0 })) + { + case 0: selectiveEraseToEndOfLine(); break; + case 1: selectiveEraseToBeginOfLine(); break; + case 2: selectiveEraseLine(_state.cursor.position.line); break; + default: return ApplyResult::Invalid; + } + return ApplyResult::Ok; + } case DECRM: { ApplyResult r = ApplyResult::Ok; crispy::for_each(crispy::times(seq.parameterCount()), [&](size_t i) { diff --git a/src/terminal/Screen.h b/src/terminal/Screen.h index 5621d022f8..99992390f8 100644 --- a/src/terminal/Screen.h +++ b/src/terminal/Screen.h @@ -144,6 +144,23 @@ class Screen final: public ScreenBase, public capabilities::StaticDatabase void clearToEndOfScreen(); void clearScreen(); + // DECSEL + void selectiveEraseToBeginOfLine(); + void selectiveEraseToEndOfLine(); + void selectiveEraseLine(LineOffset line); + + // DECSED + void selectiveEraseToBeginOfScreen(); + void selectiveEraseToEndOfScreen(); + void selectiveEraseScreen(); + + void selectiveEraseArea(Rect area); + + void selectiveErase(LineOffset line, ColumnOffset begin, ColumnOffset end); + [[nodiscard]] bool containsProtectedCharacters(LineOffset line, + ColumnOffset begin, + ColumnOffset end) const; + void eraseCharacters(ColumnCount _n); // ECH void insertCharacters(ColumnCount _n); // ICH void deleteCharacters(ColumnCount _n); // DCH @@ -293,6 +310,36 @@ class Screen final: public ScreenBase, public capabilities::StaticDatabase return { pos.line + _state.margin.vertical.from, pos.column + _state.margin.horizontal.from }; } + [[nodiscard]] LineOffset applyOriginMode(LineOffset line) const noexcept + { + if (!_state.cursor.originMode) + return line; + else + return line + _state.margin.vertical.from; + } + + [[nodiscard]] ColumnOffset applyOriginMode(ColumnOffset column) const noexcept + { + if (!_state.cursor.originMode) + return column; + else + return column + _state.margin.horizontal.from; + } + + [[nodiscard]] Rect applyOriginMode(Rect area) const noexcept + { + if (!_state.cursor.originMode) + return area; + + auto const top = Top::cast_from(area.top.value + _state.margin.vertical.from.value); + auto const left = Left::cast_from(area.top.value + _state.margin.horizontal.from.value); + auto const bottom = Bottom::cast_from(area.bottom.value + _state.margin.vertical.from.value); + auto const right = Right::cast_from(area.right.value + _state.margin.horizontal.from.value); + // TODO: Should this automatically clamp to margin's botom/right values? + + return Rect { top, left, bottom, right }; + } + /// Clamps given coordinates, respecting DECOM (Origin Mode). CellLocation clampCoordinate(CellLocation coord) const noexcept { diff --git a/src/terminal/Screen_test.cpp b/src/terminal/Screen_test.cpp index db504e1fca..dc6f1eb9e4 100644 --- a/src/terminal/Screen_test.cpp +++ b/src/terminal/Screen_test.cpp @@ -1140,6 +1140,155 @@ TEST_CASE("InsertLines", "[screen]") // TODO: test with (top/bottom and left/right) margins enabled } +// {{{ DECSEL +TEST_CASE("DECSEL-0", "[screen]") +{ + // Erasing from the cursor position forwards to the end of the current line. + for (auto const param: { "0"sv, ""sv }) + { + INFO(fmt::format("param: \"{}\"", param)); + auto mock = MockTerm { PageSize { LineCount(2), ColumnCount(6) } }; + auto& screen = mock.terminal.primaryScreen(); + mock.writeToScreen( + fmt::format("AB{on}CDE{off}F", fmt::arg("on", "\033[1\"q"), fmt::arg("off", "\033[2\"q"))); + REQUIRE("ABCDEF" == screen.grid().lineText(LineOffset(0))); + mock.writeToScreen("\033[1;2H"); + mock.writeToScreen(fmt::format("\033[?{}K", param)); + REQUIRE("A CDE " == screen.grid().lineText(LineOffset(0))); + } +} + +TEST_CASE("DECSEL-1", "[screen]") +{ + // Erasing from the cursor position backwards to the beginning of the current line. + auto mock = MockTerm { PageSize { LineCount(2), ColumnCount(6) } }; + auto& screen = mock.terminal.primaryScreen(); + mock.writeToScreen( + fmt::format("A{on}BCD{off}EF", fmt::arg("on", "\033[1\"q"), fmt::arg("off", "\033[2\"q"))); + REQUIRE("ABCDEF" == screen.grid().lineText(LineOffset(0))); + + mock.writeToScreen("\033[1;5H"); + mock.writeToScreen("\033[?1K"); + REQUIRE(" BCD F" == screen.grid().lineText(LineOffset(0))); +} + +TEST_CASE("DECSEL-2", "[screen]") +{ + auto mock = MockTerm { PageSize { LineCount(2), ColumnCount(4) } }; + auto& screen = mock.terminal.primaryScreen(); + mock.writeToScreen("ABCD"); + REQUIRE("ABCD" == screen.grid().lineText(LineOffset(0))); + + mock.writeToScreen( + fmt::format("\ra{on}bc{off}d\r", fmt::arg("on", "\033[1\"q"), fmt::arg("off", "\033[2\"q"))); + REQUIRE("abcd" == screen.grid().lineText(LineOffset(0))); + + mock.writeToScreen("\033[?2K"); + REQUIRE(" bc " == screen.grid().lineText(LineOffset(0))); + + mock.writeToScreen(fmt::format( + "\r{on}A{off}BC{on}D", fmt::arg("on", "\033[1\"q"), fmt::arg("off", "\033[2\"q"))); // DECSCA 2 + REQUIRE("ABCD" == screen.grid().lineText(LineOffset(0))); + mock.writeToScreen("\033[?2K"); + REQUIRE("A D" == screen.grid().lineText(LineOffset(0))); +} +// }}} + +// {{{ DECSED +TEST_CASE("DECSED-0", "[screen]") +{ + for (auto const param: { "0"sv, ""sv }) + { + auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(3) } }; + auto& screen = mock.terminal.primaryScreen(); + + mock.writeToScreen(fmt::format("{on}A{off}B{on}C{off}\r\n" + "D{on}E{off}F\r\n" + "{on}G{off}H{on}I{off}", + fmt::arg("on", "\033[1\"q"), + fmt::arg("off", "\033[2\"q"))); + + REQUIRE(e(mainPageText(screen)) == "ABC\\nDEF\\nGHI\\n"); + + mock.writeToScreen("\033[2;2H"); + mock.writeToScreen(fmt::format("\033[?{}J", param)); + REQUIRE(e(mainPageText(screen)) == "ABC\\nDE \\nG I\\n"); + } +} + +TEST_CASE("DECSED-1", "[screen]") +{ + auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(3) } }; + auto& screen = mock.terminal.primaryScreen(); + + mock.writeToScreen(fmt::format("{on}A{off}B{on}C{off}\r\n" + "D{on}E{off}F\r\n" + "{on}G{off}H{on}I{off}", + fmt::arg("on", "\033[1\"q"), + fmt::arg("off", "\033[2\"q"))); + + REQUIRE(e(mainPageText(screen)) == "ABC\\nDEF\\nGHI\\n"); + + mock.writeToScreen("\033[2;2H"); + mock.writeToScreen("\033[?1J"); + REQUIRE(e(mainPageText(screen)) == "A C\\n EF\\nGHI\\n"); +} + +TEST_CASE("DECSED-2", "[screen]") +{ + auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(3) } }; + auto& screen = mock.terminal.primaryScreen(); + + mock.writeToScreen(fmt::format("{on}A{off}B{on}C{off}\r\n" + "D{on}E{off}F\r\n" + "{on}G{off}H{on}I{off}", + fmt::arg("on", "\033[1\"q"), + fmt::arg("off", "\033[2\"q"))); + + REQUIRE(e(mainPageText(screen)) == "ABC\\nDEF\\nGHI\\n"); + + mock.writeToScreen("\033[2;2H"); + mock.writeToScreen("\033[?2J"); + REQUIRE(e(mainPageText(screen)) == "A C\\n E \\nG I\\n"); +} +// }}} + +// {{{ DECSERA +TEST_CASE("DECSERA-all-defaults", "[screen]") +{ + auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(3) } }; + auto& screen = mock.terminal.primaryScreen(); + + mock.writeToScreen(fmt::format("{on}A{off}B{on}C{off}\r\n" + "D{on}E{off}F\r\n" + "{on}G{off}H{on}I{off}", + fmt::arg("on", "\033[1\"q"), + fmt::arg("off", "\033[2\"q"))); + + REQUIRE(e(mainPageText(screen)) == "ABC\\nDEF\\nGHI\\n"); + + mock.writeToScreen("\033[${"); + REQUIRE(e(mainPageText(screen)) == "A C\\n E \\nG I\\n"); +} + +TEST_CASE("DECSERA", "[screen]") +{ + auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(3) } }; + auto& screen = mock.terminal.primaryScreen(); + + mock.writeToScreen(fmt::format("{on}A{off}B{on}C{off}\r\n" + "D{on}E{off}F\r\n" + "{on}G{off}H{on}I{off}", + fmt::arg("on", "\033[1\"q"), + fmt::arg("off", "\033[2\"q"))); + + REQUIRE(e(mainPageText(screen)) == "ABC\\nDEF\\nGHI\\n"); + + mock.writeToScreen("\033[2;2;3;3${"); + REQUIRE(e(mainPageText(screen)) == "ABC\\nDE \\nG I\\n"); +} +// }}} + TEST_CASE("DeleteLines", "[screen]") { auto mock = MockTerm { PageSize { LineCount(3), ColumnCount(2) } }; diff --git a/src/terminal/primitives.h b/src/terminal/primitives.h index b1a0786d65..ae1d147d63 100644 --- a/src/terminal/primitives.h +++ b/src/terminal/primitives.h @@ -280,6 +280,41 @@ struct Range // iterator end() const { return crispy::boxed_cast(to) + iterator{1}; } }; +// }}} +// {{{ PageSize +struct PageSize +{ + LineCount lines; + ColumnCount columns; + [[nodiscard]] int area() const noexcept { return *lines * *columns; } +}; + +constexpr PageSize operator+(PageSize pageSize, LineCount lines) noexcept +{ + return PageSize { pageSize.lines + lines, pageSize.columns }; +} + +constexpr PageSize operator-(PageSize pageSize, LineCount lines) noexcept +{ + return PageSize { pageSize.lines - lines, pageSize.columns }; +} + +constexpr bool operator==(PageSize a, PageSize b) noexcept +{ + return a.lines == b.lines && a.columns == b.columns; +} + +constexpr bool operator!=(PageSize a, PageSize b) noexcept +{ + return !(a == b); +} + +/// Tests whether given CellLocation is within the right hand side's PageSize. +constexpr bool operator<(CellLocation location, PageSize pageSize) noexcept +{ + return location.line < boxed_cast(pageSize.lines) + && location.column < boxed_cast(pageSize.columns); +} // }}} // {{{ Rect & Margin @@ -298,6 +333,14 @@ struct Rect Left left; Bottom bottom; Right right; + + [[nodiscard]] Rect clampTo(PageSize size) const noexcept + { + return Rect { top, + left, + std::min(bottom, Bottom::cast_from(size.lines)), + std::min(right, Right::cast_from(size.columns)) }; + } }; // Screen's page margin @@ -325,41 +368,6 @@ constexpr Range vertical(PageMargin m) noexcept // Lengths and Ranges using Length = crispy::boxed; -// }}} -// {{{ PageSize -struct PageSize -{ - LineCount lines; - ColumnCount columns; - [[nodiscard]] int area() const noexcept { return *lines * *columns; } -}; - -constexpr PageSize operator+(PageSize pageSize, LineCount lines) noexcept -{ - return PageSize { pageSize.lines + lines, pageSize.columns }; -} - -constexpr PageSize operator-(PageSize pageSize, LineCount lines) noexcept -{ - return PageSize { pageSize.lines - lines, pageSize.columns }; -} - -constexpr bool operator==(PageSize a, PageSize b) noexcept -{ - return a.lines == b.lines && a.columns == b.columns; -} - -constexpr bool operator!=(PageSize a, PageSize b) noexcept -{ - return !(a == b); -} - -/// Tests whether given CellLocation is within the right hand side's PageSize. -constexpr bool operator<(CellLocation location, PageSize pageSize) noexcept -{ - return location.line < boxed_cast(pageSize.lines) - && location.column < boxed_cast(pageSize.columns); -} // }}} // {{{ Coordinate types