-
Notifications
You must be signed in to change notification settings - Fork 27
Tutorial: 'Console Screen' Cursor
- Introduction | | Starting Point | | What Is It | | Visual Representation | | Moving | | Showing And Hiding | | Double Height Printing | | Scrolling | | Double Height Scrolling | | Automatic Scrolling | | Exercise | | Conclusion | | The Final Program
We're going to need a basic program to use as a starting point. The following program is our starting point and has a Console Screen already prepared:
#include <SFML/Graphics.hpp>
#include <SelbaWard/ConsoleScreen.hpp>
int main()
{
sf::Texture csTexture;
if (!csTexture.loadFromFile("PATH/Windows Console ASCII.png"))
return EXIT_FAILURE;
sf::RenderWindow window(sf::VideoMode(800, 600), "Console Screen tutorial - Cursor");
Cs cs;
cs.setTexture(csTexture);
cs.setTextureTileSize({ 8, 12 });
cs.setNumberOfTextureTilesPerRow(16);
cs.setMode({ 40, 22 });
cs.setSize(cs.getPerfectSize());
cs.setScale({ 2.f, 2.f });
cs.setOrigin(cs.getSize() / 2.f);
cs.setPosition(sf::Vector2f(window.getSize() / 2u));
while (window.isOpen())
{
sf::Event event;
while (window.pollEvent(event))
{
switch (event.type)
{
case sf::Event::Closed:
window.close();
break;
}
}
window.clear(sf::Color(128, 128, 128));
window.draw(cs);
window.display();
}
return EXIT_SUCCESS;
}
This starting point is based on the Basics tutorial but with all the output to the Console Screen removed so that this is just an empty Console Screen, ready to use. Note that the window's title has been altered here to match the current tutorial.
If you run the 'Starting Point' code above, you'll see a black Console Screen and a small, single white horizontal line near the top-left corner of that screen; this is the cursor. It represents a 'current location' within the screen and is where any following 'printing' will begin.
The cursor is just a normal character from the texture like any other. However, the cursor is drawn instead of the character that would normally be shown at that location. The cursor, though, is temporary so if it is moved, the character 'underneath' would be shown again.
The way the cursor is shown can be customised.
First, the character that is used for the cursor can be specified.
To change the character that is used for the cursor so that it appears as a star shape (the last character on the top row of the texture), add:
cs.setCursor(15);
We can also change how the colour of the cursor is decided.
First, we need to tell the cursor to use its own colour:
cs.setUseCursorColor(true);
By default, the cursor is drawn in the colour of whichever foreground colour is set for that particular cell at that time. When we tell it to use its own colour, it becomes independant of the cell's colour and the colour becomes temporary in the same way that the character is.
The cursor's default colour is "Contrast", it will show as either black or white dependant on the cell's background colour.
We can change the colour that it uses to any ColorCommand or, indeed, any Color. To set it to a bright magenta (using the default 16-color palette):
cs.setCursorColor(Cs::Color(11));
One thing to note is that setting the cursor colour doesn't automatically update the screen so we need to do that manually:
cs.update();
This refreshes the screen so that its visual representation is up-to-date from the information about its cells etc..
Telling the cursor to use its own colour does update the screen automatically so if the colour is set before telling it to use its own, it will be updated automatically.
Another thing that can change the way the cursor looks (or affects its cell) is inverting the cursor:
cs.setInvertCursor(true);
This swaps the foreground and background colours for the cell that contains the cursor while it is in that cell. The colours swapped are the colours that would have been used to display it without inverting. This means that if it's using its own colour, the background will now be the cursor colour and the cursor will be the colour of the cell's background. However, if it's not using its own colour, the cell's background will be displayed as the cell's foreground colour and the cursor will be displayed as the cell's background colour.
The cursor can be set to any valid cell location within the screen.
If you have been following all of the Console Screen tutorials so far, you will have already moved the cursor using a "Cursor Command" - "Newline". As you have seen, that command moves the cursor to the beginning (left-hand side) of the row below its current row. This command is very useful when outputting text or multiple lines at once. However, it's less useful when you need to output strings at more specific locations around the screen i.e. not at the beginning of the next line.
To move the cursor directly to the eighth column on the fourth row, we feed a Cs::Location into the stream:
cs << Cs::Location({ 7u, 3u });
A Cs::Location is a two-dimensional (unsigned) integer co-ordinate and represents a cell location within the screen. The top-left cell is (0, 0)
If you run the program now, you will see that the magenta-coloured star-shaped cursor will have moved away from the corner. It is now ready to print at that location.
Let's print something there:
cs << "We're over here!";
Notice that the cursor is automatically moved when printing to the cell directly after the last printed character so it is now directly after the exclamation mark of that string. If you printed anything else, it would continue after the previous print.
It turns out that we actually want to change how that string ends - instead of an exclamation mark, we want three full-stops (periods). Since the cursor is immediately after the exclamation mark, we simply need to move it left once and the print over it with the full-stops.
To do this, we can use the left cursor command:
cs << Cs::CursorCommand::Left;
and then print the replacement string:
cs << "...";
This, of course, can be chained so we can replace the above two lines with:
cs << Cs::CursorCommand::Left << "...";
Suppose we now want to replace the h in "here" with an apostrophy. First, we need to move the cursor left by 7 cells. We can do that with a movement shortcut:
cs << Cs::Left(7u);
There are shortcuts for moving the cursor around relative to its current position. These are "Cs::Up", "Cs::Down", "Cs::Left" and "Cs::Down". These take a parameter representing how many cells to move so they can move multiple cells at once.
If you were to run the program at this point, you would see that the cursor has replaced the h character in the word "here". It's very important to note that the h character is still in that cell but it is not being displayed because the cursor is there. If the cursor was moved, it would be shown again.
So, to replace the h, we print the apostrophy:
cs << "'";
If we run the program now, we would see that the apostrophy has replaced the h but the cursor is being displayed instead of the e.
We need to move the cursor to see the entire string.
We will use the Home command:
cs << Cs::CursorCommand::Home;
This places the cursor back into the top-left corner of the screen as it was at the very beginning. It now looks like the printing was done directly without using the cursor!
In fact, it is possible to print to the screen at other locations without even moving the cursor. This is called Direct Printing and will be addressed in a future tutorial. For more information, see the Console Screen documentation
We can use the Tab command to move to the next 'tab-stop'. We are going to use this to align columns into a simple table.
However, first, we should move the cursor to below the string we outputted earlier:
cs << Cs::Down(5u);
This is a shortcut command to move the cursor downwards by the amount specified. The base command is Cs::CursorCommand::Down.
Then, we output a table of arbitrary numbers inside a double-lined frame:
cs << Cs::CursorCommand::Tab << Cs::Char('\xC9') << std::string(7u, '\xCD') << Cs::Char('\xCB') << std::string(7u, '\xCD') << Cs::Char('\xCB') << std::string(7u, '\xCD') << Cs::Char('\xBB') << Cs::CursorCommand::Newline;
cs << Cs::CursorCommand::Tab << "\xBA Col A" << Cs::CursorCommand::Tab << "\xBA Col B" << Cs::CursorCommand::Tab << "\xBA Col C" << Cs::CursorCommand::Tab << Cs::Char('\xBA') << Cs::CursorCommand::Newline;
cs << Cs::CursorCommand::Tab << Cs::Char('\xCC') << std::string(7u, '\xCD') << Cs::Char('\xCE') << std::string(7u, '\xCD') << Cs::Char('\xCE') << std::string(7u, '\xCD') << Cs::Char('\xB9') << Cs::CursorCommand::Newline;
for (std::size_t row{ 0u }; row < 5; ++row)
{
cs << Cs::CursorCommand::Tab;
for (std::size_t column{ 0u }; column < 3; ++column)
{
cs << "\xBA " << Cs::Number(row * row * row * row + 8 - column * column * column) << Cs::CursorCommand::Tab;
}
cs << Cs::Char('\xBA') << Cs::CursorCommand::Newline;
}
cs << Cs::CursorCommand::Tab << Cs::Char('\xC8') << std::string(7u, '\xCD') << Cs::Char('\xCA') << std::string(7u, '\xCD') << Cs::Char('\xCA') << std::string(7u, '\xCD') << Cs::Char('\xBC') << Cs::CursorCommand::Newline;
This uses the frame graphic characters of the texture. The first three rows and the bottom row are drawn separately from the data rows - the top and bottom rows have a corner character on each end, a 'junction' character every 8 characters (the tab of our table) and 7 horizontal characters to fill in between those. The third row is similar but has 'junction' characters instead of the corner characters. The second row is similar to a data row (see below) but drawn as one - vertical frame characters and a header for each column. The data rows each have 7 spaces per column with a (vertical) frame character inbetween. We output one vertical frame character with a space, followed by our data. Then, we tab across to the next column so we can repeat the process. A final frame character is added to finish the frame. In addition to the previous parts, each row also has a tab command at the beginning of that row. That pushes the table to right by the size of the tab; in this case, the table is pretty much centre-aligned horizontally. Note that the 'data' value (
row * row * row * row + 8 - column * column * column
) is an arbitrary number based on the row and column value. It is just an example for this table.
If you were to run this program now, you'd see the table with its frame but you would notice that a couple of the rows do not align with the others. This is because the default tab (tab-stop) is 4, not 8. We based our table on a tab of 8 so we need to change it:
cs.setCursorTab(8u);
This changes the tab-stop for future tabs so does not affect previous tab movements made. Because of this, it is important that the tab is set before we display the table. Place this line before the first table frame output (just after the Cs::Down(5u) command).
The table should then be arranged similarly to this:
Col A | Col B | Col C |
---|---|---|
8 | 7 | 0 |
9 | 8 | 1 |
24 | 23 | 16 |
89 | 88 | 81 |
264 | 263 | 256 |
Try running it at this point; the table should be correct. Feel free to take a break from this tutorial for a while to customise this table how you would like - you can change the content (headers and data) or the frame characters; you can even add colour to it by streaming colours inline with the printing. Remember that a Cs::Color will, by default, change the foreground colour. See the Colour tutorial for more information and see how to change both the foreground and background colours.
The cursor can be hidden from view. This means that it will no longer replace the character 'underneath' it and that character will be visible.
Hiding the cursor is simple:
cs.setShowCursorVisible(false);
Showing the cursor is also very simple:
cs.setShowCursorVisible(true);
There is no need to add code to show or hide the cursor just yet; we are going to alternate between them using a clock to simulate the flashing of actual console screens.
First, we create an SFML clock:
sf::Clock clock;
This goes just before the main window loop (the while(window.isOpen()) loop)
Then we can continuously update if the cursor is being shown based on the current time:
cs.setShowCursor((clock.getElapsedTime() % sf::seconds(1.5f)) < sf::seconds(0.9f));
This goes after the event loop and just before the window update (clear/draw/display)
You can run this program now and see that the cursor is now flashing in a way you would expect!
Console Screen can also perform double-height printing. That is, each character is stretched vertically across two cells. This is achieved using cell attributes, which will be explained in a future tutorial.
A 'stretched' cell is, technically, not a single cell stretched over two cells but, instead, two cells displaying a stretched top or bottom half of the character. A bottom half cell directly below a top half cell effectively displays what appears to be a single cell stretched over two cells. In fact, the 'stretched half' can be used independantly per cell so you can have just the top or bottom half without its counterpart or even swap the two around so that the bottom half is above the top half, if you really wanted to.
The reason we mention double printing here is that it affects the cursor. For example, as soon as we change the printing method to print as double-height, the cursor now fills two adjoining cells - the original location (actual cursor location) - and the cell directly below it.
Of course, they are both inverted and flashing.
We can change to double-height printing using:
cs << Cs::StretchType::Both;
Then, we can print a string, which is printing at double-height:
cs << "Double Height";
And then, we can use a newline cursor command:
cs << Cs::CursorCommand::Newline;
At this point, you will notice that the cursor actually moves down by two rows instead of just one so that it is beneath the double-height print.
You can now switch off double-height printing:
cs << Cs::StretchType::None;
If you switch off double-height printing before performing the newline command, the cursor only moves down by a single row (as normal) so it would then be in the bottom half of the double print. Feel free to try it: move the Cs::StretchType::None so that it is before the Cs::CursorCommand::Newline and notice where the cursor is now. Remember to move it back again so that the stretch type is reset after the newline command.
This is about how the screen automatically scrolls when you move the cursor off the edge of the screen in certain places (and how moving too far upwards sets the cursor to "home"
When the cursor attempts to travel to a location outside of the screen, it will be placed into a location determined by the attempt. If the cursor is too far to the right, it will be placed on the next line, starting from the left and however 'too-far' it was, will be how far from the left it will be.
If the cursor attempts to be located at (42, 0), then it will be placed at (2, 1) instead. That is 3 cells to far to the right so it will be at the third cell from the left on the next row. Remember that the far right column is column 39 (in our example screen).
The cursor cannot directly be set to a negative number so it cannot be directly past the left edge. However, it can be moved left from the left edge and that works similarly - it is placed on the row above, how 'too-far' from the right.
If the cursor is at (0, 1) and you command the cursor to move 3 to the left, it will be placed (37, 0) - the third cell from the right on the above row.
Again, the cursor cannot directly be set above the top-edge but it can be moved past that edge. Any attempt to move the cursor past the top edge results in the cursor been 'sent home'. That is, set to (0, 0) - the top-left corner.
The final edge - the bottom - acts very differently. If the cursor attempts to be located below the bottom row, it is placed on the bottom row at the correct column. However, the entire screen is also 'scrolled' upwards by the amount that the attempt was 'too-far'.
If the cursor attempts to be located at (5, 25), it will be placed at (5, 21) and the screen will be scrolled upwards by 4 rows. Remember that the bottom row is row 21 (in our example screen).
There is another thing to note: scrolling past the right edge on the bottom row will result in in being placed on the bottom row at the column determined by its 'too-far' amount and the screen will also be scrolled by the number of rows required. The most common time this happens is when you move to the right from the bottom-right cell; this happens when you print to that cell. This causes the screen to scroll upwards and the cursor goes to the left of the bottom row (the same row is was but is effectively on the row below since the screen was scrolled upwards.
We will now add some interactivity to our example so that we can see these effects. This following code is to be added to the event loop under the KeyPressed case. In the event.key.code switch, we already have an sf::Keyboard::Escape case. These four cases are additional cases for that switch:
case sf::Keyboard::Right:
cs << Cs::CursorCommand::Right;
break;
case sf::Keyboard::Left:
cs << Cs::CursorCommand::Left;
break;
case sf::Keyboard::Up:
cs << Cs::CursorCommand::Up;
break;
case sf::Keyboard::Down:
cs << Cs::CursorCommand::Down;
break;
The 4 indentation tabs were removed from this code to allow it to show more cleanly here.
This code allows you to press a cursor key to move the cursor. It moves one cell at a time in direction of the key.
Try moving the cursor off the left or right of the screen and see how the cursor 'wraps' but on a different row. Try moving the cursor off the top and see the cursor moved to its top-left. Try moving the cursor off the bottom of the screen and see it scrolled to accomodate the move. Try also moving the cursor off the right of the screen while on the bottom row to see the screen scroll there too.
It's very important to remember that any cell that is 'scrolled off the edge screen' is lost forever.
If the cursor is still in double-print mode, the cursor attempts to behave as if it is actually two-cells tall.
Double-print mode is set by changing the stretch type in the print properties. This will be discussed further in a future tutorial.
If, in double-print mode, the cursor moves past the right or left edge, it will 'wrap' as before but move up or down by 2 rows instead of just 1. This is similar to how double-height printing affects the newline command.
Since the screen is scrolled to accommodate the space needed for another 'row of characters' and a row of double-height characters takes two actual rows, scrolling off the bottom may scroll two rows instead.
It will scroll two rows if in double-print mode and cursor is moved downward once while it is resident in the bottom two rows, or, if it is moved to the right while it is resident in the bottom two cells in the right-most column. It will only scroll one row if in double-print mode and cursor is moved downward once while it is resident in the two rows above the bottom (single) row or moved off the right edge from this row position.
We will add a key event to allow switching between standard- and double-print mode:
case sf::Keyboard::D:
cs << ((cs.getStretchType() == Cs::StretchType::None) ? Cs::StretchType::Both : Cs::StretchType::None);
break;
Again, this goes in the same switch (for key presses) as before and can just go directly below the previously added code. Also again, the 4 preceding tabs have been removed.
Now, if you run the program in its current state, you can move the cursor around the screen with the cursor keys and also press the D key to flip between standard-sized printing and double-height printing; this allows you to see the affect of double-print mode on the cursor movement as well as the scrolling.
If you want to see exactly where the cursor is at any one time, you may want to disable the line that flashes the cursor. This is up to you. If you scroll off the entire screen so there is nothing on display (all blank cells), you will need to re-run the program to get back those characters. You should note that the screen only ever automatically scrolls when the cursor passes the bottom area, not the other edges. You can, actually, manually scroll the screen at any time in any direction but we will surely talk about that in the future.
The scrolling effect discussed above is called "automatic scrolling". That is because it automatically scrolls upwards when the cursor attempts to be in a row below the bottom one.
This can actually be disabled manually, which removes any automatic scrolling. The cursor is moved differently if automatic scrolling is disabled.
Let us add another key command to switch between enabled automatic scrolling and disabled automatic scrolling:
case sf::Keyboard::S:
cs.setScrollAutomatically(!cs.getScrollAutomatically());
break;
As before, this goes in the key press section; this can go directly below the double-print mode switch above. It has also had the 4 preceding tabs removed.
Try now to test the cursor on the edges as before. You will find that it behaves the same for the top, left and right edges but differently for the bottom edge (and right from the bottom-right cell). This is because it cannot move into the space allocated for it by a scroll. It instead moves to the 'end' of the screen (the bottom-right cell - opposite of 'home').
Basically, when automatic scrolling is disabled, the bottom edge and bottom-right corner behave in the same way as the top edge and top-left corner. You can also note that even if the cursor is not occupying the bottom row, it may still be 'ended' if in double-print mode, if only actual row is below the double-height cursor.
Remember, that after every printed character, the cursor is moved to the right. Knowing how the cursor moves around also lets you know how printing works since it follows the same path as if, in our program, we just kept moving right for each character.
Now we have a small exercise for you to do for yourselves.
Add another case to the key press event section so that whenever you press the space bar, a character is printed to the screen. This will allow you to see the effect on the cursor of printing a character at the cursor's location.
The character can be any specific one you choose (although it would be better to be a visible one, not a blank one) or even a (psuedo) random one each time!
We won't be showing the solution here but we will include a simple one in the final code in case you get really stuck.
There we have it. A program that shows you how to move around the cursor using commands that allows you print from any location of the screen and also allows you to interactively test cursor movement using keys.
You can now run the program and see the result - the first text including corrections, the table and the double-height text are all immediately displayed and the cursor is colour-inverted purple star underneath:
You can then use the keyboard to interactively control the cursor.
That concludes the tutorial on the cursor.
#include <SFML/Graphics.hpp>
#include <SelbaWard/ConsoleScreen.hpp>
int main()
{
sf::Texture csTexture;
if (!csTexture.loadFromFile("PATH/Windows Console ASCII.png"))
return EXIT_FAILURE;
sf::RenderWindow window(sf::VideoMode(800, 600), "Console Screen tutorial - Cursor");
Cs cs;
cs.setTexture(csTexture);
cs.setTextureTileSize({ 8, 12 });
cs.setNumberOfTextureTilesPerRow(16);
cs.setMode({ 40, 22 });
cs.setSize(cs.getPerfectSize());
cs.setScale({ 2.f, 2.f });
cs.setOrigin(cs.getSize() / 2.f);
cs.setPosition(sf::Vector2f(window.getSize() / 2u));
// Visual Representation
cs.setCursor(15);
cs.setCursorColor(Cs::Color(11));
cs.setUseCursorColor(true);
cs.update();
cs.setInvertCursor(true);
// Moving
cs << Cs::Location({ 7u, 3u });
cs << "We're over here!";
cs << Cs::CursorCommand::Left << "...";
cs << Cs::Left(7u);
cs << "'";
cs << Cs::CursorCommand::Home;
cs << Cs::Down(5u);
cs.setCursorTab(8u);
cs << Cs::CursorCommand::Tab << Cs::Char('\xC9') << std::string(7u, '\xCD') << Cs::Char('\xCB') << std::string(7u, '\xCD') << Cs::Char('\xCB') << std::string(7u, '\xCD') << Cs::Char('\xBB') << Cs::CursorCommand::Newline;
cs << Cs::CursorCommand::Tab << "\xBA Col A" << Cs::CursorCommand::Tab << "\xBA Col B" << Cs::CursorCommand::Tab << "\xBA Col C" << Cs::CursorCommand::Tab << Cs::Char('\xBA') << Cs::CursorCommand::Newline;
cs << Cs::CursorCommand::Tab << Cs::Char('\xCC') << std::string(7u, '\xCD') << Cs::Char('\xCE') << std::string(7u, '\xCD') << Cs::Char('\xCE') << std::string(7u, '\xCD') << Cs::Char('\xB9') << Cs::CursorCommand::Newline;
for (std::size_t row{ 0u }; row < 5; ++row)
{
cs << Cs::CursorCommand::Tab;
for (std::size_t column{ 0u }; column < 3; ++column)
{
cs << "\xBA " << Cs::Number(row * row * row * row + 8 - column * column * column) << Cs::CursorCommand::Tab;
}
cs << Cs::Char('\xBA') << Cs::CursorCommand::Newline;
}
cs << Cs::CursorCommand::Tab << Cs::Char('\xC8') << std::string(7u, '\xCD') << Cs::Char('\xCA') << std::string(7u, '\xCD') << Cs::Char('\xCA') << std::string(7u, '\xCD') << Cs::Char('\xBC') << Cs::CursorCommand::Newline;
// Double Height Printing
cs << Cs::StretchType::Both;
cs << "Double Height";
cs << Cs::CursorCommand::Newline;
cs << Cs::StretchType::None;
sf::Clock clock;
while (window.isOpen())
{
sf::Event event;
while (window.pollEvent(event))
{
switch (event.type)
{
case sf::Event::Closed:
window.close();
break;
case sf::Event::KeyPressed:
switch (event.key.code)
{
case sf::Keyboard::Escape:
window.close();
break;
// Move The Cursor
case sf::Keyboard::Right:
cs << Cs::CursorCommand::Right;
break;
case sf::Keyboard::Left:
cs << Cs::CursorCommand::Left;
break;
case sf::Keyboard::Up:
cs << Cs::CursorCommand::Up;
break;
case sf::Keyboard::Down:
cs << Cs::CursorCommand::Down;
break;
// Toggle Double-Height Printing
case sf::Keyboard::D:
cs << ((cs.getStretchType() == Cs::StretchType::None) ? Cs::StretchType::Both : Cs::StretchType::None);
break;
// Toggle Automatic Scrolling
case sf::Keyboard::S:
cs.setScrollAutomatically(!cs.getScrollAutomatically());
break;
// Print Random Character At Cursor Location
case sf::Keyboard::Space:
cs << Cs::Char(rand() % 256u);
break;
}
break;
}
}
// Show And Hide (Flash) Cursor
cs.setShowCursor((clock.getElapsedTime() % sf::seconds(1.5f)) < sf::seconds(0.9f));
window.clear(sf::Color(128, 128, 128));
window.draw(cs);
window.display();
}
return EXIT_SUCCESS;
}