Convert "sl" Source Code from C to Rust

Click here to see entire code to this point.
Previous = Use "ncurses" to Draw the Train

Create Our Own my_mvaddstr() Function

In the last tutorial, Use "ncurses" to Draw the Train, we used ncurses to draw our image of choice on a text-based terminal window, and ran into several problems, most of which we have now fixed. The problems remaining are:

This lesson will focus on creating a function, "my_mvaddstr()", instead of using the standard "mvaddstr()" function that is a part of ncurses, which will allow the image to be drawn anywhere on screen without the problems that would otherwise occur when parts of the train are off the screen edges. We'll do this by trimming off the pieces of each line that would otherwise print in the off-screen areas.

We can start by replacing our existing call to "mvaddstr()" with a call to our soon-to-exist new function, "mymvaddstr()":

Replace the Call to "mvaddstr()" with a Call to "my_mvaddstr()"

display.rs
/* display.rs */

use ncurses::*;

pub fn draw_frame(mut row: i32, col: i32, frame: &Vec<String>) {
    for each_line in frame {
        my_mvaddstr(row, col, each_line);
        row += 1;
    }
    refresh();
} // end of draw_frame()  

Create the New "my_mvaddstr()" Function

And now we create the new function, at the bottom of "display.rs". Notice that this fumction does not need to be public to other project modules; it's only used by the "draw_frame()" function in this module.

display.rs
...
} // end of draw_frame()


fn my_mvaddstr(row: i32, col: i32, frame_line: &str) {
    /* This function recieves one line of an ASCII-art image, and
       trims away any of that line that would otherwise be displayed
       off-screen; then it displays the remaining portion of the
       line at the designated row,col coordinates.
    */
    mvaddstr(row, col, frame_line);
} // end of my_mvaddstr()

This should not change the functioning of our program in any way; it just sets up a dummy function to see if the call to the function works. If you compile and run the program, it should behave exactly as it did before.

But now that we have a working skeleton for this function, we can modify it to do some bounds-checking. Let's do that now.

In order to check for the bounds, this new function will need to know the limits of the screen dimensions. It will know that the left-edge is zero, and that the top edge is zero. But we'll need to provide it with the right- and bottom-edges. We could pass those two values from the "main()" function to our "draw_image()" function, and then pass them from the "draw_image()" function to our new "my_mvaddstr()" function. That might even be the best way. But since the ncurses functions are available to all the functions within the "main.rs" module, we can also do it another way: we can simply call the ncurses' getmaxyx() function again from within our new "my_mvaddstr()" function. We'll do it both ways, so that you can see it both ways.

Method 1 - Passing the Screen Boundaries from main() to draw_image() to my_mvaddstr()

main.rs
...
fn main() {
...
        draw_frame(frame, row, col, screen_width, screen_height);
        sleep(delay_between_frames);
    }
    endwin(); // Exit ncurses and restore screen to normal.
} // end of main()
display.rs
...
pub fn draw_frame(frame: &Vec<String>, mut row: i32, mut col: i32, screen_width: i32, screen_height: i32) {
    for each_line in frame {
        my_mvaddstr(row, col, each_line, screen_width, screen_height);
        row += 1;
    }
    refresh();
} // end of draw_frame()

fn my_mvaddstr(row: i32, col: i32, frame_line: &str, screen_width: i32, screen_height: i32) {
    /* This function recieves one line of an ASCII-art image, and
       trims away any of that line that would otherwise be displayed
       off-screen; then it displays the remaining portion of the
       line at the designated row,col coordinates.
    */
    mvaddstr(row, col, frame_line);
} // end of my_mvaddstr()

Again, you should see no difference, yet, in how your program runs. Now let's try the other method. Make sure to undo the changes you may have made in the Method 1 above before doing the method below.

Method 2 - Getting Screen Boundaries from ncurses in the my_mvaddstr() function

