Converting my existing code into using the Elm architecture

I’ve been having a good conversation about Iced pros/cons in this thread and so decided to try converting one of my side projects into using Iced instead of FLTK.rs. And, of course, got confused right from the get-go. Don’t get me wrong – I think learning how to use the Elm architecture is going to improve my programming skills, so I’m not complaining. I’m just having trouble getting started. Here’s my question:

In all or most of my various projects I try to keep main.rs fairly simple and divide up my lib.rs file into several modules. Each module has it’s own set of structs that, at least in my mind, correspond to Iced’s State segment of the State-Message-View-Update (SMVU) architecture. So, how do I fit my existing code into the SMVU framework? This is really, really important for when (if) I decide to convert my main project (1000’s of lines) over into Iced. Thoughts anyone?

You could take a look at GitHub - airstrike/iced_receipts: An iced example showcasing clean screen management, unified event handling, and keyboard-navigable forms for an example on how you structure your app, breaking things up into multiple pieces

1 Like

Thanks for the shoutout to iced_receipts!

I’d also say that the very first concept @jtreagan should take a look at is how we use .map() to compose views (Elements, really) and Tasks.

Make sure to read the official pocket guide too, particularly the “Scaling Applications” section:

Thanks for your replies. I did take a look at @airstrike ‘s iced_receipts and have been looking through the documentation on docs.rs. I find I’m still confused and am wondering if the documentation’s statement that “iced is easy to learn for advanced Rust programmers” simply doesn’t apply to me. I certainly am not an “advanced” Rust programmer and that is probably why, when I look at examples like iced_receipts or the examples in the repository, I still am struggling to see Elm’s SMVU structure working in the code. I’ve never been good at learning by looking at examples and that weakness is likely in play now. I’m wondering if I should take @hecrj’s advice in the documentation and “wait patiently until the book is finished.” (Note: I really like what Hector has done in the book so far and am looking forward to seeing it finished.)

That said I would like to give it one more try. So, in an attempt to simplify the problem as much as possible, I attempted to create a single window that did nothing and contained nothing. That might be too basic and I didn’t succeed. My AI kept messing up, too, so I gave up on that tactic. So, instead of that tactic I pulled up my just-for-fun side project of creating a solitaire game. If I can convert that project over to Iced then I will have made some progress. The simplest task is to create and display a single card using an *.svg image file. (Keep in mind that I’m currently using FLTK.rs.)

Here’s the struct I use for a basic card:

#[derive(Debug, Clone)]
pub struct Card {
    pub front: SvgImage,
    pub back_svg: SvgImage,
    pub back_png: PngImage,
    pub value: String,
    pub suit: String,
    pub color: String,
}

The types SvgImage & PngImage come from FLTK and, of course, I will need to find equivalents in Iced. Anyway, am I correct in saying that this struct should be considered my State in the SMVU architecture?

It is. You’re looking for iced::widget::{image, svg}::Handle. You’ll also want to enable the svg and image feature flags.

Just so you know, it’s okay if you don’t want to baby me along on this. I’ll be fine waiting for a better tutorial to come out.

Anyway, I found this example in iced::widget::image but I’m not sure how to implement it. Want to show me how?

use iced::widget::image;

enum Message {
    // ...
}

fn view(state: &State) -> Element<'_, Message> {
    image("ferris.png").into()
}

Check out the pokedex example. I suggest cloning iced locally and running cargo run -p example -r

It may be years before that tutorial exists, so we’re be happy to help in the meantime.

And since you mentioned AI, I suggest feeding your AI a few examples from the repo to ground its answers. Either feed it manually or tell Claude Code/Codex etc to peruse the examples and stick to the API they demonstrate

I took your advice and pointed my AI toward the examples folder on the Iced github site. It does seem to help. I feel like I may have made some progress toward understand how Iced is put together and how the Elm architecture works.

Here’s what I have so far for my state:

pub struct Card {
    pub front: svg::Handle,
    pub back_svg: svg::Handle,
    pub back_png: image::Handle,
    pub value: String,
    pub suit: String,
    pub color: String,
}
1 Like

Continuing on, since I had my State figured out, I next needed to figure out the Message, Update, and View sections. And got stuck. So, taking my own advice from when I was teaching mathematics, I attempted to simplify the problem once again. Leaving my solitaire card project behind for the moment, I pulled up the code for an older side project that was more of a lark than anything else. I had just installed a new CPU and wanted to see if I could write an app that would max it out. So, decided to identify prime numbers between any two given min and max limits. I used a brute force algorithm hoping it would tax the capabilities of my CPU. It didn’t, but was a fun & simple project. (I haven’t added multiple threads to try to use all the available CPU cores, but maybe some day??) Anyway, this app doesn’t use a GUI at all and is entirely terminal based. My goal was to learn how to use the ELM architecture. I succeeded, but would like to run my code past you guys to get your suggestions. Iced isn’t involved so far, but that is next step. Anyway, here is the original code before ELM in two sections (main.rs and lib.rs)……. main.rs is first:

