From d9195c64a0558b318f2bb1456b7a01f19baacde4 Mon Sep 17 00:00:00 2001 From: xolatile Date: Sat, 11 Nov 2023 04:45:28 -0500 Subject: [PATCH] Explained functions in chapter 2... --- chapters/chapter_2.c | 157 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 118 insertions(+), 39 deletions(-) diff --git a/chapters/chapter_2.c b/chapters/chapter_2.c index 314d5a4..a58da08 100644 --- a/chapters/chapter_2.c +++ b/chapters/chapter_2.c @@ -164,9 +164,9 @@ int curses_active = 1; /* I need to quickly explain how I'm structuring this subprogram. You can think of functions and variables starting with 'curses_*' as tiny standalone library if it's easier. They -deal with terminal input and output. User only needs to call function 'curses_configure' only once, before doing any rendering, then make an "infinite" loop that'll stop when -external variable 'curses_active' is equal to zero. In that loop, user can call any 'curses_render_*' function, and at the end call function 'curses_synchronize' only once. So the -following program structure would look something like this: +deal with terminal input and output. User only needs to call function 'curses_configure' only once, and necessary amount of 'curses_bind' functions, before doing any rendering, +then make an "infinite" loop that'll stop when external variable 'curses_active' is equal to zero. In that loop, user can call any amount 'curses_render_*' functions, and at the +end call function 'curses_synchronize' again, only once. So the following program structure would look something like this: @C // ... @@ -183,6 +183,11 @@ int main (int argc, char * * argv) { return (0); } @ + +We don't need to use 'curses_initialize' and the begining of the program, or 'curses_deinitialize' at the end. Also, there are no functions like 'BeginDrawing' or 'EndDrawing', +with 'Draw*' functions strictly between those two, like you would see in Raylib. We won't cover SDL2 and Raylib in this book, since I think that anyone with minimal knowledge of +C can efficiently write good programs using those libraries, please don't misunderstand me, those two are good libraries. We'll simply eliminate that "begin" or "end" step in +our subprogram called 'curses'. */ static void curses_initialize (void) { // This function will be called when 'curses_configure' is called, automatically. @@ -199,10 +204,10 @@ static void curses_initialize (void) { curses_new_terminal = curses_old_terminal; // Here we set our raw terminal to be the same as the non-raw one. - curses_new_terminal.c_cc [VMIN] = (unsigned char) 0; + curses_new_terminal.c_cc [VMIN] = (unsigned char) 0; // Now it's time to modify it to be raw, this essentially means no-echo. curses_new_terminal.c_cc [VTIME] = (unsigned char) 1; - curses_new_terminal.c_iflag &= (unsigned int) ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); // Now it's time to modify it to be raw, this essentially means no-echo. + curses_new_terminal.c_iflag &= (unsigned int) ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); curses_new_terminal.c_oflag &= (unsigned int) ~(OPOST); curses_new_terminal.c_cflag |= (unsigned int) (CS8); curses_new_terminal.c_lflag &= (unsigned int) ~(ECHO | ICANON | IEXTEN | ISIG); @@ -222,16 +227,50 @@ static void curses_deinitialize (void) { "tcsetattr: Failed to set default terminal attributes."); } +/* +Next two functions are also internal, and we'll use them in our other 'curses_*' functions. The goal of extracting that piece of code into separate functions is to eliminate code +repetition. Copying and pasting the same piece of code everywhere has pros and cons. Pros are that they're very good indication what should be factored out. The best example that +comes from top of my head is 'echo' function from chapter zero. It'd be boring to use 'out' everywhere, since it also requires the 'size' argument, and I want to just output +string literals to terminal in most cases. But that doesn't mean that 'out' is useless, as you'll see, we'll use it in 'curses_synchronize' funcion. Cons are, if you really have +repeating code all over your program, if there's a bug in one of them, there's a bug in all of them. Also, if you'll use some extracted function (also refered to as refactored in +some cases) only once, it's not a bad thing. We've extracted some code into 'curses_initialize' function, and we call it only once, inside 'curses_configure' function, but the +intention behind what it does is clear. However, I still believe that procedural code, that's executed line by line, from top to bottom, is best. + +Now, we could also implement some error checking functions for 'curses_*' functions. Good idea, when something is wrong, like we want to render a character out of the screen, we +just print an error message to terminal, right? Well, no. Remember, we're using terminal as a framebuffer (about which we'll talk about a lot more in ray tracing and rasterization +chapters), so if we just print message there, it'll corrupt our framebuffer. So, we could just write them to some log file. How about binding them, whenever something is wrong, +we don't just abort the program or stop rendering, but continue running it, while also writing error messages to that same file. Then, when we exit the program, it'll print all +error messages (if any) normally. Here's how it would look like: + +@C +// Enumeration for log type. +enum { + LOG_FAILURE, LOG_WARNING, LOG_SUCCESS, LOG_COUNT +}; + +static void log_in (int type, int condition, char * message); +static void log_out (char * name); + +// Usage example: +log_in (LOG_FAILURE, condition != 0, "function_name: The kind of error that occured or a general notification."); +log_out ("file_name.log"); +@ + +To be honest, I consider that a bad idea, because if we render something that's not supposed to be rendered (we pass wrong arguments to any of the 'curses_render_*' functions), +it'll be obvious when we look at it. So, I wrote commented-out examples below of how we'd do it, but it won't be compiled into the final object file. If we pass a wrong value to +those functions, they'll be fixed (modified), and give the wrong results, which will be obvious when we look at the screen. Nothing will crash or burn. +*/ + static char * curses_screen_offset (int x, int y) { // log_in (LOG_FAILURE, x <= -1, "curses_screen_offset: X position is below the lower bound."); // log_in (LOG_FAILURE, y <= -1, "curses_screen_offset: Y position is below the lower bound."); // log_in (LOG_FAILURE, x >= curses_screen_width, "curses_screen_offset: X position is above the upper bound."); // log_in (LOG_FAILURE, y >= curses_screen_height, "curses_screen_offset: Y position is above the upper bound."); - limit (& x, 0, curses_screen_width - 1); - limit (& y, 0, curses_screen_height - 1); + limit (& x, 0, curses_screen_width - 1); // We're limiting the values of X and Y coordinates to screen dimensions. + limit (& y, 0, curses_screen_height - 1); // Function 'limit' uses inclusive values for minimum and maximum. - return (& curses_screen [CURSES_REVERT + CURSES_FORMAT * (y * curses_screen_width + x)]); + return (& curses_screen [CURSES_REVERT + CURSES_FORMAT * (y * curses_screen_width + x)]); // And returning the offset of the screen buffer. } static char * curses_format_character (char character, int colour, int effect) { @@ -239,15 +278,21 @@ static char * curses_format_character (char character, int colour, int effect) { // log_in (LOG_FAILURE, colour >= COLOUR_COUNT, "curses_format_character: Colour is invalid enumeration value."); // log_in (LOG_FAILURE, effect >= EFFECT_COUNT, "curses_format_character: Effect is invalid enumeration value."); - if (character_is_invisible (character) != 0) { + if (character_is_invisible (character) != 0) { // Since we don't want to render invisible ASCII characters, we'll just change 'character' to space. character = ' '; } - colour %= COLOUR_COUNT; + colour %= COLOUR_COUNT; // Instead of breaking and burning everything, we'll just "round" it down to one of the allowed values. effect %= EFFECT_COUNT; - switch (effect) { - case EFFECT_NORMAL: effect = 0; break; + switch (effect) { // Effects aren't enumerated nicely as colours, so here we'll use switch statement instead of nested if-else statements. + case EFFECT_NORMAL: effect = 0; break; // We could break these into any combination of new lines, we'll just show one example below of how else you can do it. + // case EFFECT_NORMAL: + // effect = 0; + // break; + // case EFFECT_NORMAL: { + // effect = 0; + // } break; case EFFECT_BOLD: effect = 1; break; case EFFECT_ITALIC: effect = 3; break; case EFFECT_UNDERLINE: effect = 4; break; @@ -256,48 +301,46 @@ static char * curses_format_character (char character, int colour, int effect) { default: effect = 0; break; } - curses_format [2] = (char) effect + '0'; + curses_format [2] = (char) effect + '0'; // Now, here comes ASCII escape sequence magic, this will format the character to have colour and effect. curses_format [5] = (char) colour + '0'; curses_format [7] = character; - // log_out ("curses.log"); - - return (curses_format); + return (curses_format); // And we return the value (pointer to character) of formatted internal variables. } static void curses_idle (void) { return; } // If you have a lot of short functions that are intended to be in array of function pointers, you can align them like this. -static void curses_exit (void) { curses_active = 0; } +static void curses_exit (void) { curses_active = 0; } // And this is our main function for quitting main loop in curses. /* External function definitions, those found in "chapter_2.h" header file. */ void curses_configure (void) { - atexit (curses_deinitialize); + atexit (curses_deinitialize); // Deinitialization is automatically on exit. - curses_initialize (); + curses_initialize (); // Initializing curses, yaay. - curses_screen = allocate (CURSES_REVERT + CURSES_FORMAT * curses_screen_width * curses_screen_height + CURSES_CURSOR + 1); + curses_screen = allocate (CURSES_REVERT + CURSES_FORMAT * curses_screen_width * curses_screen_height + CURSES_CURSOR + 1); // We're requesting new memory for framebuffer. - curses_bind (SIGNAL_ESCAPE, curses_exit); + curses_bind (SIGNAL_ESCAPE, curses_exit); // Binding universal exit key (signal). terminal_clear (); - string_copy (& curses_screen [0], "\033[H"); + string_copy (& curses_screen [0], "\033[H"); // ASCII black magic to always clear screen. } void curses_synchronize (void) { int signal; - curses_signal = curses_character = signal = 0; + curses_signal = curses_character = signal = 0; // Reassigning signals to 0. - out (curses_screen, CURSES_REVERT + CURSES_FORMAT * curses_screen_width * curses_screen_height + CURSES_CURSOR); + out (curses_screen, CURSES_REVERT + CURSES_FORMAT * curses_screen_width * curses_screen_height + CURSES_CURSOR); // We output the entire framebuffer to terminal (present). - in (& signal, 4); + in (& signal, (int) sizeof (signal)); // We're now checking for user input to modify it. - curses_character = signal; + curses_character = signal; // We need literal value of 'signal' for text input. - if (signal == '\033') { + if (signal == '\033') { // And then we modify the actual 'curses_signal'. curses_signal |= SIGNAL_ESCAPE; } else if (character_is_digit (signal) != 0) { curses_signal |= SIGNAL_0 + (int) (signal - '0'); @@ -310,13 +353,17 @@ void curses_synchronize (void) { curses_signal = SIGNAL_NONE; } - for (signal = 0; signal != curses_action_count; ++signal) { - if (curses_signal == curses_activator [signal]) { - curses_action [signal] (); + for (signal = 0; signal != curses_action_count; ++signal) { // Now, it's time to loop over bound actions. + if (curses_signal == curses_activator [signal]) { // If we have a bound signal, then: + curses_action [signal] (); // We execute corresponding action (function). } } } +/* +It's still to early to talk about signal binding and unbinding, so I'll skip over them as they are more complex. +*/ + void curses_bind (int signal, void (* action) (void)) { ++curses_action_count; @@ -336,36 +383,68 @@ void curses_unbind (int signal) { --curses_action_count; } +/* +Finally, we're getting to rendering, something that we can actually see on our screen. There's ASCII escape sequences involved into this, so lets explain them briefly. We +mentioned in "chapter_2.h" header file about how some of them work. Now, we're extending them, with minimal amount of error checking in order to keep things more clear. Worth +noting is that not all terminals support all of ASCII escape sequences, but most of them support at least a subset of them. There are many terminals such as ones provided by +desktop environment (xfce4-terminal, etc.), and independent ones (st, sakura, xterm, etc.), use whatever, it'll (in 99% of cases) work as intended. + +If you want to know what rendering truly is, it's just copying certain value to some offset. Nothing more... +*/ + void curses_render_cursor (int x, int y) { + // We're expecting that our terminal dimensions (width and height in unit of 'characters') is always less 1000. If it's not, I really don't care. + // Also, there's the application of "limited" string functions, since we don't want to copy until null character ('\0') is reached. + // Lastly, if you remember that 'curses_cursor' is string "\033[---;---H", we're just writing two numbers to "---" parts, from 0 to 999 inclusively. + // We're adding one because ASCII uses 1 ... 1000, and we use 0 ... 999, so it doesn't causes hidden bugs, and realign them to be prefixed with '0' characters. + // If we have set of values (x = 31, y = 37), then the copied string would look like "\033[031;037H". It's not complex as it may sound. + // And remember our ASCII table, thing scary thing (\033) is just octal value for number 27, which is CHARACTER_ESCAPE, hence the escape sequences start with it. x %= 1000; y %= 1000; string_copy_limit (curses_cursor + 2, string_realign (number_to_string (y + 1), 3, '0'), 3); string_copy_limit (curses_cursor + 6, string_realign (number_to_string (x + 1), 3, '0'), 3); - string_copy_limit (& curses_screen [CURSES_REVERT + CURSES_FORMAT * curses_screen_width * curses_screen_height], curses_cursor, CURSES_CURSOR); + string_copy_limit (& curses_screen [CURSES_REVERT + CURSES_FORMAT * curses_screen_width * curses_screen_height], curses_cursor, CURSES_CURSOR); // Actual rendering. } void curses_render_character (char character, int colour, int effect, int x, int y) { - if ((x >= curses_screen_width) || (y >= curses_screen_height)) { + if ((x < 0) || (x > curses_screen_width - 1) || (y < 0) || (y > curses_screen_height - 1)) { // If any of these are true, we don't render. return; } + // Again, lets show some code formatting examples: + // if ((x < 0) + // || (y < 0) + // || (x > curses_screen_width - 1) + // || (y > curses_screen_height - 1)) { + // return; + // } + // Or, if you don't want to subtract one from values there: + // if ((x < 0) || + // (y < 0) || + // (x >= curses_screen_width) || + // (y >= curses_screen_height)) { + // return; + // } + // Or if you really hate adding 2 more lines of code and curly braces: + // if ((x < 0) || (x > curses_screen_width - 1) || (y < 0) || (y > curses_screen_height - 1)) return; - string_copy_limit (curses_screen_offset (x, y), curses_format_character (character, colour, effect), CURSES_FORMAT); + string_copy_limit (curses_screen_offset (x, y), curses_format_character (character, colour, effect), CURSES_FORMAT); // Again, actual rendering, copying a value to offset. } void curses_render_background (char character, int colour, int effect) { - int x, y; + int x, y; // We declare local variables like this, I usually don't initialize them right away. - for (y = 0; y != curses_screen_height; ++y) { - for (x = 0; x != curses_screen_width; ++x) { - curses_render_character (character, colour, effect, x, y); + for (y = 0; y != curses_screen_height; ++y) { // Iterating through rows (by height) of our framebuffer ('curses_screen'). + for (x = 0; x != curses_screen_width; ++x) { // Iterating through columns (by width) of our framebuffer. + curses_render_character (character, colour, effect, x, y); // Now, we can use function 'curses_render_character' to simplify our life... } } } /* -We've mentioned before, in chapter zero, that you can implement 'string_*' functions by 'string_*_limit', and here's an example of that. +We've mentioned before, in chapter zero, that you can implement 'string_*' functions by 'string_*_limit', and here's an example of that. Functions below are quite self +explanatory, so you can do a "homework" of reading them and trying to understand what they'll do. Since I use very verbose naming style, I hope that won't be a problem... */ int curses_render_string (char * string, int colour, int effect, int x, int y) { @@ -394,7 +473,7 @@ int curses_render_string_limit (char * string, int limit, int colour, int effect return (limit); } -int curses_render_number_limit (int number, int limit, int colour, int effect, int x, int y) { +int curses_render_number_limit (int number, int limit, int colour, int effect, int x, int y) { // TO DO (: (void) number; (void) limit; (void) colour;