In the previous step, we added state to our app as part of making the image grid dynamic. Recall that our plan of attack is as follows: - First, we'll add state to our app. - Then we'll use this state to load real images. We’ve already added state; now we’ll use it to load real images into the grid. > **Note:** > If you don't feel like typing along, you can find all the code for this step here: > https://github.com/makepad/image_viewer/tree/main/step_7 ## What you will learn In this step, you will learn: - How to initialise the state of the app when the app starts. - How to load images from disk. ## Downloading the Images Since we're going to display real images, we first need some images to display. You can either use your own images, or download an archive of the images we've used here: ![[images.zip]] ## Importing Types In the code that follows, we're going to make use of the `Path` and `PathBuf` types from the Rust standard library. Before we can use those types, however, we need to import them. Add the following code to the top of **app.rs**: ```Rust use std::path::{Path, PathBuf}; ``` This makes the `Path` and `PathBuf` types available for us to use. ## Updating the `State` struct Now that we have some images, let's update the `State` struct to store a list of image paths, instead of just a number of images. In **app.rs**, replace the definition of the `State` struct with the one here below: ```rust #[derive(Debug)] pub struct State { image_paths: Vec<PathBuf>, max_images_per_row: usize, } ``` This replaces the `num_images` field with an `image_paths` field, which contains a list of image paths. ### Updating the Implementation of the `Default` trait We also need to update the implementation of the `Default` trait for the `State` struct to reflect this change. In **app.rs**, replace the implementation of the Default trait for the State struct with the one here below: ```rust impl Default for State { fn default() -> Self { Self { image_paths: Vec::new(), max_images_per_row: 4, } } } ``` ### Updating the `num_images` method Finally, we need to update the `num_images` method for the `State` struct so that it returns the number of elements in `image_paths` (since we no longer have `num_images`). In **app.rs**, replace the definition of the `num_images` method for the `State` struct with the one here below: ```rust fn num_images(&self) -> usize { self.image_paths.len() } ``` We now have all the information we need to load real images. ## Updating the `App` struct Before we continue, we need to make a small change to the `App` struct. In **app.rs**, replace the definition of the `App` struct with the one here below: ```rust #[derive(Live)] pub struct App { #[live] ui: WidgetRef, #[rust] state: State, } ``` All this does is remove the derivation of the `LiveHook` trait for `App`. This is because we're about to write our own implementation of the `LiveHook` trait for App (we'll explain why shortly). ## Adding Helper Methods To make subsequent code easier to write, we're going to add some helper functions to the `App` struct. Add the following code to **app.rs**: ```rust impl App { fn load_image_paths(&mut self, cx: &mut Cx, path: &Path) { self.state.image_paths.clear(); for entry in path.read_dir().unwrap() { let entry = entry.unwrap(); let path = entry.path(); if !path.is_file() { continue; } self.state.image_paths.push(path); } self.ui.redraw(cx); } } ``` Here's what the `load_image_paths` method does: - First, it removes any existing paths from `image_paths`. - Next, it iterates over all entries in the directory at the given `path`: - For each entry: - It checks whether the entry is a file. - If it is not, the entry is skipped. - Otherwise, the entry's path is added to `image_paths`. - It calls `self.ui.redraw(cx)` to schedule the UI to be redrawn. This effectively replaces the paths in `image_paths` with the paths to all files in the directory at the given `path`. ### Error Handling For simplicity, we have not added any error handling to the `load_image_paths` method. There are several ways that this method could fail, including: - The given `path` does not exist. - The given `path` exists, but points to a non-directory file. - We don't have permission to read the directory. If any of these errors occur, our app will simply panic. That is acceptable for a tutorial, but in a real life app, we'd want more robust error handling here. ## Implementing the `LiveHook` trait Now that we have a method to load the paths to all images in a given directory, we need to make sure that this method gets called when the app starts. To do this, we can use the `LiveHook` trait. Recall that this trait provides several overridable methods which will be called at various points during our app's lifetime. In **app.rs**, add the following code: ```rust impl LiveHook for App { fn after_new_from_doc(&mut self, cx: &mut Cx) { let path = "path/to/your/images"; self.load_image_paths(cx, path.as_ref()); } } ``` > **Caution:** > Make sure to replace `"path/to/your/images"` with the path to your image directory! This implements the `LiveHook` trait for the `App` struct. We've overridden a single method, `after_new_from_doc`. This method will be called after our app has been fully initialised by the live design system, but before it starts running. That makes it the perfect place to call the `load_image_paths` method. ## Updating the Draw Code Our final step is to reload each `Image` in the image grid with the correct image right before it is drawn, using the list of image paths in the state. To do this, we need to change how `ImageRow`s are drawn. Replace the implementation of the `draw_walk` method on the `Widget` trait for the `ImageRow` struct with the one here below: ```rust fn draw_walk( &mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk, ) -> DrawStep { while let Some(item) = self.view.draw_walk(cx, scope, walk).step() { if let Some(mut list) = item.as_portal_list().borrow_mut() { let state = scope.data.get_mut::<State>().unwrap(); let row_idx = *scope.props.get::<usize>().unwrap(); list.set_item_range(cx, 0, state.num_images_for_row(row_idx)); while let Some(item_idx) = list.next_visible_item(cx) { if item_idx >= state.num_images_for_row(row_idx) { continue; } let item = list.item(cx, item_idx, live_id!(ImageItem)); let first_image_idx = state.first_image_idx_for_row(row_idx); let image_idx = first_image_idx + item_idx; let image_path = &state.image_paths[image_idx]; let image = item.image(id!(image)); image .load_image_file_by_path_async(cx, &image_path) .unwrap(); item.draw_all(cx, &mut Scope::empty()); } } } DrawStep::done() } ``` That's quite a lot of code, but the only thing that is new here are the following lines: ```rust let first_image_idx = state.first_image_idx_for_row(row_idx); let image_idx = first_image_idx + item_idx; let image_path = &state.image_paths[image_idx]; let image = item.image(id!(image)); image .load_image_file_by_path_async(cx, &image_path) .unwrap(); ``` Here's what this code does: - It obtains the first image index for the current row from the state (`first_image_idx`). - It computes the current image index (`image_idx`). - It uses the current image index to obtain the corresponding image path. - It calls item.image(..) to get a reference to the current `Image`. - It calls `image.load_image_file_by_path_async(...)` to reload the `Image`. The net result of this is that the `Image` inside each `ImageItem` is reloaded with the correct image right before the `ImageItem` is drawn. Image loading happens asynchronously, and is handled behind the scenes. > **Note:** > If the Image was already displaying the correct image, nothing is loaded. # Checking our Progress so far Let's check our progress so far. Make sure you're in your package directory, and run: ``` cargo run --release ``` If everything is working correctly, a window containing a grid of images should appear on your screen, but this time, they should be real images, not placeholder images: ![[ImageGrid Images.png]] ## What's next We now have a pretty decent implementation of an image grid. It can display real images, and dynamically change both the number of rows and number of items per row based on the number of images. We'll leave the image grid for what it is right now. In the next few steps, we're going to build a slideshow for our app.