/*
        Find and print all prime numbers within a given range.
 */
use std::thread;
use std::thread::JoinHandle;
use coldprimes::numput::inpt_u64;

fn main() {

    // region Input the range being searched

    println!("\nWhat range of numbers do you wish to check for primes?");
    println!("Please enter your starting number:    ");
    let mut start = inpt_u64();
    let beginrange = start;
    println!("Please enter your ending number:    ");
    let endrange = inpt_u64();

    // endregion

    let mut primeslist: Vec<u64> = Vec::new();

    let mut numtup: (bool, u64);
    let mut i = 0;

    while start <= endrange {
        numtup = porc(start);
        if numtup.0 == true {
            println!("{}    is a prime number.", start);
            primeslist.push(start);  // Store the prime number in the vector.
            i += 1;
        }
        start += 1;

        if start % 10 == 0 {
            // Nothing to do with finding primes.  This is feedback.
            println!("Working on {} and higher.", start);
        }
    }
    println!(
        "\nThere are  {}  primes between  {} and {}. \n",
        i, beginrange, endrange);
    println!("Here they are: \n, {:?}", primeslist);
}


/*
                    Prime or Composite (porc())

    Determine if a number is prime and return 'true' for primeness
    along with a divisor of 1.  If the number is not prime, then
    return 'false' along with the first divisor encountered.  Please
    note that this algorithm uses a brute force tactic.  There are
    better solutions that should be explored.  

*/  // porc() description -- Prime or Composite

pub fn porc(check4prime: u64) -> (bool, u64) {
    if check4prime == 2 {
        return (true, 1);
    } // 2 is the only even prime.  This ensures it returns as prime.

    if check4prime % 2 == 0 {
        return (false, 2);
    } // If the number is even, it can't be prime (except for 2).
    // Checking this here lets me increment i by 2 rather than 1,
    //     thus cutting in half the number of necessary iterations.

    let mut i = 3;
    while i < check4prime {
        if check4prime % i == 0 {
            return (false, i);
        }
        i += 2; // Skip the even numbers;
    }
    return (true, 1);
}

and here is the old lib.rs :

// Input, error check, and return numbers
// from standard input.

pub mod numput {

    // Return a u64 from standard input.

    pub fn inpt_u64() -> u64 {
        loop {
            let mut input = String::new();

            std::io::stdin()
                .read_line(&mut input)
                .expect("Failed to read line.");

            let input: u64 = match input.trim().parse() {
                Ok(num) => num,
                Err(_) => {
                    println!("Please enter a numeric value.  Thanks!");
                    continue;
                }
            };
            break input;
        }
    }
}

Here is my new and improved version using the ELM format:

use coldprimes::{getprimes, Message};

fn main() {
    let mut state = getprimes::new();  // Initialize the state.

    // Initial View (Introduction)
    println!("\n --- Prime Finder (Elm Architecture) --- \n");

    // Start the message loop.
    state.update(Message::Getmin);
    state.update(Message::Getmax);
    state.update(Message::CheckPrime(0));  // CheckPrime(0) is a dummy value.

    // View the results
    state.view();
}

and

use crate::numput::inpt_u64;
use crate::primes_functions::porc;

// region State

pub struct getprimes {
    rangemin: u64,
    rangemax: u64,
    primeslist: Vec<u64>,
    primescount: u64,
}

impl getprimes {
    pub fn new() -> Self {
        Self {
            rangemin: 0,
            rangemax: 0,
            primeslist: Vec::new(),
            primescount: 0,
        }
    }
}

// endregion

// region Messages

pub enum Message {
    Getmin,
    Getmax,
    CheckPrime(u64),
}

// endregion

// region Update

    impl getprimes {
        pub fn update(&mut self, message: Message) {
            match message {
                Message::Getmin => {
                    println!("Please enter the starting number: ");
                    self.rangemin = inpt_u64();
                },
                Message::Getmax => {
                    println!("Please enter the ending number: ");
                    self.rangemax = inpt_u64();
                },
                Message::CheckPrime(num) => {

                    let mut use_tuple: (bool, u64);

                    // region Loop through the range to find primes.

                    let mut i = 0;
                    while self.rangemin <= self.rangemax {
                        use_tuple = porc(self.rangemin);
                        if use_tuple.0 == true {
                            println!("{}    is a prime number.", self.rangemin);
                            self.primeslist.push(self.rangemin);  // Store the prime number in the vector.
                            self.primescount += 1;
                            i += 1;
                        }
                        self.rangemin += 1;

                        // Provide feedback on progress.
                        if self.rangemin % 100 == 0 {
                            println!("Working on {} and higher.", self.rangemin);
                        }
                    }

                    // endregion

                }
            }
        }
    }

// endregion

// region View

impl getprimes {

