Hello everyone, This is my first post, and I'm pretty new to Rust and have never used a GUI before. Normally, I would spend the time to learn a GUI library, but I'm in a very big time crunch, and just need to get this done. Could someone help me implement a GUI that's compatible with Windows into my program? I've heard about iced and Druid. The program I'm making is a recipe finder that helps you find recipes out of a ginormous JSON file I have. I'm much more used to Python (though I gotta say, Rust is better hehe), but I had to use Rust for this project. My code is as follows:
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use native_dialog::MessageDialog;
use native_dialog::MessageType;
use dialoguer::Input;
use std::env;
use std::path::PathBuf;
#[derive(Debug, Deserialize, Serialize)]
struct Recipe {
json: RecipeDetails,
}
#[derive(Debug, Deserialize, Serialize)]
struct RecipeDetails {
title: String,
ingredients: Vec<String>,
directions: Vec<String>,
}
fn load_recipe_data(file_path: &str) -> Result<HashMap<String, RecipeDetails>, Box<dyn std::error::Error>> {
let content = fs::read_to_string(file_path)?;
let recipe_data: HashMap<String, RecipeDetails> = serde_json::from_str(&content)?;
Ok(recipe_data)
}
fn find_matching_recipes<'a>(
recipe_data: &'a HashMap<String, RecipeDetails>,
available_ingredients: &[String],
) -> Vec<&'a RecipeDetails> {
let mut matching_recipes = Vec::new();
let tokenizer = Regex::new(r"\w+").unwrap();
for recipe in recipe_data.values() {
let recipe_ingredients: Vec<String> = recipe
.ingredients
.iter()
.flat_map(|ingredient| tokenizer.find_iter(ingredient))
.map(|m| m.as_str().to_lowercase())
.collect();
if available_ingredients
.iter()
.all(|keyword| recipe_ingredients.contains(&keyword.to_string()))
{
matching_recipes.push(recipe);
}
}
matching_recipes
}
fn display_recipes(recipes: &[&RecipeDetails]) {
if recipes.is_empty() {
println!("No matching recipes found.");
} else {
println!("Matching recipes:");
for (index, recipe) in recipes.iter().enumerate() {
println!("{}. {}", index + 1, recipe.title);
}
}
}
fn display_recipe_details(recipe: &RecipeDetails) {
println!("\nRecipe: {}\n", recipe.title);
println!("Ingredients:");
for ingredient in &recipe.ingredients {
println!("- {}", ingredient);
}
println!("\nDirections:");
for (step, direction) in recipe.directions.iter().enumerate() {
println!("{}. {}", step + 1, direction);
}
println!();
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let exe_path = env::current_exe()?;
let mut file_path = PathBuf::from(exe_path.parent().ok_or("Failed to get executable's parent directory")?);
file_path.push("recipes.json");
let recipe_data = load_recipe_data(&file_path.to_string_lossy())?;
loop {
let input: String = Input::new()
.with_prompt("Available Ingredients (comma-separated) or 'exit' to quit")
.interact()?;
if input.trim().to_lowercase() == "exit" {
break;
}
let available_ingredients: Vec<String> = input
.split(',')
.map(|ingredient| ingredient.trim().to_lowercase())
.collect();
let matching_recipes = find_matching_recipes(&recipe_data, &available_ingredients);
display_recipes(&matching_recipes);
if !matching_recipes.is_empty() {
let recipe_number: usize = Input::<usize>::new()
.with_prompt("Recipe Selection (enter the number)")
.interact()? - 1;
if let Some(selected_recipe) = matching_recipes.get(recipe_number) {
display_recipe_details(selected_recipe);
} else {
MessageDialog::new()
.set_type(MessageType::Error)
.set_title("Error")
.set_text("Invalid recipe number.")
.show_alert()?;
}
}
}
Ok(())
}
My cargo file is:
[package]
name = "recipe_finder"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
regex = "1.5"
native-dialog = "0.6.3"
dialoguer = "0.5"
dirs = "3.0"
If you want "I don't care, something simple that just works", you can definitely do worse than egui.
I really don't like immediate mode UI toolkits, but egui is bearable and gets the job done. Just don't try and design much more than a simple layout, or you'll be tearing your hair out.
Honestly, I've used it for a few small projects, mostly because everything else I've tried has made me want to go back to Visual Basic. As in the old one from the late 90's.
If you're finding a mature, abundant GUI framework that compiles to major platforms with beautiful UI and widgets, Flutter is for you. This Rust-In-Flutter connect best of two worlds with well-organized documentation. 10 minutes would be enough if you have Flutter SDK on your system.
Your app doesn't use clap or even any command line arguments so klask will be useless to you I'm afraid (FYI gooey needs to read your argparser to work).
But I thought it was cool that there is something like gooey in rust, albeit a bid old.
If you're very lucky, someone who is a bit further ahead in their learning about rust GUIs and wants practice might like to write a GUI for you.
Get your code on GitHub in a public repo so people can make pull requests against it.
I have no idea how this could be so urgent but if it is offer some money! I'm sure someone would love to add "professional rust GUI developer" to their cv
Having said that, this forum will always be happy to help you learn things. It sounds like you don't have the time right now though. When's your homework due deadline?
My opinion might be unpopular, but this seems fit for a lightweight webservice. Just serve 2 endpoints: input form and search results. Depending on your programming background, this approach might get you working product much faster than "classical" GUI.
I say that aware of my deep hate toward Electron and similar technologies - you have also https://tauri.app, which was already mentioned here. However, a simple Actix-web server should suffice.
Unfortunately there isn't really a fast way to learn GUI programming completely from scratch.
It's fundamentally event driven, asynchronous, stateful and has heterogeneous data flow cycles. It doesn't help that Rust is fundamentally terrible at all of these things , but they are logically all hard to get used to from a CLI, procedural flow background.
Further, we've all built up a lot of experience using GUIs and formed a lot of expectations about what is a "not broken" interface that turns out to be a lot of additional work you wouldn't expect otherwise. Simple things like being able to go back at any time, scrolling up and down when there's more items that fit in screen, making buttons only enable when they would actually work, even just showing errors requires a lot more work. Libraries can give you better or worse tools to solve these problems, but often the trade-off is learning more and weirder ways to contort your logic away from traditional code flow.
In other words, the library's not really the hard bit, most of the time. Immediate mode libraries like egui are certainly your best bet if you've never done GUI work before and need to get something done quick, but it only solves half of these issues, and it has lots of "fun" traps of it's own to fall into.
Surprisingly it's not homework, but a family friend is paying me too have it done by September 2nd. I wish I could offer money if I wasn't so broke hehe.
Okay, so not so much "jumping in at the deep end" as "jumping into shark-infested waters while covered in chum wearing a T-shirt saying 'sharks r losers' and screaming obscenities about marine life"... when you don't know how to swim.
Good luck
(No sarcasm, by the way. You learn the way that works for you.)
The way something like egui works (that is, an immediate mode toolkit), the update function runs every frame and is responsible for both drawing the interface, and handling all user interaction.
So, every frame, it calls text_edit_singleline with a mutable reference to the name field. The text_edit_singleline function is responsible for drawing the text box, tracking where in the string the user is editing, and making any changes directly.
If you want to know what interface elements are available, most of them are listed in the documentation for egui::containers, and egui::widgets. Although, you're generally expected to use shortcut methods available on the egui::Ui type where available. (That's what's being done in the example.)
You can also look for functionality in the live egui demo, then check the source code for the demo to see how it's done. The "widget gallery" window in the demo has a direct link to its own source down the bottom.
For a long time GUIs in the browser in Javascript were event driven, there was no async in JS, there still is not threads in JS. Ergo I conclude jour assertion of "event driven [AND] asynchronous" is required is not necessarily true.
As for stateful... Sure state has to be kept somewhere. Pure HTML pages with forms, text fields, buttons etc are stateless, immediate mode GUI's are stateless. Games are the ultimate GUI's and are there GUI's are stateless. Ergo I conclude the need for "stateful" is not necessarily true.
I have no idea what you mean by "heterogeneous data flow cycles".
Finally Mozilla adopted Rust to implement code in their browser. Browsers are up there with games as the "ultimate GUI", are you saying that was a "fundamentally terrible" idea that is doomed to failure. That seems very unlikely to me.
I feel you're not really taking this statement as intended; I'm not making statements about GUI libraries, but GUIs themselves; that's what I meant by "inherently".
They are asynchronous, because your users will expect that you don't lock up whenever you do anything that takes any amount of time.
They are stateful, because they directly represent a "current", often mutable value. Games absolutely have stateful rendering, they just don't often use stateful rendering libraries, eg you're holding the state, not the library.
Heterogeneous data flow cycles does need to be broken down:
data flow is how state changes in one place in your application affects another place
data flow cycles are where this flow forms a cycle, and therefore you must ensure your state settles
heterogeneous cycles are those that are not within one container or data shape, and therefore cannot use a single and simple approach to ensure that this settling is correct.
Remember, none of these are about a library, but the actual interface you're building.
And saying that Rust is "fundamentally terrible" at these things isn't saying that it's a fundamentally terrible idea to use Rust for GUIs, just that Rust makes these things much harder in lots of little ways. Relax, Rust's feelings weren't hurt, I'm a good friend and it knows I say these things with love.