├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── examples ├── bar_chart.cpp ├── gauge.cpp ├── hello_world.cpp ├── list.cpp └── paragraph.cpp ├── single_include └── tui │ └── tui.hpp └── test ├── test_main.cpp └── test_tui.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | *.exe 3 | *.o 4 | test_output.txt 5 | !./examples/*.cpp 6 | !./test/*.cpp -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Edward Wibowo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OSFLAG := 2 | ifeq ($(OS), Windows_NT) 3 | test-executable = test.exe 4 | test-open = start 5 | ncurses-flag = 6 | else 7 | test-executable = test 8 | test-open = 9 | ncurses-flag = -lncurses 10 | endif 11 | 12 | .PHONY: test test-compile build-examples 13 | 14 | test: ./test/$(test-executable) 15 | $(info Test output will be written to ./test/test_output.txt) 16 | cd test && $(test-open) $(test-executable) --durations yes --out test_output.txt 17 | 18 | ./test/$(test-executable): ./test/test_main.o 19 | g++ -std=c++17 ./test/test_main.o ./test/test_tui.cpp $(ncurses-flag) -o ./test/test 20 | 21 | ./test/test_main.o: 22 | g++ ./test/test_main.cpp -c -o ./test/test_main.o 23 | 24 | test-compile: 25 | ifeq (,$(wildcard ./test/test_main.o)) 26 | $(info Compiling ./test/test_main.cpp, this only needs to be done once.) 27 | g++ ./test/test_main.cpp -c -o ./test/test_main.o 28 | g++ -std=c++17 ./test/test_main.o ./test/test_tui.cpp $(ncurses-flag) -o ./test/test 29 | else 30 | g++ -std=c++17 ./test/test_main.o ./test/test_tui.cpp $(ncurses-flag) -o ./test/test 31 | endif 32 | 33 | build-examples: 34 | g++ -std=c++17 ./examples/bar_chart.cpp $(ncurses-flag) -o ./examples/bar_chart 35 | g++ -std=c++17 ./examples/gauge.cpp $(ncurses-flag) -o ./examples/gauge 36 | g++ -std=c++17 ./examples/hello_world.cpp $(ncurses-flag) -o ./examples/hello_world 37 | g++ -std=c++17 ./examples/list.cpp $(ncurses-flag) -o ./examples/list 38 | g++ -std=c++17 ./examples/paragraph.cpp $(ncurses-flag) -o ./examples/paragraph 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # termui-cpp 2 | 3 | C++ header-only terminal user interface library. 4 | 5 | termui-cpp is currently available for Windows with partial support for Unix (through Ncurses). 6 | 7 | ## Features 8 | 9 | - Premade widgets 10 | - Event handling 11 | - Custom styling 12 | - Colors (Windows only) 13 | 14 | ## Installation 15 | 16 | 1. Download the latest [single header version](https://raw.githubusercontent.com/claby2/termui-cpp/master/single_include/tui/tui.hpp). 17 | 2. Either put the header file in a central location (with a specified path) or directly in your project tree. 18 | 19 | ## Hello World 20 | 21 | ```cpp 22 | #include 23 | 24 | int main() { 25 | // Construct window with default dimensions 26 | tui::Window window; 27 | 28 | window.set_title("Hello, World!"); 29 | 30 | tui::Paragraph p; 31 | p.text = "Hello, World!"; 32 | p.set_dimensions(0, 0, 25, 5); 33 | 34 | bool quit = false; 35 | tui::Event event; 36 | 37 | // Add paragraph widget to the window 38 | window.add(p); 39 | 40 | while(!quit) { 41 | if(window.poll_event(event)) { 42 | if(event.type == tui::KEYDOWN) { 43 | quit = true; 44 | } 45 | } 46 | window.render(); 47 | } 48 | 49 | window.close(); 50 | return 0; 51 | } 52 | ``` 53 | 54 | ## Widgets 55 | 56 | - [Bar Chart](./examples/bar_chart.cpp) 57 | - [Gauge](./examples/gauge.cpp) 58 | - [List](./examples/list.cpp) 59 | - [Paragraph](./examples/paragraph.cpp) 60 | 61 | Examples in `/examples` are cross-platform; however, some features may be limited on unix. 62 | 63 | Build all the examples with `make build-examples`. 64 | 65 | ## Testing 66 | 67 | Testing requires [Catch2](https://github.com/catchorg/Catch2/). 68 | 69 | Test with `make test`. Compile the tests with `make test-compile`. 70 | 71 | The test output is written to the file `/test/test_output.txt`. 72 | -------------------------------------------------------------------------------- /examples/bar_chart.cpp: -------------------------------------------------------------------------------- 1 | #include "../single_include/tui/tui.hpp" 2 | 3 | int main() { 4 | // Construct window 5 | tui::Window window; 6 | 7 | window.set_title("Bar Chart Example"); 8 | 9 | tui::BarChart bc; 10 | bc.data = {134, 145, 80, 70, 30}; 11 | bc.labels = {"first", "second", "third", "fourth", "fifth"}; 12 | bc.title = "Bar Chart"; 13 | bc.set_dimensions(0, 0, 40, 20); 14 | bc.bar_width = 5; 15 | bc.bar_color = tui::RED; 16 | bc.label_style.foreground = tui::BLUE; 17 | bc.number_style.foreground = tui::YELLOW; 18 | 19 | bool quit = false; 20 | tui::Event event; 21 | 22 | // Add bar chart widget to the window 23 | window.add(bc); 24 | 25 | while(!quit) { 26 | if(window.poll_event(event)) { 27 | if(event.type == tui::KEYDOWN) { 28 | switch(event.key) { 29 | case 'q': 30 | quit = true; 31 | break; 32 | } 33 | } 34 | } 35 | window.render(); 36 | } 37 | 38 | window.close(); 39 | return 0; 40 | } -------------------------------------------------------------------------------- /examples/gauge.cpp: -------------------------------------------------------------------------------- 1 | #include "../single_include/tui/tui.hpp" 2 | 3 | int main() { 4 | // Construct window 5 | tui::Window window; 6 | 7 | window.set_title("Gauge Example"); 8 | 9 | tui::Gauge g0; 10 | g0.title = "Small Gauge"; 11 | g0.set_dimensions(0, 0, 30, 3); 12 | g0.label = "75%"; 13 | g0.percent = 75; 14 | g0.label_style.foreground = tui::BLACK; 15 | g0.bar_color = tui::YELLOW; 16 | 17 | tui::Gauge g1; 18 | g1.title = "Big Gauge"; 19 | g1.set_dimensions(0, 3, 30, 10); 20 | g1.label = "30%"; 21 | g1.percent = 30; 22 | g1.label_style.foreground = tui::BLACK; 23 | g1.bar_color = tui::GREEN; 24 | 25 | bool quit = false; 26 | tui::Event event; 27 | 28 | // Add bar gauge widgets to the window 29 | window.add(g0, g1); 30 | 31 | while(!quit) { 32 | if(window.poll_event(event)) { 33 | if(event.type == tui::KEYDOWN) { 34 | switch(event.key) { 35 | case 'q': 36 | quit = true; 37 | break; 38 | } 39 | } 40 | } 41 | window.render(); 42 | } 43 | 44 | window.close(); 45 | return 0; 46 | } -------------------------------------------------------------------------------- /examples/hello_world.cpp: -------------------------------------------------------------------------------- 1 | #include "../single_include/tui/tui.hpp" 2 | 3 | int main() { 4 | // Construct window 5 | tui::Window window; 6 | 7 | window.set_title("Hello, World!"); 8 | 9 | tui::Paragraph p; 10 | p.text = "Hello, World!"; 11 | p.set_dimensions(0, 0, 25, 5); 12 | 13 | bool quit = false; 14 | tui::Event event; 15 | 16 | // Add paragraph widget to the window 17 | window.add(p); 18 | 19 | while(!quit) { 20 | if(window.poll_event(event)) { 21 | if(event.type == tui::KEYDOWN) { 22 | quit = true; 23 | } 24 | } 25 | window.render(); 26 | } 27 | 28 | window.close(); 29 | return 0; 30 | } -------------------------------------------------------------------------------- /examples/list.cpp: -------------------------------------------------------------------------------- 1 | #include "../single_include/tui/tui.hpp" 2 | 3 | int main() { 4 | // Construct window 5 | tui::Window window; 6 | 7 | window.set_title("List Example"); 8 | 9 | tui::List l; 10 | l.title = "List"; 11 | l.rows = { 12 | "[0] This", 13 | "[1] is", 14 | "[2] a", 15 | "[3] list.", 16 | "[4] This line is longer than the other rows.", 17 | "[5] You can control lists!", 18 | "[6] Hello, World!", 19 | "[8] Foo", 20 | "[9] Bar", 21 | "[10] Last element" 22 | }; 23 | l.text_style.foreground = tui::YELLOW; 24 | l.set_dimensions(0, 0, 25, 8); 25 | 26 | bool quit = false; 27 | tui::Event event; 28 | 29 | while(!quit) { 30 | if(window.poll_event(event)) { 31 | if(event.type == tui::KEYDOWN) { 32 | switch(event.key) { 33 | case 'q': 34 | quit = true; 35 | break; 36 | case 'j': 37 | l.scroll_down(window); 38 | break; 39 | case 'k': 40 | l.scroll_up(window); 41 | break; 42 | } 43 | } 44 | } 45 | // Add list widget to the window 46 | // in the while loop to update 47 | // the list for any changes. 48 | window.add(l); 49 | window.render(); 50 | } 51 | 52 | window.close(); 53 | return 0; 54 | } -------------------------------------------------------------------------------- /examples/paragraph.cpp: -------------------------------------------------------------------------------- 1 | #include "../single_include/tui/tui.hpp" 2 | 3 | int main() { 4 | // Construct window 5 | tui::Window window; 6 | 7 | window.set_title("Paragraph Example"); 8 | 9 | tui::Paragraph p0; 10 | p0.text = "Borderless Text"; 11 | p0.set_dimensions(0, 0, 20, 5); 12 | p0.border = false; 13 | 14 | tui::Paragraph p1; 15 | p1.text = "Hello"; 16 | p1.text = "Hello, World!"; 17 | p1.set_dimensions(23, 0, 36, 5); 18 | 19 | tui::Paragraph p2; 20 | p2.title = "Multiline"; 21 | p2.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus lacinia dui ut augue eleifend, at condimentum felis porta. Aenean at."; 22 | p2.set_dimensions(0, 5, 59, 5); 23 | p2.border_style.foreground = tui::YELLOW; 24 | 25 | tui::Paragraph p3; 26 | p3.title = "Colored title"; 27 | p3.text = "You can customize foreground and background color!"; 28 | p3.set_dimensions(0, 10, 59, 5); 29 | p3.title_style.foreground = tui::BLACK; 30 | p3.title_style.background = tui::WHITE; 31 | p3.text_style.foreground = tui::YELLOW; 32 | p3.text_style.background = tui::CYAN; 33 | 34 | tui::Paragraph p4; 35 | p4.title = "Text Box with Wrapping"; 36 | p4.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis sed dui bibendum, consequat mauris molestie, ultricies enim. Nunc ut pellentesque orci. Nullam at maximus sapien. Quisque egestas posuere ex tempor mattis. In diam ex, vestibulum eget tortor a, convallis euismod dui. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc."; 37 | p4.set_dimensions(60, 0, 40, 15); 38 | p4.border_style.foreground = tui::BLUE; 39 | 40 | bool quit = false; 41 | tui::Event event; 42 | 43 | // Add paragraph widgets to the window 44 | window.add(p0, p1, p2, p3, p4); 45 | 46 | while(!quit) { 47 | if(window.poll_event(event)) { 48 | if(event.type == tui::KEYDOWN) { 49 | switch(event.key) { 50 | case 'q': 51 | quit = true; 52 | break; 53 | } 54 | } 55 | } 56 | window.render(); 57 | } 58 | 59 | window.close(); 60 | return 0; 61 | } -------------------------------------------------------------------------------- /single_include/tui/tui.hpp: -------------------------------------------------------------------------------- 1 | #ifndef TUI_HPP 2 | #define TUI_HPP 3 | 4 | #ifdef _WIN32 5 | # define IS_WIN 6 | #include 7 | #else 8 | # define IS_POSIX 9 | #include 10 | #include 11 | #include 12 | #endif 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | namespace tui { 23 | // Event handling 24 | enum EventType { 25 | KEYDOWN, 26 | MOUSEBUTTONDOWN, 27 | UNDEFINED 28 | }; 29 | 30 | #ifdef IS_WIN 31 | struct Event { 32 | EventType type = UNDEFINED; 33 | uint8_t key; 34 | }; 35 | 36 | // Color handling 37 | enum Color : short { 38 | BLACK = 0x0000, 39 | DARK_BLUE = 0x0001, 40 | DARK_GREEN = 0x0002, 41 | DARK_CYAN = 0x0003, 42 | DARK_RED = 0x0004, 43 | DARK_MAGENTA = 0x0005, 44 | DARK_YELLOW = 0x0006, 45 | GRAY = 0x0007, 46 | DARK_GRAY = 0x0008, 47 | BLUE = 0x0009, 48 | GREEN = 0x000A, 49 | CYAN = 0x000B, 50 | RED = 0x000C, 51 | MAGENTA = 0x000D, 52 | YELLOW = 0x000E, 53 | WHITE = 0x000F 54 | }; 55 | #elif defined(IS_POSIX) 56 | struct Event { 57 | EventType type = UNDEFINED; 58 | int key; 59 | }; 60 | 61 | // Color handling 62 | enum Color { 63 | BLACK = 0, 64 | RED = 1, 65 | GREEN = 2, 66 | YELLOW = 3, 67 | BLUE = 4, 68 | MAGENTA = 5, 69 | CYAN = 6, 70 | WHITE = 7 71 | }; 72 | #endif 73 | 74 | // Return color after bitwise operation as short 75 | // Combines foreground and background 76 | inline short get_color(short foreground, short background) { 77 | return (foreground | (background << 4)); 78 | } 79 | 80 | // Widget definitions 81 | struct Widget { 82 | struct { 83 | short foreground = WHITE; 84 | short background = BLACK; 85 | } border_style; 86 | struct { 87 | short foreground = WHITE; 88 | short background = BLACK; 89 | } text_style; 90 | struct { 91 | short foreground = WHITE; 92 | short background = BLACK; 93 | } title_style; 94 | std::string title; 95 | bool border = true; 96 | int x; // Position of left side of widget 97 | int y; // Position of top of widget 98 | int width; // Width of widget 99 | int height; // Height of widget 100 | 101 | void set_dimensions(int x, int y, int width, int height); 102 | }; 103 | 104 | struct Paragraph : Widget { 105 | std::string text; 106 | }; 107 | 108 | struct List : Widget{ 109 | std::vector rows; 110 | int first_element = 0; // Element at the top of the list 111 | template 112 | void scroll_up(Window &window, int factor = 1); 113 | template 114 | void scroll_down(Window &window, int factor = 1); 115 | }; 116 | 117 | struct BarChart : Widget { 118 | std::vector data; 119 | std::vector labels; 120 | int bar_width; 121 | short bar_color; 122 | struct { 123 | short foreground = WHITE; 124 | short background = BLACK; 125 | } label_style; 126 | struct { 127 | short foreground = WHITE; 128 | short background = BLACK; 129 | } number_style; 130 | }; 131 | 132 | struct Gauge : Widget { 133 | int percent; 134 | std::string label; 135 | short bar_color; 136 | struct { 137 | short foreground = WHITE; 138 | short background = BLACK; 139 | } label_style; 140 | }; 141 | 142 | // Widget comparisons 143 | bool operator==(const Paragraph& paragraph1, const Paragraph& paragraph2) { 144 | return ( 145 | paragraph1.title == paragraph2.title && 146 | paragraph1.text == paragraph2.text && 147 | paragraph1.x == paragraph2.x && 148 | paragraph1.y == paragraph2.y && 149 | paragraph1.width == paragraph2.width && 150 | paragraph1.height == paragraph2.height && 151 | paragraph1.border == paragraph2.border && 152 | paragraph1.border_style.foreground == paragraph2.border_style.foreground && 153 | paragraph1.border_style.background == paragraph2.border_style.background && 154 | paragraph1.text_style.foreground == paragraph2.text_style.foreground && 155 | paragraph1.text_style.background == paragraph2.text_style.background && 156 | paragraph1.title_style.foreground == paragraph2.title_style.foreground && 157 | paragraph1.title_style.background == paragraph2.title_style.background 158 | ); 159 | }; 160 | 161 | bool operator==(const List& list1, const List& list2) { 162 | return ( 163 | list1.title == list2.title && 164 | list1.rows == list2.rows && 165 | list1.x == list2.x && 166 | list1.y == list2.y && 167 | list1.width == list2.width && 168 | list1.height == list2.height && 169 | list1.first_element == list2.first_element && 170 | list1.border == list2.border && 171 | list1.border_style.foreground == list2.border_style.foreground && 172 | list1.border_style.background == list2.border_style.background && 173 | list1.text_style.foreground == list2.text_style.foreground && 174 | list1.text_style.background == list2.text_style.background && 175 | list1.title_style.foreground == list2.title_style.foreground && 176 | list1.title_style.background == list2.title_style.background 177 | ); 178 | } 179 | 180 | bool operator==(const BarChart& bar_chart1, const BarChart& bar_chart2) { 181 | return ( 182 | bar_chart1.title == bar_chart2.title && 183 | bar_chart1.data == bar_chart2.data && 184 | bar_chart1.labels == bar_chart2.labels && 185 | bar_chart1.bar_width == bar_chart2.bar_width && 186 | bar_chart1.bar_color == bar_chart2.bar_color && 187 | bar_chart1.x == bar_chart2.x && 188 | bar_chart1.y == bar_chart2.y && 189 | bar_chart1.width == bar_chart2.width && 190 | bar_chart1.height == bar_chart2.height && 191 | bar_chart1.border == bar_chart2.border && 192 | bar_chart1.label_style.foreground == bar_chart2.label_style.foreground && 193 | bar_chart1.number_style.background == bar_chart2.number_style.background && 194 | bar_chart1.text_style.foreground == bar_chart2.text_style.foreground && 195 | bar_chart1.text_style.background == bar_chart2.text_style.background && 196 | bar_chart1.border_style.foreground == bar_chart2.border_style.foreground && 197 | bar_chart1.border_style.background == bar_chart2.border_style.background && 198 | bar_chart1.text_style.foreground == bar_chart2.text_style.foreground && 199 | bar_chart1.text_style.background == bar_chart2.text_style.background && 200 | bar_chart1.title_style.foreground == bar_chart2.title_style.foreground && 201 | bar_chart1.title_style.background == bar_chart2.title_style.background 202 | ); 203 | } 204 | 205 | bool operator==(const Gauge& gauge1, const Gauge& gauge2) { 206 | return ( 207 | gauge1.title == gauge2.title && 208 | gauge1.bar_color == gauge2.bar_color && 209 | gauge1.x == gauge2.x && 210 | gauge1.y == gauge2.y && 211 | gauge1.width == gauge2.width && 212 | gauge1.height == gauge2.height && 213 | gauge1.border == gauge2.border && 214 | gauge1.percent == gauge2.percent && 215 | gauge1.label == gauge2.label && 216 | gauge1.label_style.foreground == gauge2.label_style.foreground && 217 | gauge1.text_style.foreground == gauge2.text_style.foreground && 218 | gauge1.text_style.background == gauge2.text_style.background && 219 | gauge1.border_style.foreground == gauge2.border_style.foreground && 220 | gauge1.border_style.background == gauge2.border_style.background && 221 | gauge1.text_style.foreground == gauge2.text_style.foreground && 222 | gauge1.text_style.background == gauge2.text_style.background && 223 | gauge1.title_style.foreground == gauge2.title_style.foreground && 224 | gauge1.title_style.background == gauge2.title_style.background 225 | ); 226 | } 227 | 228 | // Exception handling 229 | class TUIException : public std::runtime_error { 230 | public: 231 | template 232 | TUIException(Args... args) : std::runtime_error(args...){} 233 | }; 234 | 235 | class Window { 236 | public: 237 | // Updates width, height, rows, and columns values 238 | void update_dimensions() { 239 | window_width = width(); 240 | window_height = height(); 241 | rows_ = rows(); 242 | columns_ = columns(); 243 | } 244 | 245 | // Draw border with given widget dimensions 246 | template 247 | void draw_border(Widget widget) { 248 | short border_color = get_color( 249 | widget.border_style.foreground, 250 | widget.border_style.background 251 | ); 252 | for(int i = widget.y; i < widget.y + widget.height; i++) { 253 | for(int j = widget.x; j < widget.x + widget.width; j++) { 254 | if( 255 | (i == widget.y && j == widget.x) || 256 | (i == widget.y && j == (widget.x + widget.width) - 1) || 257 | (i == (widget.y + widget.height) - 1 && j == widget.x) || 258 | (i == (widget.y + widget.height) - 1 && j == (widget.x + widget.width) - 1)) { 259 | draw_char(j, i, '+', border_color); 260 | } else if(i == widget.y || i == (widget.y + widget.height) - 1) { 261 | draw_char(j, i, '-', border_color); 262 | } else if(j == widget.x || j == (widget.x + widget.width) - 1) { 263 | draw_char(j, i, '|', border_color); 264 | } 265 | } 266 | } 267 | } 268 | 269 | // Draw title 270 | template 271 | void draw_title(Widget widget) { 272 | short title_color = get_color( 273 | widget.title_style.foreground, 274 | widget.title_style.background 275 | ); 276 | for(int i = widget.x + 2; i < widget.x + std::min(widget.width, (int)(widget.title.length()) + 2); i++) { 277 | draw_char(i, widget.y, widget.title[i - (widget.x + 2)], title_color); 278 | } 279 | } 280 | 281 | // Add a widget to the window 282 | // using recursive template function 283 | template 284 | inline void add(Widget first, Rest... rest) { 285 | add(first); 286 | add(rest...); 287 | } 288 | 289 | #ifdef IS_WIN 290 | Window(int window_width_ = 0, int window_height_ = 0) : window_width((LONG)window_width_), window_height((LONG)window_height_) { 291 | // Resize console to window_width and window_height 292 | console = GetConsoleWindow(); 293 | // Get default dimensions 294 | default_width = width(); 295 | default_height = height(); 296 | // Check if no width and height was given 297 | if(window_width_ <= 0 || window_height_ < 0) { 298 | // Window width or height given is 0 or less 299 | // Use default dimensions 300 | window_width = default_width; 301 | window_height = default_height; 302 | } 303 | RECT r; 304 | GetWindowRect(console, &r); 305 | MoveWindow(console, r.left, r.top, window_width, window_height, TRUE); 306 | // Tidy up the window 307 | hide_cursor(); 308 | remove_scrollbar(); 309 | update_dimensions(); 310 | // Allocate memory for content 311 | content = new CHAR_INFO[columns_ * rows_]; 312 | memset(content, 0, sizeof(CHAR_INFO) * rows_ * columns_); 313 | } 314 | 315 | // Close the tui and revert to default settings 316 | void close() { 317 | clear(); 318 | // Render the cleared content 319 | render(); 320 | // Show the cursor 321 | show_cursor(); 322 | RECT r; 323 | GetWindowRect(console, &r); 324 | MoveWindow(console, r.left, r.top, default_width, default_height, TRUE); 325 | } 326 | 327 | // Remove scrollbar from console 328 | void remove_scrollbar() { 329 | GetConsoleScreenBufferInfo(handle, &csbi); 330 | COORD new_size = { 331 | (short)(csbi.srWindow.Right - csbi.srWindow.Left + 1), 332 | (short)(csbi.srWindow.Bottom - csbi.srWindow.Top + 1) 333 | }; 334 | SetConsoleScreenBufferSize(handle, new_size); 335 | } 336 | 337 | // Clear content 338 | inline void clear() { 339 | // Set content to ' ' 340 | memset(content, 0, sizeof(CHAR_INFO) * rows_ * columns_); 341 | } 342 | 343 | // Hide cursor from console 344 | void hide_cursor() { 345 | CONSOLE_CURSOR_INFO info; 346 | info.bVisible = FALSE; 347 | info.dwSize = 100; 348 | SetConsoleCursorInfo(handle, &info); 349 | } 350 | 351 | // Show cursor from console 352 | void show_cursor() { 353 | CONSOLE_CURSOR_INFO info; 354 | info.bVisible = TRUE; 355 | SetConsoleCursorInfo(handle, &info); 356 | } 357 | 358 | // Return width of window 359 | LONG width() { 360 | RECT r; 361 | GetWindowRect(console, &r); 362 | return r.right - r.left; 363 | } 364 | 365 | // Return height of window 366 | LONG height() { 367 | RECT r; 368 | GetWindowRect(console, &r); 369 | return r.bottom - r.top; 370 | } 371 | 372 | // Return number of columns 373 | short columns() { 374 | GetConsoleScreenBufferInfo(handle, &csbi); 375 | return csbi.srWindow.Right - csbi.srWindow.Left; 376 | } 377 | 378 | // Return number of rows 379 | short rows() { 380 | GetConsoleScreenBufferInfo(handle, &csbi); 381 | return csbi.srWindow.Bottom - csbi.srWindow.Top; 382 | } 383 | 384 | // Set window title 385 | void set_title(std::string str) { 386 | TCHAR new_title[MAX_PATH]; 387 | new_title[str.size()] = 0; 388 | std::copy(str.begin(), str.end(), new_title); 389 | SetConsoleTitle(new_title); 390 | } 391 | 392 | // Set character in content 393 | void draw_char(int x, int y, char c, short color = 0x000F) { 394 | if(x >= 0 && x < columns_ && y >= 0 && y < rows_) { 395 | content[y * columns_ + x].Char.AsciiChar = c; 396 | content[y * columns_ + x].Attributes = color; 397 | } 398 | } 399 | 400 | // Render (print) content 401 | void render() { 402 | SMALL_RECT sr = {0, 0, (short)(columns_ - 1), (short)(rows_ - 1)}; 403 | hide_cursor(); 404 | remove_scrollbar(); 405 | WriteConsoleOutput(handle, content, {columns_, rows_}, {0, 0}, &sr); 406 | } 407 | 408 | // Poll for event 409 | bool poll_event(Event &event) { 410 | event = Event{}; 411 | for(uint8_t k = VK_LBUTTON; k <= VK_OEM_CLEAR; k++) { 412 | if(GetKeyState(k) & 0x8000) { 413 | if(k >= VK_LBUTTON && k <= VK_XBUTTON2 && k != VK_CANCEL) { 414 | event.type = MOUSEBUTTONDOWN; 415 | } else { 416 | event.type = KEYDOWN; 417 | } 418 | event.key = tolower(k); 419 | // Wait for key unpress 420 | while(GetKeyState(k) & 0x8000); 421 | } 422 | } 423 | if(event.type != UNDEFINED) { 424 | // Event has been registered 425 | return true; 426 | } else { 427 | return false; 428 | } 429 | } 430 | 431 | // Return the content of the buffer 432 | inline CHAR_INFO * get_content() { 433 | return content; 434 | } 435 | #elif defined(IS_POSIX) 436 | Window() { 437 | initscr(); 438 | raw(); 439 | keypad(stdscr, TRUE); 440 | noecho(); 441 | start_color(); 442 | hide_cursor(); 443 | update_dimensions(); 444 | timeout(1); 445 | } 446 | 447 | // Close the tui and revert to default settings 448 | void close() { 449 | show_cursor(); 450 | endwin(); 451 | } 452 | 453 | // Remove scrollbar from console (no op) 454 | inline void remove_scrollbar() { }; 455 | 456 | // Clear content (no op) 457 | inline void clear() { }; 458 | 459 | // Hide cursor from console 460 | inline void hide_cursor() { 461 | curs_set(0); 462 | } 463 | 464 | // Show cursor from console 465 | inline void show_cursor() { 466 | curs_set(1); 467 | } 468 | 469 | // Return width of window 470 | short width() { 471 | struct winsize size; 472 | ioctl(STDOUT_FILENO, TIOCGWINSZ, &size); 473 | return size.ws_xpixel; 474 | } 475 | 476 | // Return height of window 477 | short height() { 478 | struct winsize size; 479 | ioctl(STDOUT_FILENO, TIOCGWINSZ, &size); 480 | return size.ws_ypixel; 481 | } 482 | 483 | // Return number of columns 484 | short columns() { 485 | struct winsize size; 486 | ioctl(STDOUT_FILENO, TIOCGWINSZ, &size); 487 | return size.ws_col; 488 | } 489 | 490 | // Return number of rows 491 | short rows() { 492 | struct winsize size; 493 | ioctl(STDOUT_FILENO, TIOCGWINSZ, &size); 494 | return size.ws_row; 495 | } 496 | 497 | // Set window title (no op) 498 | inline void set_title(std::string str){ }; 499 | 500 | // Set character in tui 501 | inline void draw_char(int x, int y, char c, short color = 0x000F) { 502 | // TODO: Add color functionality 503 | mvaddch(y, x, c); 504 | } 505 | 506 | // Render tui 507 | inline void render() { 508 | refresh(); 509 | } 510 | 511 | // Poll for event 512 | bool poll_event(Event &event) { 513 | event.key = 0; 514 | event.type = UNDEFINED; 515 | int ch; 516 | ch = getch(); 517 | if(ch != EOF) { 518 | if(ch == KEY_MOUSE) { 519 | return false; 520 | } else { 521 | event.type = KEYDOWN; 522 | event.key = ch; 523 | } 524 | return true; 525 | } 526 | return false; 527 | } 528 | 529 | // Return the content of the buffer (no op) 530 | inline void get_content(){ }; 531 | #endif 532 | private: 533 | #ifdef IS_WIN 534 | HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); 535 | CONSOLE_SCREEN_BUFFER_INFO csbi; 536 | HWND console; 537 | LONG window_width; // Width of window 538 | LONG window_height; // Height of window 539 | short columns_; 540 | short rows_; 541 | CHAR_INFO *content; // Content to be rendered to buffer 542 | LONG default_width; // Width of the window before tui started 543 | LONG default_height; // Height of the window before tui started 544 | #elif defined(IS_POSIX) 545 | short window_width; 546 | short window_height; 547 | short columns_; 548 | short rows_; 549 | int current_pair = 1; 550 | #endif 551 | }; 552 | 553 | // Widget add to window method definitions 554 | template<> 555 | void Window::add(Paragraph paragraph) { 556 | if(paragraph.border == true) { 557 | draw_border(paragraph); 558 | } 559 | if(paragraph.title.empty() == false) { 560 | draw_title(paragraph); 561 | } 562 | // Get color 563 | short text_color = get_color( 564 | paragraph.text_style.foreground, 565 | paragraph.text_style.background 566 | ); 567 | // Draw text 568 | int maximum_characters = (paragraph.width - 2) * (paragraph.height - 2); 569 | for(int i = paragraph.y + 1; i < paragraph.y + paragraph.height - 1; i++) { 570 | int current_row = i - (paragraph.y + 1); 571 | for(int j = paragraph.x + 1; j < paragraph.x + paragraph.width - 1; j++) { 572 | int current_column = j - (paragraph.x + 1); 573 | int current_index = (current_row * (paragraph.width - 2)) + current_column; 574 | if(current_index < paragraph.text.length()) { 575 | draw_char(j, i, paragraph.text[current_index], text_color); 576 | } else { 577 | // Paragraph text is shorter than maximum characters 578 | break; 579 | } 580 | } 581 | } 582 | if(paragraph.text.length() > maximum_characters && paragraph.width - 2 >= 3) { 583 | // Draw ellipsis 584 | int last_row = (paragraph.y + paragraph.height) - 2; 585 | for(int i = 0; i < 3; i++) { 586 | int current_column = ((paragraph.x + paragraph.width) - 2) - i; 587 | draw_char(current_column, last_row, '.', text_color); 588 | } 589 | } 590 | } 591 | 592 | template<> 593 | void Window::add(List list) { 594 | if(list.border == true) { 595 | draw_border(list); 596 | } 597 | if(list.title.empty() == false) { 598 | draw_title(list); 599 | } 600 | // Get color 601 | short text_color = get_color( 602 | list.text_style.foreground, 603 | list.text_style.background 604 | ); 605 | for(int i = list.y + 1; i < list.y + list.height - 1; i++) { 606 | // Calculate current row with list's first element 607 | int current_row = list.first_element + (i - (list.y + 1)); 608 | for(int j = list.x + 1; j < list.x + list.width - 1; j++) { 609 | int current_column = j - (list.x + 1); 610 | // Naively assume character is empty 611 | draw_char(j, i, ' '); 612 | if(current_row < list.rows.size() && current_column < list.rows[current_row].length()) { 613 | if(list.rows[current_row].length() > list.width - 2 && j >= (list.x + list.width - 4)) { 614 | // Draw ellipsis 615 | if(list.width - 2 >= 3) { 616 | // Only draw ellipsis if inner width is at least 3 617 | draw_char(j, i, '.', text_color); 618 | } 619 | } else { 620 | draw_char(j, i, list.rows[current_row][current_column], text_color); 621 | } 622 | } 623 | } 624 | } 625 | } 626 | 627 | template<> 628 | void Window::add(BarChart bar_chart) { 629 | if(bar_chart.border == true) { 630 | draw_border(bar_chart); 631 | } 632 | if(bar_chart.title.empty() == false) { 633 | draw_title(bar_chart); 634 | } 635 | // Get colors 636 | short label_color = get_color( 637 | bar_chart.label_style.foreground, 638 | bar_chart.label_style.background 639 | ); 640 | short number_color = get_color( 641 | bar_chart.number_style.foreground, 642 | bar_chart.number_style.background 643 | ); 644 | // Get minimum and maximum values 645 | int minimum = INT_MAX; 646 | int maximum = INT_MIN; 647 | for(int i = 0; i < bar_chart.data.size(); i++) { 648 | if(bar_chart.data[i] < minimum) { 649 | minimum = bar_chart.data[i]; 650 | } 651 | if(bar_chart.data[i] > maximum) { 652 | maximum = bar_chart.data[i]; 653 | } 654 | } 655 | // Draw 656 | int current_bar = 0; 657 | int current_character = 0; 658 | for(int i = bar_chart.x + 1; i < bar_chart.x + bar_chart.width - 1; i += bar_chart.bar_width + 1) { 659 | if(current_bar < bar_chart.labels.size()) { 660 | for(int j = 0; j < bar_chart.bar_width; j++) { 661 | if(j < bar_chart.labels[current_bar].size() && i + j < bar_chart.x + bar_chart.width - 1) { 662 | draw_char( 663 | (i + j), 664 | (bar_chart.y + bar_chart.height - 2), 665 | bar_chart.labels[current_bar][j], 666 | label_color 667 | ); 668 | } 669 | } 670 | } 671 | if(current_bar < bar_chart.data.size()) { 672 | auto draw_numbers = [&]() { 673 | for(int j = 0; j < bar_chart.bar_width; j++) { 674 | std::string current_number = std::to_string(bar_chart.data[current_bar]); 675 | if(j < current_number.length() && i + j < bar_chart.x + bar_chart.width - 1) { 676 | draw_char( 677 | (i + j), 678 | (bar_chart.y + bar_chart.height - 3), 679 | current_number[j], 680 | number_color 681 | ); 682 | } 683 | } 684 | }; 685 | 686 | #ifdef IS_WIN 687 | // If defined IS_WIN, draw numbers before bars 688 | draw_numbers(); 689 | #endif 690 | float normalized = (floor)(bar_chart.data[current_bar]) / (maximum); 691 | int maximum_height = bar_chart.height - 3; 692 | int height = floor(normalized * maximum_height); 693 | for(int y = bar_chart.y + maximum_height - height + 2; y < bar_chart.y + maximum_height + 1; y++) { 694 | for(int x = 0; x < bar_chart.bar_width; x++) { 695 | #ifdef IS_WIN 696 | // Skip draw char to avoid overriding char 697 | content[y * columns_ + (i + x)].Attributes = get_color( 698 | number_color, 699 | bar_chart.bar_color 700 | ); 701 | #elif defined(IS_POSIX) 702 | draw_char( 703 | (i + x), 704 | y, 705 | '#', 706 | get_color(number_color, bar_chart.bar_color) 707 | ); 708 | #endif 709 | } 710 | } 711 | #ifdef IS_POSIX 712 | // If defined IS_POSIX, draw numbers after bars 713 | draw_numbers(); 714 | #endif 715 | } 716 | current_bar++; 717 | } 718 | } 719 | 720 | template<> 721 | void Window::add(Gauge gauge) { 722 | if(gauge.border == true) { 723 | draw_border(gauge); 724 | } 725 | if(gauge.title.empty() == false) { 726 | draw_title(gauge); 727 | } 728 | // Get color 729 | short label_color = get_color( 730 | gauge.label_style.foreground, 731 | gauge.label_style.background 732 | ); 733 | // Draw bar 734 | int bar_width = floor(((float)gauge.percent / 100) * (gauge.width - 2)); 735 | for(int i = gauge.y + 1; i < gauge.y + gauge.height - 1; i++) { 736 | for(int j = gauge.x + 1; j < gauge.x + gauge.width - 1; j++) { 737 | // Naively assume character is empty 738 | draw_char(j, i, ' '); 739 | if((j - (gauge.x + 1)) < bar_width) { 740 | #ifdef IS_WIN 741 | content[(i * columns_) + j].Attributes = get_color( 742 | gauge.label_style.foreground, 743 | gauge.bar_color 744 | ); 745 | #elif defined(IS_POSIX) 746 | draw_char( 747 | j, 748 | i, 749 | '#', 750 | get_color(gauge.label_style.foreground, gauge.bar_color) 751 | ); 752 | #endif 753 | } 754 | } 755 | } 756 | // Draw label 757 | int label_y = gauge.y + floor(gauge.height / 2); 758 | int current_char = 0; 759 | for(int i = gauge.x + 1; i < gauge.x + gauge.width - 1; i++) { 760 | if(current_char < gauge.label.length()) { 761 | short color; 762 | if(i - (gauge.x + 1) < bar_width) { 763 | // If label is to be drawn in a bar cell, 764 | // background color of bar should override 765 | // background color of label 766 | color = get_color(label_color, gauge.bar_color); 767 | } else { 768 | color = label_color; 769 | } 770 | draw_char( 771 | i, 772 | label_y, 773 | gauge.label[current_char], 774 | color 775 | ); 776 | } 777 | current_char++; 778 | } 779 | } 780 | 781 | // Widget set dimensions shortcut 782 | void Widget::set_dimensions(int x_, int y_, int width_, int height_) { 783 | x = x_; 784 | y = y_; 785 | width = width_; 786 | height = height_; 787 | } 788 | 789 | // Scroll up the list 790 | template<> 791 | void List::scroll_up(Window &window, int factor) { 792 | if(rows.size() > height - 2) { 793 | first_element = std::max(0, (int)(first_element - factor)); 794 | window.add(*this); 795 | } 796 | } 797 | 798 | // Scroll down the list 799 | template<> 800 | void List::scroll_down(Window &window, int factor) { 801 | if(rows.size() > height - 2) { 802 | first_element = std::min((int)(rows.size() - (height - 2)), (first_element + factor)); 803 | window.add(*this); 804 | } 805 | } 806 | }; 807 | #endif -------------------------------------------------------------------------------- /test/test_main.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN 2 | #include -------------------------------------------------------------------------------- /test/test_tui.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "../single_include/tui/tui.hpp" 3 | 4 | #ifdef _WIN32 5 | # define IS_WIN 6 | #include 7 | #else 8 | # define IS_POSIX 9 | #endif 10 | 11 | TEST_CASE("Widget Set Dimensions", "[widget_set_dimensions]") { 12 | tui::Paragraph paragraph; 13 | paragraph.set_dimensions(1, 2, 3, 4); 14 | REQUIRE(paragraph.x == 1); 15 | REQUIRE(paragraph.y == 2); 16 | REQUIRE(paragraph.width == 3); 17 | REQUIRE(paragraph.height == 4); 18 | 19 | tui::List list; 20 | list.set_dimensions(5, 6, 7, 8); 21 | REQUIRE(list.x == 5); 22 | REQUIRE(list.y == 6); 23 | REQUIRE(list.width == 7); 24 | REQUIRE(list.height == 8); 25 | } 26 | 27 | #ifdef IS_WIN 28 | #include 29 | 30 | std::string sample_string = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pharetra."; 31 | 32 | TEST_CASE("Widget Representation", "[widget_representation]") { 33 | 34 | // Testing for widget representation tests 35 | // the relative position of different characters 36 | // to account for varying terminal sizes. 37 | 38 | SECTION("Paragraph1", "[paragraph1]") { 39 | // Test large paragraph representation 40 | tui::Window window; 41 | tui::Paragraph paragraph; 42 | paragraph.title = sample_string; 43 | paragraph.text = sample_string; 44 | paragraph.x = 1; 45 | paragraph.y = 2; 46 | paragraph.width = 3; 47 | paragraph.height = 4; 48 | paragraph.border_style.foreground = tui::BLACK; 49 | paragraph.border_style.background = tui::MAGENTA; 50 | paragraph.text_style.foreground = tui::DARK_GREEN; 51 | paragraph.text_style.background = tui::YELLOW; 52 | paragraph.title_style.foreground = tui::DARK_RED; 53 | paragraph.title_style.background = tui::WHITE; 54 | window.add(paragraph); 55 | 56 | CHAR_INFO *content = window.get_content(); 57 | std::vector > requirements = { 58 | {"43", "208"}, 59 | {"45", "208"}, 60 | {"76", "244"}, 61 | {"124", "208"}, 62 | {"76", "226"}, 63 | {"124", "208"}, 64 | {"124", "208"}, 65 | {"111", "226"}, 66 | {"124", "208"}, 67 | {"43", "208"}, 68 | {"45", "208"}, 69 | {"43", "208"} 70 | }; 71 | int current_char = 0; 72 | for(int i = 0; i < window.rows() * window.columns(); i++) { 73 | if(std::to_string(content[i].Char.AsciiChar) != "0" && std::to_string(content[i].Attributes) != "0") { 74 | REQUIRE(std::to_string(content[i].Char.AsciiChar) == requirements[current_char].first); 75 | REQUIRE(std::to_string(content[i].Attributes) == requirements[current_char].second); 76 | current_char++; 77 | } 78 | } 79 | } 80 | SECTION("Paragraph2", "[paragraph2") { 81 | // Test large paragraph representation 82 | tui::Window window; 83 | tui::Paragraph paragraph; 84 | paragraph.title = sample_string; 85 | paragraph.text = sample_string; 86 | paragraph.x = 0; 87 | paragraph.y = 0; 88 | paragraph.width = 5; 89 | paragraph.height = 10; 90 | paragraph.border_style.foreground = tui::BLACK; 91 | paragraph.border_style.background = tui::MAGENTA; 92 | paragraph.text_style.foreground = tui::DARK_GREEN; 93 | paragraph.text_style.background = tui::YELLOW; 94 | paragraph.title_style.foreground = tui::DARK_RED; 95 | paragraph.title_style.background = tui::WHITE; 96 | window.add(paragraph); 97 | 98 | CHAR_INFO *content = window.get_content(); 99 | std::vector > requirements = { 100 | {"43", "208"}, 101 | {"45", "208"}, 102 | {"76", "244"}, 103 | {"111", "244"}, 104 | {"114", "244"}, 105 | {"124", "208"}, 106 | {"76", "226"}, 107 | {"111", "226"}, 108 | {"114", "226"}, 109 | {"124", "208"}, 110 | {"124", "208"}, 111 | {"101", "226"}, 112 | {"109", "226"}, 113 | {"32", "226"}, 114 | {"124", "208"}, 115 | {"124", "208"}, 116 | {"105", "226"}, 117 | {"112", "226"}, 118 | {"115", "226"}, 119 | {"124", "208"}, 120 | {"124", "208"}, 121 | {"117", "226"}, 122 | {"109", "226"}, 123 | {"32", "226"}, 124 | {"124", "208"}, 125 | {"124", "208"}, 126 | {"100", "226"}, 127 | {"111", "226"}, 128 | {"108", "226"}, 129 | {"124", "208"}, 130 | {"124", "208"}, 131 | {"111", "226"}, 132 | {"114", "226"}, 133 | {"32", "226"}, 134 | {"124", "208"}, 135 | {"124", "208"}, 136 | {"115", "226"}, 137 | {"105", "226"}, 138 | {"116", "226"}, 139 | {"124", "208"}, 140 | {"124", "208"}, 141 | {"46", "226"}, 142 | {"46", "226"}, 143 | {"46", "226"}, 144 | {"124", "208"}, 145 | {"43", "208"}, 146 | {"45", "208"}, 147 | {"45", "208"}, 148 | {"45", "208"}, 149 | {"43", "208"} 150 | }; 151 | int current_char = 0; 152 | for(int i = 0; i < window.rows() * window.columns(); i++) { 153 | if(std::to_string(content[i].Char.AsciiChar) != "0" && std::to_string(content[i].Attributes) != "0") { 154 | REQUIRE(std::to_string(content[i].Char.AsciiChar) == requirements[current_char].first); 155 | REQUIRE(std::to_string(content[i].Attributes) == requirements[current_char].second); 156 | current_char++; 157 | } 158 | } 159 | } 160 | SECTION("List1", "[list1]") { 161 | // Test list representation and scrolling functionality 162 | tui::Window window; 163 | tui::List list; 164 | list.title = sample_string; 165 | list.rows = { 166 | "[0] Hello World. Foo Bar", 167 | "[1] Foo Bar. Hello World.", 168 | "[2] Foo Bar", 169 | "[3] Hello World", 170 | "[4] Bar Foo", 171 | "[5] World Hello" 172 | }; 173 | list.x = 1; 174 | list.y = 2; 175 | list.width = 12; 176 | list.height = 4; 177 | list.border_style.foreground = tui::BLACK; 178 | list.border_style.background = tui::MAGENTA; 179 | list.text_style.foreground = tui::DARK_GREEN; 180 | list.text_style.background = tui::YELLOW; 181 | list.title_style.foreground = tui::DARK_RED; 182 | list.title_style.background = tui::WHITE; 183 | window.add(list); 184 | 185 | CHAR_INFO *content = window.get_content(); 186 | std::vector > requirements = { 187 | {"43", "208"}, 188 | {"45", "208"}, 189 | {"76", "244"}, 190 | {"111", "244"}, 191 | {"114", "244"}, 192 | {"101", "244"}, 193 | {"109", "244"}, 194 | {"32", "244"}, 195 | {"105", "244"}, 196 | {"112", "244"}, 197 | {"115", "244"}, 198 | {"117", "244"}, 199 | {"124", "208"}, 200 | {"91", "226"}, 201 | {"48", "226"}, 202 | {"93", "226"}, 203 | {"32", "226"}, 204 | {"72", "226"}, 205 | {"101", "226"}, 206 | {"108", "226"}, 207 | {"46", "226"}, 208 | {"46", "226"}, 209 | {"46", "226"}, 210 | {"124", "208"}, 211 | {"124", "208"}, 212 | {"91", "226"}, 213 | {"49", "226"}, 214 | {"93", "226"}, 215 | {"32", "226"}, 216 | {"70", "226"}, 217 | {"111", "226"}, 218 | {"111", "226"}, 219 | {"46", "226"}, 220 | {"46", "226"}, 221 | {"46", "226"}, 222 | {"124", "208"}, 223 | {"43", "208"}, 224 | {"45", "208"}, 225 | {"45", "208"}, 226 | {"45", "208"}, 227 | {"45", "208"}, 228 | {"45", "208"}, 229 | {"45", "208"}, 230 | {"45", "208"}, 231 | {"45", "208"}, 232 | {"45", "208"}, 233 | {"45", "208"}, 234 | {"43", "208"} 235 | }; 236 | int current_char = 0; 237 | for(int i = 0; i < window.rows() * window.columns(); i++) { 238 | if(std::to_string(content[i].Char.AsciiChar) != "0" && std::to_string(content[i].Attributes) != "0") { 239 | REQUIRE(std::to_string(content[i].Char.AsciiChar) == requirements[current_char].first); 240 | REQUIRE(std::to_string(content[i].Attributes) == requirements[current_char].second); 241 | current_char++; 242 | } 243 | } 244 | // Scroll down by factor of 100 and assert 245 | // factor should clamp in scroll down method with max 246 | list.scroll_down(window, 100); 247 | 248 | content = window.get_content(); 249 | requirements = { 250 | {"43", "208"}, 251 | {"45", "208"}, 252 | {"76", "244"}, 253 | {"111", "244"}, 254 | {"114", "244"}, 255 | {"101", "244"}, 256 | {"109", "244"}, 257 | {"32", "244"}, 258 | {"105", "244"}, 259 | {"112", "244"}, 260 | {"115", "244"}, 261 | {"117", "244"}, 262 | {"124", "208"}, 263 | {"91", "226"}, 264 | {"52", "226"}, 265 | {"93", "226"}, 266 | {"32", "226"}, 267 | {"66", "226"}, 268 | {"97", "226"}, 269 | {"114", "226"}, 270 | {"46", "226"}, 271 | {"46", "226"}, 272 | {"46", "226"}, 273 | {"124", "208"}, 274 | {"124", "208"}, 275 | {"91", "226"}, 276 | {"53", "226"}, 277 | {"93", "226"}, 278 | {"32", "226"}, 279 | {"87", "226"}, 280 | {"111", "226"}, 281 | {"114", "226"}, 282 | {"46", "226"}, 283 | {"46", "226"}, 284 | {"46", "226"}, 285 | {"124", "208"}, 286 | {"43", "208"}, 287 | {"45", "208"}, 288 | {"45", "208"}, 289 | {"45", "208"}, 290 | {"45", "208"}, 291 | {"45", "208"}, 292 | {"45", "208"}, 293 | {"45", "208"}, 294 | {"45", "208"}, 295 | {"45", "208"}, 296 | {"45", "208"}, 297 | {"43", "208"} 298 | }; 299 | current_char = 0; 300 | for(int i = 0; i < window.rows() * window.columns(); i++) { 301 | if(std::to_string(content[i].Char.AsciiChar) != "0" && std::to_string(content[i].Attributes) != "0") { 302 | REQUIRE(std::to_string(content[i].Char.AsciiChar) == requirements[current_char].first); 303 | REQUIRE(std::to_string(content[i].Attributes) == requirements[current_char].second); 304 | current_char++; 305 | } 306 | } 307 | // Scroll up by a factor of 2 and assert 308 | list.scroll_up(window, 2); 309 | 310 | content = window.get_content(); 311 | requirements = { 312 | {"43", "208"}, 313 | {"45", "208"}, 314 | {"76", "244"}, 315 | {"111", "244"}, 316 | {"114", "244"}, 317 | {"101", "244"}, 318 | {"109", "244"}, 319 | {"32", "244"}, 320 | {"105", "244"}, 321 | {"112", "244"}, 322 | {"115", "244"}, 323 | {"117", "244"}, 324 | {"124", "208"}, 325 | {"91", "226"}, 326 | {"50", "226"}, 327 | {"93", "226"}, 328 | {"32", "226"}, 329 | {"70", "226"}, 330 | {"111", "226"}, 331 | {"111", "226"}, 332 | {"46", "226"}, 333 | {"46", "226"}, 334 | {"46", "226"}, 335 | {"124", "208"}, 336 | {"124", "208"}, 337 | {"91", "226"}, 338 | {"51", "226"}, 339 | {"93", "226"}, 340 | {"32", "226"}, 341 | {"72", "226"}, 342 | {"101", "226"}, 343 | {"108", "226"}, 344 | {"46", "226"}, 345 | {"46", "226"}, 346 | {"46", "226"}, 347 | {"124", "208"}, 348 | {"43", "208"}, 349 | {"45", "208"}, 350 | {"45", "208"}, 351 | {"45", "208"}, 352 | {"45", "208"}, 353 | {"45", "208"}, 354 | {"45", "208"}, 355 | {"45", "208"}, 356 | {"45", "208"}, 357 | {"45", "208"}, 358 | {"45", "208"}, 359 | {"43", "208"} 360 | }; 361 | current_char = 0; 362 | for(int i = 0; i < window.rows() * window.columns(); i++) { 363 | if(std::to_string(content[i].Char.AsciiChar) != "0" && std::to_string(content[i].Attributes) != "0") { 364 | REQUIRE(std::to_string(content[i].Char.AsciiChar) == requirements[current_char].first); 365 | REQUIRE(std::to_string(content[i].Attributes) == requirements[current_char].second); 366 | current_char++; 367 | } 368 | } 369 | } 370 | } 371 | 372 | TEST_CASE("Widget Equality", "[widget_equality]") { 373 | // Test widget operator overloading for widget equality 374 | SECTION("Paragraph") { 375 | tui::Paragraph paragraphFoo; 376 | paragraphFoo.title = sample_string; 377 | paragraphFoo.text = (sample_string + sample_string); 378 | paragraphFoo.x = 1; 379 | paragraphFoo.y = 2; 380 | paragraphFoo.width = 3; 381 | paragraphFoo.height = 4; 382 | paragraphFoo.border = true; 383 | paragraphFoo.border_style.foreground = tui::BLACK; 384 | paragraphFoo.border_style.background = tui::MAGENTA; 385 | paragraphFoo.text_style.foreground = tui::DARK_GREEN; 386 | paragraphFoo.text_style.background = tui::YELLOW; 387 | paragraphFoo.title_style.foreground = tui::DARK_RED; 388 | paragraphFoo.title_style.background = tui::WHITE; 389 | 390 | tui::Paragraph paragraphBar; 391 | paragraphBar.title = sample_string; 392 | paragraphBar.text = (sample_string + sample_string); 393 | paragraphBar.x = 1; 394 | paragraphBar.y = 2; 395 | paragraphBar.width = 3; 396 | paragraphBar.height = 4; 397 | paragraphBar.border = true; 398 | paragraphBar.border_style.foreground = tui::BLACK; 399 | paragraphBar.border_style.background = tui::MAGENTA; 400 | paragraphBar.text_style.foreground = tui::DARK_GREEN; 401 | paragraphBar.text_style.background = tui::YELLOW; 402 | paragraphBar.title_style.foreground = tui::DARK_RED; 403 | paragraphBar.title_style.background = tui::WHITE; 404 | 405 | tui::Paragraph paragraphDifferent; 406 | paragraphDifferent.title = ""; 407 | paragraphDifferent.text = ""; 408 | paragraphDifferent.x = 100; 409 | paragraphDifferent.y = 100; 410 | paragraphDifferent.width = 100; 411 | paragraphDifferent.height = 100; 412 | paragraphDifferent.border = false; 413 | paragraphDifferent.border_style.foreground = tui::BLUE; 414 | paragraphDifferent.border_style.background = tui::BLUE; 415 | paragraphDifferent.text_style.foreground = tui::BLUE; 416 | paragraphDifferent.text_style.background = tui::BLUE; 417 | paragraphDifferent.title_style.foreground = tui::BLUE; 418 | paragraphDifferent.title_style.background = tui::BLUE; 419 | 420 | REQUIRE(paragraphFoo == paragraphBar); 421 | REQUIRE(!(paragraphFoo == paragraphDifferent)); 422 | } 423 | SECTION("List") { 424 | tui::List listFoo; 425 | listFoo.title = sample_string; 426 | listFoo.rows = { 427 | "[0] Hello World!", 428 | "[1] Foo Bar" 429 | }; 430 | listFoo.x = 1; 431 | listFoo.y = 2; 432 | listFoo.width = 3; 433 | listFoo.height = 4; 434 | listFoo.first_element = 0; 435 | listFoo.border = true; 436 | listFoo.border_style.foreground = tui::BLACK; 437 | listFoo.border_style.background = tui::MAGENTA; 438 | listFoo.text_style.foreground = tui::DARK_GREEN; 439 | listFoo.text_style.background = tui::YELLOW; 440 | listFoo.title_style.foreground = tui::DARK_RED; 441 | listFoo.title_style.background = tui::WHITE; 442 | 443 | tui::List listBar; 444 | listBar.title = sample_string; 445 | listBar.rows = { 446 | "[0] Hello World!", 447 | "[1] Foo Bar" 448 | }; 449 | listBar.x = 1; 450 | listBar.y = 2; 451 | listBar.width = 3; 452 | listBar.height = 4; 453 | listBar.first_element = 0; 454 | listBar.border = true; 455 | listBar.border_style.foreground = tui::BLACK; 456 | listBar.border_style.background = tui::MAGENTA; 457 | listBar.text_style.foreground = tui::DARK_GREEN; 458 | listBar.text_style.background = tui::YELLOW; 459 | listBar.title_style.foreground = tui::DARK_RED; 460 | listBar.title_style.background = tui::WHITE; 461 | 462 | tui::List listDifferent; 463 | listDifferent.title = ""; 464 | listDifferent.rows = {}; 465 | listDifferent.x = 100; 466 | listDifferent.y = 100; 467 | listDifferent.width = 100; 468 | listDifferent.height = 100; 469 | listDifferent.first_element = 100; 470 | listDifferent.border = false; 471 | listDifferent.border_style.foreground = tui::BLUE; 472 | listDifferent.border_style.background = tui::BLUE; 473 | listDifferent.text_style.foreground = tui::BLUE; 474 | listDifferent.text_style.background = tui::BLUE; 475 | listDifferent.title_style.foreground = tui::BLUE; 476 | listDifferent.title_style.background = tui::BLUE; 477 | 478 | REQUIRE(listFoo == listBar); 479 | REQUIRE(!(listFoo == listDifferent)); 480 | } 481 | } 482 | 483 | TEST_CASE("Color Handling", "[color_handling]") { 484 | // Test color constants and bitwise operations 485 | REQUIRE(tui::get_color(tui::BLACK, tui::WHITE) == 0x00F0); 486 | REQUIRE(tui::get_color(tui::DARK_BLUE, tui::YELLOW) == 0x00E1); 487 | REQUIRE(tui::get_color(tui::DARK_GREEN, tui::MAGENTA) == 0x00D2); 488 | REQUIRE(tui::get_color(tui::DARK_CYAN, tui::RED) == 0x00C3); 489 | REQUIRE(tui::get_color(tui::DARK_RED, tui::CYAN) == 0x00B4); 490 | REQUIRE(tui::get_color(tui::DARK_MAGENTA, tui::GREEN) == 0x00A5); 491 | REQUIRE(tui::get_color(tui::DARK_YELLOW, tui::BLUE) == 0x0096); 492 | REQUIRE(tui::get_color(tui::GRAY, tui::DARK_GRAY) == 0x0087); 493 | 494 | REQUIRE(tui::get_color(tui::WHITE, tui::BLACK) == 0x000F); 495 | REQUIRE(tui::get_color(tui::YELLOW, tui::DARK_BLUE) == 0x001E); 496 | REQUIRE(tui::get_color(tui::MAGENTA, tui::DARK_GREEN) == 0x002D); 497 | REQUIRE(tui::get_color(tui::RED, tui::DARK_CYAN) == 0x003C); 498 | REQUIRE(tui::get_color(tui::CYAN, tui::DARK_RED) == 0x004B); 499 | REQUIRE(tui::get_color(tui::GREEN, tui::DARK_MAGENTA) == 0x005A); 500 | REQUIRE(tui::get_color(tui::BLUE, tui::DARK_YELLOW) == 0x0069); 501 | REQUIRE(tui::get_color(tui::DARK_GRAY, tui::GRAY) == 0x0078); 502 | } 503 | #elif defined(IS_POSIX) 504 | 505 | #endif --------------------------------------------------------------------------------