main.rs
...
fn main() {
...  
        draw_frame(frame, row, col, screen_width, screen_height);
        sleep(delay_between_frames);
    }
    endwin(); // Exit ncurses and restore screen to normal.
} // end of main()
display.rs
...
pub fn draw_frame(frame: &Vec<String>, mut row: i32, mut col: i32, screen_width: i32, screen_height: i32) {
    for each_line in frame {
        my_mvaddstr(row, col, each_line, screen_width, screen_height);
        row += 1;
    }
    refresh();
} // end of draw_frame()

fn my_mvaddstr(row: i32, col: i32, frame_line: &str, screen_width: i32, screen_height: i32) {
    /* This function recieves one line of an ASCII-art image, and
       trims away any of that line that would otherwise be displayed
       off-screen; then it displays the remaining portion of the
       line at the designated row,col coordinates.
    */

    // Get screen size.
    let mut screen_height = 0;
    let mut screen_width = 0;
    getmaxyx(stdscr(), &mut screen_height, &mut screen_width);
    
    mvaddstr(row, col, frame_line);
} // end of my_mvaddstr()

So Which Method?

So which method should we use? With the second method, we have to call the "getmaxyx()" function every time the "my_mvaddstr()" function is called, which likely increases the "expense" of the code, costing more CPU cycles. On the other hand, that means that each time the "my_mvaddstr()" function is called, it gets updated with the size of the terminal window, so that if the terminal window size changes during the running of the program, the "my_mvaddstr()" function will learn about the new size. But the "col" value back in "main()" does not, so that's probably not of much value to us. The second method is also perhaps a bit easier to program, as it doesn't involve passing the two parameters from function to function to function. But back on the first hand, these values are needed back in "main()" to calculte the "row" and "col" values, so we'll need to get the screen size ther in "main()" anyway. I think the best option is probably Mathod 1, because that would limit the "my_mvaddstr()" to having only one point-of-entry for the needed data.

