diff --git a/CHANGELOG.md b/CHANGELOG.md index e14aee8..8d321f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ * Added unit testing in `tests/` directory * Added zig build option * `extras/menu.h` - New widget added for adding a vertical scrolling menu +* `extras/border.h` - New widget added for drawing borders around a given +vector of strings * `screen.h` - Added hashing for `Pos` object to use as a key in `std::unordered_map` and other hashed objects * `text.h` - Add `raw_str` function to strip a string of any added ansi escape @@ -9,7 +11,7 @@ codes * Added new examples: * `examples/vertical_menu.cpp` for trialing menu.h * `examples/game.cpp` porting a simple "collect the coin" game to Rawterm - + * `examples/borders.cpp` for a simple display of how to use border.h ### v4.0.6 * `extras/pane.h` - Added handling for blacklisting Regions from the cursor diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 63915f1..42cc750 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,21 +1,23 @@ include_directories(${PROJECT_SOURCE_DIR}/rawterm) +add_executable(border borders.cpp) add_executable(cells cells.cpp) add_executable(colors colors.cpp) add_executable(cursor_position cursor_position.cpp) +add_executable(game game.cpp) add_executable(hello_world hello_world.cpp) add_executable(keys keys.cpp) add_executable(raw_escapes raw_escapes.cpp) add_executable(red_blue_panes red_blue_panes.cpp) add_executable(vertical_menu vertical_menu.cpp) -add_executable(game game.cpp) +target_link_libraries(border PUBLIC rawterm) target_link_libraries(cells PUBLIC rawterm) target_link_libraries(colors PUBLIC rawterm) target_link_libraries(cursor_position PUBLIC rawterm) +target_link_libraries(game PUBLIC rawterm) target_link_libraries(hello_world PUBLIC rawterm) target_link_libraries(keys PUBLIC rawterm) target_link_libraries(raw_escapes PUBLIC rawterm) target_link_libraries(red_blue_panes PUBLIC rawterm) target_link_libraries(vertical_menu PUBLIC rawterm) -target_link_libraries(game PUBLIC rawterm) diff --git a/examples/borders.cpp b/examples/borders.cpp new file mode 100644 index 0000000..0fd88b7 --- /dev/null +++ b/examples/borders.cpp @@ -0,0 +1,33 @@ +#include +#include +#include +#include +#include + +#include +#include + +int main() { + rawterm::enable_raw_mode(); + rawterm::enter_alt_screen(); + rawterm::enable_signals(); + + rawterm::Cursor::cursor_hide(); + rawterm::Cursor cur; + cur.reset(); + + std::vector text = {"Hello world", "foo", "bar", "some text again and again"}; + rawterm::Region region = rawterm::Region(rawterm::Pos(3, 3), rawterm::Pos(10, 25)); + auto border = rawterm::Border(region).set_padding(1).set_title("Hello world"); + border.draw(cur, &text); + + rawterm::Region region2 = rawterm::Region(rawterm::Pos(13, 3), rawterm::Pos(20, 40)); + + auto color = rawterm::Color(109, 192, 35); + auto border2 = rawterm::Border(region2, '#').set_title("This is my title").set_color(color); + border2.draw(cur, &text); + + std::ignore = rawterm::wait_for_input(); + rawterm::Cursor::cursor_show(); + return 0; +} diff --git a/examples/keys.cpp b/examples/keys.cpp index 1cc3468..042f676 100644 --- a/examples/keys.cpp +++ b/examples/keys.cpp @@ -1,7 +1,6 @@ #include #include #include -// #include #include #include diff --git a/rawterm/.clang-format b/rawterm/.clang-format new file mode 100644 index 0000000..a49eace --- /dev/null +++ b/rawterm/.clang-format @@ -0,0 +1,41 @@ +--- +BasedOnStyle: Chromium +AlignAfterOpenBracket: AlwaysBreak +AllowShortIfStatementsOnASingleLine: WithoutElse +ColumnLimit: '100' +ConstructorInitializerAllOnOneLineOrOnePerLine: 'true' +Cpp11BracedListStyle: 'true' +IncludeBlocks: Regroup +IncludeCategories: + # Standard library headers (e.g., , ) + - Regex: '<(algorithm|array|cassert|cctype|cerrno|cfloat|chrono|cmath|complex|condition_variable|csetjmp|csignal|cstdarg|cstddef|cstdint|cstdio|cstdlib|cstring|ctime|cwchar|cwctype|deque|exception|fstream|functional|initializer_list|iomanip|ios|iosfwd|iostream|istream|iterator|limits|list|locale|map|memory|mutex|new|numeric|ostream|queue|random|regex|scoped_allocator|set|sstream|stack|stdexcept|string|strstream|system_error|thread|tuple|type_traits|typeindex|typeinfo|unordered_map|unordered_set|utility|valarray|vector|[c]*[a-z_]+)>' + Priority: 1 + SortPriority: 1 + CaseSensitive: false + + # Third-party headers (enclosed in <>, but not standard library) + - Regex: '<.+[/0-9a-z_]*\.h[p]?[p]?>' + Priority: 2 + SortPriority: 2 + CaseSensitive: false + + # Project headers (enclosed in "", without paths) + - Regex: '".*\.h[p]?[p]?"' + Priority: 3 + SortPriority: 3 + CaseSensitive: false + + # Project headers (enclosed in "", with paths) + - Regex: '".+[/0-9a-z_]*\.h[p]?[p]?"' + Priority: 4 + SortPriority: 4 + CaseSensitive: false +IndentWidth: '4' +Language: Cpp +NamespaceIndentation: All +PointerAlignment: Left +ReflowComments: 'true' +SpaceBeforeCpp11BracedList: 'true' +SpacesInContainerLiterals: 'true' +Standard: Cpp11 +TabWidth: '4' diff --git a/rawterm/color.h b/rawterm/color.h index ff64466..aca3cc0 100644 --- a/rawterm/color.h +++ b/rawterm/color.h @@ -1,7 +1,6 @@ #ifndef RAWTERM_COLOR_H #define RAWTERM_COLOR_H -#include #include namespace rawterm { @@ -20,6 +19,10 @@ namespace rawterm { return os << std::to_string(c.red) + ";" + std::to_string(c.green) + ";" + std::to_string(c.blue) + "m"; } + + [[nodiscard]] bool operator==(const Color& other) const { + return this->red == other.red && this->blue == other.blue && this->green == other.green; + } }; // Color presets @@ -41,6 +44,7 @@ namespace rawterm { inline const Color purple {128, 0, 128}; inline const Color fuchsia {255, 0, 255}; inline const Color orange {255, 127, 0}; + inline const std::string reset = "\x1b[0m"; } // namespace Colors [[nodiscard]] std::string set_foreground(const std::string&, const Color&); diff --git a/rawterm/core.h b/rawterm/core.h index 400215d..aa75631 100644 --- a/rawterm/core.h +++ b/rawterm/core.h @@ -1,9 +1,6 @@ #ifndef RAWTERM_CORE_H #define RAWTERM_CORE_H -#include -#include - #include #include #include @@ -21,6 +18,9 @@ #include #include +#include +#include + #if __linux__ #include #include diff --git a/rawterm/extras/border.h b/rawterm/extras/border.h new file mode 100644 index 0000000..7e70f1d --- /dev/null +++ b/rawterm/extras/border.h @@ -0,0 +1,163 @@ +#ifndef RAWTERM_BORDER_H +#define RAWTERM_BORDER_H + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace rawterm { + struct Border { + Region size; + int border_padding = 0; + + std::optional border_char; + std::optional border_title; + std::optional border_color; + + static constexpr std::string CORNER_TL = "\u250C"; + static constexpr std::string CORNER_TR = "\u2510"; + static constexpr std::string CORNER_BL = "\u2514"; + static constexpr std::string CORNER_BR = "\u2518"; + static constexpr std::string VERTICAL_BAR = "\u2502"; + static constexpr std::string HORIZONTAL_BAR = "\u2500"; + + Border(Region size) : size(size), border_char(std::nullopt) {} + Border(Region size, char border_char) : size(size), border_char(border_char) {} + + Border& set_padding(int padding) { + border_padding = padding; + return *this; + } + + Border& set_title(const std::string& title) { + border_title = title; + return *this; + } + + Border& set_color(const Color& color) { + border_color = color; + return *this; + } + + [[nodiscard]] const std::string truncated_title() const { + if (!(border_title.has_value())) { + return ""; + } + + const int truncated_length = size.width() - 2; + if (truncated_length < 4) { + return "..."; + } + if (truncated_length >= border_title.value().size()) { + return border_title.value(); + } + return border_title.value().substr(0, size.width() - 4) + "..."; + } + + [[nodiscard]] std::vector render(const std::vector* text) const { + auto trunc_title = truncated_title(); + std::vector render = {""}; + int longest_txt = + std::max_element( + text->begin(), text->end(), + [](const std::string& a, const std::string& b) { return a.size() < b.size(); }) + ->size() + + border_padding; + if (longest_txt > size.width()) { + longest_txt = size.width() - 2; + } + + // Top line + const int post_title_len = (longest_txt + border_padding + 2) - trunc_title.size() - 1; + if (border_char.has_value()) { + render.at(0) = border_char.value() + trunc_title; + render.at(0) += std::string(post_title_len, border_char.value()); + } else { + render.at(0) = CORNER_TL + trunc_title; + for (int i = 0; i < post_title_len; i++) { + render.at(0) += HORIZONTAL_BAR; + } + render.at(0) += CORNER_TR; + } + + // Drawing text + for (const auto& line : *text) { + std::string rendered_line = ""; + if (border_char.has_value()) { + rendered_line.push_back(border_char.value()); + } else { + rendered_line += VERTICAL_BAR; + } + + std::string drawable_text = line; + if (line.size() > longest_txt) { + drawable_text = line.substr(0, longest_txt); + } + const int line_buffer = longest_txt - drawable_text.size(); + rendered_line += std::string(border_padding, ' ') + drawable_text + + std::string(border_padding + line_buffer, ' '); + + if (border_char.has_value()) { + rendered_line.push_back(border_char.value()); + } else { + rendered_line += VERTICAL_BAR; + } + + render.push_back(rendered_line); + } + + // Bottom line + std::string btm = ""; + if (border_char.has_value()) { + btm = std::string(render.at(0).size(), border_char.value()); + } else { + btm = CORNER_BL; + for (int i = 0; i <= longest_txt + border_padding; i++) { + btm += HORIZONTAL_BAR; + } + btm += CORNER_BR; + } + + render.push_back(btm); + + return render; + }; + + void draw(Cursor& cur, const std::vector* text) const { + // Disable if rawterm_debug + if (detail::is_debug()) { + return; + } + + // Check if terminal size is reasonable and early return if not + const Pos current_term_size = get_term_size(); + if (current_term_size.vertical < size.height()) { + return; + } else if (current_term_size.horizontal < size.width()) { + return; + } + + const std::vector render_lines = render(text); + for (int i = 0; i < render_lines.size(); i++) { + cur.move({size.top_left.vertical + i, size.top_left.horizontal}); + + if (border_color.has_value()) { + std::cout << "\x1b[" << border_color.value(); + } + std::cout << render_lines.at(i) << std::flush; + if (border_color.has_value()) { + std::cout << Colors::reset; + } + } + } + }; + +} // namespace rawterm + +#endif // RAWTERM_BORDER_H diff --git a/rawterm/extras/pane.h b/rawterm/extras/pane.h index c6a9dd8..be36225 100644 --- a/rawterm/extras/pane.h +++ b/rawterm/extras/pane.h @@ -1,9 +1,3 @@ -#include -#include -#include -#include -#include - #include #include #include @@ -15,6 +9,12 @@ #include #include +#include +#include +#include +#include +#include + namespace rawterm { template > class PaneManager { diff --git a/rawterm/screen.cpp b/rawterm/screen.cpp index 1699479..e0b8000 100644 --- a/rawterm/screen.cpp +++ b/rawterm/screen.cpp @@ -80,4 +80,13 @@ namespace rawterm { // Return the intersection region return {intersection_top_left, intersection_bottom_right}; } + + [[nodiscard]] const int Region::width() const { + return bottom_right.horizontal - top_left.horizontal; + } + + [[nodiscard]] const int Region::height() const { + return bottom_right.vertical - top_left.vertical; + } + } // namespace rawterm diff --git a/rawterm/screen.h b/rawterm/screen.h index 40c878f..98f53a1 100644 --- a/rawterm/screen.h +++ b/rawterm/screen.h @@ -31,6 +31,8 @@ namespace rawterm { Region(const Pos& tl, const Pos& br) : top_left(tl), bottom_right(br) {} [[nodiscard]] bool contains(const Pos& cmp) const; [[nodiscard]] Region intersect(const Region& other); + [[nodiscard]] const int width() const; + [[nodiscard]] const int height() const; }; } // namespace rawterm diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1b94b7c..a5a7a3d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,6 +4,7 @@ add_compile_options(-g) add_executable(test_exe main.cpp # DO not modify + border_test.cpp color_test.cpp core_test.cpp cursor_test.cpp diff --git a/tests/border_test.cpp b/tests/border_test.cpp new file mode 100644 index 0000000..db0ce70 --- /dev/null +++ b/tests/border_test.cpp @@ -0,0 +1,102 @@ +#include "extras/border.h" + +#include +#include + +#include "include/ut.hpp" + +boost::ut::suite<"Border"> border_suite = [] { + using namespace boost::ut; + + auto b = rawterm::Border(rawterm::Region({1, 1}, {10, 10})); + auto b2 = rawterm::Border(rawterm::Region({1, 1}, {10, 10}), '#'); + auto b3 = rawterm::Border(rawterm::Region({1, 1}, {10, 10})); + std::vector text = { + "Lorem ipsum dolor sit amet", "consectetur adipiscing elit.", "Morbi ipsum ex semper", + "placerat quam finibus sollicitudin. Nullam condimentum tellus ante"}; + + "render without settings"_test = [&b, &text] { + std::vector expected = {"┌────────┐", "│Lorem i│", "│consect│", + "│Morbi i│", "│placera│", "└────────┘"}; + + auto rendered = b.render(&text); + expect(rendered == expected); + // either end is a box drawing char, which is 3 bytes + expect(rendered.at(1).size() == 13); + }; + + "Set padding"_test = [&b] { + expect(b.border_padding == 0); + b.set_padding(1); + expect(b.border_padding == 1); + }; + + "render with padding"_test = [&b, &text] { + std::vector expected = {"┌─────────┐", "│ Lorem i │", "│ consect │", + "│ Morbi i │", "│ placera │", "└─────────┘"}; + + auto rendered = b.render(&text); + expect(rendered == expected); + + // either end is a box drawing char, which is 3 bytes + expect(rendered.at(1).size() == 15); + }; + + "Set title"_test = [&b, &b2] { + b.set_title("Super long title please"); + expect(b.border_title == "Super long title please"); + + b2.set_title("Test"); + expect(b2.border_title == "Test"); + }; + + "truncated title"_test = [&b, &b2] { + expect(b.truncated_title() == "Super..."); + expect(b2.truncated_title() == "Test"); + }; + + "render with padding and title"_test = [&b, &text] { + auto rendered = b.render(&text); + std::string expected = "┌Super...─┐"; + + expect(rendered.at(0) == expected); + }; + + "render with title"_test = [&b2, &text] { + auto rendered = b2.render(&text); + std::string expected = "#Test####"; + + expect(rendered.at(0) == expected); + }; + + "Set color"_test = [&b, &b2, &b3] { + b.set_color(rawterm::Color("#181818")); + b2.set_color(rawterm::Color(255, 255, 255)); + b3.set_color(rawterm::Color("#FF0000")); + + expect(b.border_color.value() == rawterm::Color("#181818")); + expect(b2.border_color.value() == rawterm::Color(255, 255, 255)); + expect(b3.border_color.value() == rawterm::Color("#FF0000")); + }; + + "render with padding, title, color"_test = [&b, &text] { + auto rendered = b.render(&text); + std::string expected = "┌Super...─┐"; + + expect(rendered.at(0) == expected) << rendered.at(0); + }; + + // "render with title, color"_test = [&b2, &text] { + // auto rendered = b2.render(&text); + // std::string expected = ""; + // + // expect(rendered.at(0) == expected); + // }; + // + // "render with color"_test = [&b3, &text] { + // auto rendered = b3.render(&text); + // std::string expected = ""; + // + // expect(rendered.at(0) == expected); + // }; +}; diff --git a/tests/screen_test.cpp b/tests/screen_test.cpp index 0ef5fe1..d1ceb64 100644 --- a/tests/screen_test.cpp +++ b/tests/screen_test.cpp @@ -43,4 +43,14 @@ boost::ut::suite<"Screen"> screen_suite = [] { expect(intersect.bottom_right.vertical = 5); expect(intersect.bottom_right.horizontal = 5); }; + + "height"_test = [] { + auto r = rawterm::Region({1, 1}, {5, 5}); + expect(r.height() == 4); + }; + + "width"_test = [] { + auto r = rawterm::Region({1, 1}, {5, 5}); + expect(r.width() == 4); + }; };