    pub fn view(&self) {
        println!(
            "\nThere are  {}  primes between  {} and {}. \n",
            self.primescount, self.rangemin, self.rangemax);
        println!("Here they are: \n, {:?}", self.primeslist);
    }
}


// endregion


/// A module containing functions related to prime number operations.
///
pub mod primes_functions {

    /// # Prime or Composite (porc())
    ///
    /// Determine if a number is prime and return 'true' for primeness
    /// along with a divisor of 1.  If the number is not prime, then
    /// return 'false' along with the first divisor encountered.  Please
    /// note that this algorithm uses a brute force tactic.  
    ///
    pub fn porc(check4prime: u64) -> (bool, u64) {

        // region Two is the only even prime.  This ensures it returns as prime.
        if check4prime == 2 {
            return (true, 1);
        }
        // endregion

        // region Check if the number is even.

        if check4prime % 2 == 0 {
            return (false, 2);
        } // If the number is even, it can't be prime (except for 2).
        // Checking this here lets me increment i by 2 rather than 1,
        //     thus cutting in half the number of necessary iterations.

        // endregion

        // region Loop through and check the odd numbers as divisors.

        let mut i = 3;
        // Could you break the loop when i == check4prime / 2?  I think so.
        while i < check4prime {
            if check4prime % i == 0 {
                return (false, i);
            }
            i += 2; // Skip the even numbers;
        }

        // endregion

        return (true, 1);
    }
}

/// Input, error check, and return numbers from standard input.
///
pub mod numput {

    /// Return a u64 from standard input.
    ///
    pub fn inpt_u64() -> u64 {
        loop {
            let mut input = String::new();

            std::io::stdin()
                .read_line(&mut input)
                .expect("Failed to read line.");

            let input: u64 = match input.trim().parse() {
                Ok(num) => num,
                Err(_) => {
                    println!("Please enter a numeric value.  Thanks!");
                    continue;
                }
            };
            break input;
        }
    }
}

As you look at what I’ve done, please keep in mind that my goal with this code is to learn the ELM format. Adding the Iced GUI is my next step. I would love to hear your analysis and suggestions. Thanks.

Well, I think I was a bit presumptive in my last post. That’s just too much code to sift through, especially if you have a day job. Anyway, I did learn a lot with that code I showed you, but simplified even further with another little mini side project from when I was first learning Rust. It was a simple application that calculated and printed out all possible factors for any given integer. I first converted it into the Elm State-Message-Update-View (SMUV) format and then it was easy to add Iced for the user interface. The result was quite nice and you can look at it here. While I still have much to learn about using Iced, I’m finding it to be a much smoother and easier to implement GUI than FLTK.

However, I still need help seeing how to implement SMUV in my main project where I have my code divided into multiple modules, each with its own State. The program uses three nested structs. Below is some code demonstrating that nested struct structure. (I used the analogy of an egg with the yolk as the innermost struct, the white as the middle level, and the shell as the outer struct that contains and uses the other structs. I call this my “one shell to rule them all” architecture. :grinning_face: )



pub mod shell {
    use crate::white::White;

    pub struct Shell {
        pub name: String,
        pub carapace: Vec<White>,
    }

    impl Shell {
        pub fn new() -> Shell {
            Self {
                name: "John".to_string(),
                carapace: Vec::new()
            }
        }
    }

    /*
        Code for multiple functions dealing with the Shell struct.
     */
}


pub mod white {
    use crate::yolk::Yolk;

    pub struct White {
        pub name: String,
        pub albumin: Vec<Yolk>,
    }

    impl White {
        pub fn new() -> White {
            Self {
                name: "Thomas".to_string(),
                albumin: Vec::new()
            }
        }
    }


    /*
    Code for multiple functions dealing with the White struct.
 */

}


pub mod yolk {

    pub struct Yolk {
        pub name: String,
        pub age: u8,
    }


    impl Yolk {
        pub fn new() -> Yolk {
            Self {
                name: "Reagan".to_string(),
                age: 69,
            }
        }
    }

    /*
    Code for multiple functions dealing with the Yolk struct.
 */
}

Besides the nested structs depicted above, I also use several global constants and global variables. How do those fit in with Iced’s conventions?

I’ve considered moving all three structs with their imple’s to their own module, leaving the functions where they are in the existing modules, but I’m having trouble seeing how to create the update (U) and view (V) functions in a way that Iced can use. Do I have a separate U and V for each struct? Does Iced require that I somehow condense all three U and V needs down into one U and V each to “rule them all”? Hmmm….while I’ve been writing this post I thought about maybe creating a kind of “super struct” that would contain all three of my nested structs and then do my U and V functions as imple’s of that super struct. I’ve never tried anything like that, but maybe it would work. I need some guidance to hopefully keep me from spinning my wheels in endless experimentation, so, thoughts, anyone?

I think you’re on the right path. Again iced_receipts linked at the start of the thread was created with the purpose of showcasing what you’re trying to do.

And for a shorter example, see the official pocket guide:

here’s a simple CRUD app using a layered MVU architecture