So to get back on-track, make your "main.rs" and "display.rs" files look like the following. (Notice I removed the "#![allow(unused)]" line; that may generate more noise during compilation, but those warnings are there for a reason; let's not close our eyes to them.)

main.rs
/* main.rs */

mod convert_to_vec;
mod display;
mod get_options;
mod images;

use convert_to_vec::str_to_vecvecstring;
use display::draw_frame;
use get_options::get_opts;
use ncurses::*;
use std::{thread::sleep, time::Duration}; // For a sleep/delay between image frames.

fn main() { 
    // Get image data; put it in "model_vecvecstring".
    let (model_string, fly, oops, interruptible) = get_opts();
    let model_vecvecstring = str_to_vecvecstring(&model_string);

    // ncurses housekeeping stuff.
    initscr(); // Start ncurses, initializing the screen.
    noecho(); // Turn off keyboard echo to the screen.
    curs_set(CURSOR_VISIBILITY::CURSOR_INVISIBLE); // Turn off the display of the cursor.
    let mut screen_height = 0; // How high...
    let mut screen_width = 0; // ... and wide ...
    getmaxyx(stdscr(), &mut screen_height, &mut screen_width); // ... is the screen?

    // This will be our delay, in milliseconds, between frame images.
    let delay = 100;
    let delay_between_frames = Duration::from_millis(delay);

    // How many frames in the image?
    let num_of_frames = model_vecvecstring.len() as i32;

    // How tall is the image? (How many lines are in the first inner vector?)
    let image_height = model_vecvecstring[0].len() as i32; // Normally type usize, but we need i32 for the "let row..." below.
        
    // How wide is the image (in characters/screen columns)?
    let image_width = model_vecvecstring[0][0].len() as i32;

    let row: i32 = screen_height - image_height;
    for col in (-image_width..screen_width).rev() { // Distance to travel is width of screen plus the width of the image.
        let frame_num = (col.abs() % num_of_frames) as usize; // The remainder of this math tells us which frame to display, in descending order.
        let frame = &model_vecvecstring[frame_num]; // For a 6-frame image, the result will be somewhere in the range of 0 to 5.
        draw_frame(frame, row, col, screen_width, screen_height); // ... and draw it.
        sleep(delay_between_frames);
    }
    endwin(); // Exit ncurses and restore screen to normal.
} // end of main()
display.rs
/* display.rs */

use ncurses::*;

pub fn draw_frame(frame: &Vec<String>, mut row: i32, col: i32, screen_width: i32, screen_height: i32) {
    for each_line in frame {
        my_mvaddstr(row, col, each_line, screen_width, screen_height);
        row += 1;
    }
    refresh();
} // end of draw_frame()

fn my_mvaddstr(row: i32, col: i32, frame_line: &str, screen_width: i32, screen_height: i32) {
    /* This function recieves one line of an ASCII-art image, and
       trims away any of that line that would otherwise be displayed
       off-screen; then it displays the remaining portion of the
       line at the designated row,col coordinates.
    */

    mvaddstr(row, col, frame_line);
} // end of my_mvaddstr()

Trim the Line to Fit within the Screen Boundaries

Since we're going to be trimming the line, we need a line that can be trimmed, that is, one that is mutable. Since a slice (&str) is not mutable by nature, we'll have to convert it to an owned String that is mutable by nature:

main.rs
fn my_mvaddstr(row: i32, col: i32, frame_line: &str,  screen_width: i32, screen_height: i32) {
    /* This function recieves one line of an ASCII-art image, and
       trims away any of that line that would otherwise be displayed
       off-screen; then it displays the remaining portion of the
       line at the designated row,col coordinates.
    */
    let mut line = frame_line.to_string();   // A &str is immutable by nature; we need a mutable string. Could also have used "String::from()" or "to_owned()".

    mvaddstr(row, col, frame_&line);
} // end of my_mvaddstr()

There are three conditions we need to check:

We'll have a section for each one of those questions in our new function. They are below. Note that we need to make the incoming parameter "col" mutable.

my_mvaddstr()
fn my_mvaddstr(row: i32, mut col: i32, frame_line: &str, screen_width: i32, screen_height: i32) {
    /* This function recieves one line of an ASCII-art image, and
       trims away any of that line that would otherwise be displayed
       off-screen; then it displays the remaining portion of the
       line.
    */
    let mut line = frame_line.to_string();   // A &str is immutable by nature; we need a mutable string.
  
  // Trim from left side as image moves off left edge
  if col < 0 {                               // If we've moved left of the left-edge,
    for _ in 0..col.abs() {                  // for each column beyond edge,
      line.remove(0);                        // trim the first char off the string, repeatedly.
    }                                        // "line" should now have front end trimmed off.
    col = 0;
  }
  
  // Trim from right side if it extends off right edge
  if col + (line.len() as i32) > screen_width {
    let amt_to_trim = (col + (line.len() as i32)) - screen_width;
    for _ in 0..amt_to_trim {
      line.pop();
    }
  }
  
  if (row >= 0) && (row < screen_height) {  // We're on-screen, so it's okay to print this line.

	  mvaddstr(row, col, &line);
  }
} // end of my_mvaddstr()  

This program should compile and run the image across the screen, coming in from the right edge, and going off the left edge

Except ...

If you use the D51 or C51 train, you'll notice that as soon as the train's nose hits the left edge of the screen, the wheels start turning in reverse. Uh oh.

This is because our math produces a different order of frames, once the "col" variable goes negative. Rats! We'll have to figure out how to re-code that segment of our program. The below should do it:

main.rs
...
    // How wide is the image (in characters/screen columns)?
    let image_width = model_vecvecstring[0][0].len() as i32;
    
    let row: i32 = screen_height - image_height;
  
    let distance_to_travel = image_width + screen_width;
    
    for col in (0..distance_to_travel).rev() {
    for col in ((-image_width)..screen_width).rev() {
        let frame_num = (col.abs() % num_of_frames) as usize; // The remainder of this math tells us which frame to display, in descending order.
        let frame = &model_vecvecstring[frame_num];
        draw_frame(frame, row, col - image_width, screen_width, screen_height);
        sleep(delay_between_frames);
    }
...

Just for kicks, let's Add Some Tweaks to Ferris.